Workshop 的 slides

驚鴻一瞥:倉頡程式語言校園行

日期:
分類: 驚鴻一瞥
標籤: 倉頡程式語言

劉俊傑是華為倉頡編譯與執行時團隊的核心成員,曾參與倉頡編譯器和執行時的設計與開發,目前主要負責倉頡的生態建設。以下內容為其發言概要。

不知道大家是否聽說過,中國早期有一家做手機的公司叫做波導。那時候華為甚至還沒開始做手機。波導公司的創始人及總工程師趙建東先生今天也來到了現場。

趙總之所以來到這裡,是因為他認為中國自主研發程式語言這件事情非常有價值和意義。趙老師現在也是一家晶片公司——視芯科技的老闆。他平時並未停歇,還在業餘時間開發了自己的程式語言,叫做「發言」語言。趙老師認為做這件事的長期價值在於,透過我們未來的努力,去打破西方在一些核心技術上的壟斷,培養我們自己的人才,同時這對大家未來的工作發展也非常有幫助。他懷著一種公益的心態來做這次分享,也想借此機會鼓勵同學們,因為大家未來都可能成為非常優秀的工程師和創業者。

趙建東自述

按東南商報 2005 年 8 月 13 日文《寧波科教界授最高獎 4位科教精英每人重獎60萬元》:

趙建東,男,1965 年出生,高階工程師。1990 年畢業於蘭州大學,獲理學碩士學位;1991 年出任清華大學訪問學者;1992 年任美國協和集團順霸研究院(珠海)射頻室主任;1992年底至今先後任寧波波導公司研發部經理、波導研究院院長、副總工程師、總工程師。

趙建東是波導公司創始人之一。作為公司技術負責人,多年來一直在科研一線從事技術開發工作,並帶領公司科技人員積極研究新技術,開發新產品。先後開發了中文F系列、數字K系列尋呼機和800系列、900系列、S系列GSM、C系列CDMA手機,其中指紋識別智慧手機是國內首創開發的。

以下內容為趙建東自述概要。

我從 1983 年進入蘭州大學學習物理學,畢業後保送本校研究生,畢業論文是量子光學方面雙光子鐳射器。1990 年畢業後,進入軍工研究所,主要從事轟炸雷達、制導雷達等相關的研發工作,並曾與清華大學電子工程系合作開發專案。

到了 90 年代初,我被師兄說動,下海去了珠海一家美國投資的企業。後來,我創辦了波導公司。大家可能對「波導手機,手機中的戰鬥機」這句廣告詞還有印象,那是我們那個時代的產品。

在波導期間,大約是 2005 年左右,我們實際上做了兩年手機作業系統。我們還和浙江大學計算機學院聯合主辦了全國性的手機軟體大賽,每年吸引很多團隊到杭州參賽,我們會收購前十名的團隊或作品,並給予獎金。當時,我們就在探索手機軟體的下載和安裝模式。那時的網路環境還很差,透過 WAP 下載一個幾百K的軟體需要很長時間,流量費也很高,幾乎不可用。後來 GPRS 出現,速度提升到 100-200 Kbps,情況才有所好轉。

那段時期,國產手機經歷了激烈的競爭,我們算是最後退出的那一批。之後,我又和朋友創辦了現在的世芯科技,主要做晶片設計。我想強調的是,我們過去很多技術是跟隨性的,缺乏底層創新。晶片設計行業毛利率能達到 35-40%,這是一個創新驅動的領域。因為對作業系統和程式語言的持續興趣,我瞭解到華為正在做的倉頡語言和鴻蒙作業系統。我認為這兩項工作具有開創性,可能會改變我們國家資訊產業未來的發展方向。這也是我今天來到這裡的原因。

在過去十幾年的時間裡,我自己也斷斷續續地開發了一個類似 C# 的程式語言,叫做 Fine,目前已經比較系統化了,它內建了 GUI、資料庫和網路通訊等功能,是一個整合化的開發環境。

我的業餘時間主要有兩個愛好,打彈弓和寫程式碼。很少參加此類活動,今天主要是來和大家簡單交流一下。

技術與生態現狀彙報

這一部分的 slides 可以檢視可畫(canva.cn)

首先,我們簡單回顧一下程式語言的發展歷史。最早的 C 語言誕生於上世紀70年代,當時計算機硬體資源有限,C 語言憑藉其高效、靈活和貼近硬體的特點,迅速成為開發作業系統、編譯器等系統軟體的首選。

80年代,C++ 在 C 的基礎上引入了物件導向程式設計,就像從用磚塊蓋房子,變成了用預製好的門、窗、屋頂等模組來組裝,極大地提升了大型程式的開發效率和可維護性。

90年代,Java 和 Python 等語言嶄露頭角。Java 以其「一次編寫,到處執行」的跨平臺特性,在企業級應用和安卓開發中非常流行,目前大部分安卓應用仍是Java開發的。Python則因其語法簡潔、庫豐富,在資料科學和人工智慧領域得到了廣泛應用。

進入21世紀後,Go、Rust、Swift、Kotlin 等現代程式語言相繼出現,它們通常用於雲端計算、分散式系統等領域,在併發處理、工具鏈等方面相較於之前的語言有了很大進步。

那麼,在當前智慧化、萬物互聯的時代背景下,下一代程式語言應該具備哪些特性呢?華為研發的倉頡語言,正是面向下一代程式語言進行探索,在智慧化、高效率、安全可信以及易擴充等方面做了深入研究。

回顧倉頡的發展歷程,專案於 2019 年啟動,2020 年正式命名為「倉頡」,寓意著像倉頡造字一樣,希望這門語言能被廣大開發者喜愛並廣泛使用。2022 年,倉頡語言首次在華為自研的 HarmonyOS 路由器上首次商用,替換原有的 Go 模組(倉頡在併發策略上參考的 Go 語言)。由,因此在首次商用中表現亮眼,效能有顯著提升。

2023年,倉頡語言開始與國內多家頭部企業展開深入合作,例如與中航、國家電網等在一些重要場景進行商業驗證。整個研發過程中,國家也給予了大力支援。程式語言作為軟體產業的根技術,研發自主可控的倉頡語言,有助於我們在核心技術上掌握主動權,尤其是在當前日益嚴峻的國際形勢和科技競爭背景下,可以防範未來可能出現的「卡脖子」風險。

除了戰略層面的考量,倉頡語言也是構建鴻蒙生態的重要一環。就像蘋果的 Swift/Objective-C 支撐了 iOS 生態,谷歌的 Kotlin/Java 支撐了 Android 生態一樣,鴻蒙作為國產作業系統,也需要有自己的原生開發語言來構建繁榮的生態。

2024 年是倉頡語言發展的重要一年,工商銀行和力扣發布了使用倉頡編寫的原生鴻蒙應用。其中,有道的應用是完全使用倉頡從零開始編寫的,這證明了倉頡語言目前已經具備了開發完整應用的能力。在 6 月的華為開發者大會(HDC)上,倉頡語言將正式對外發布,屆時開發者可以透過IDE外掛等方式來使用倉頡語言。

接下來,介紹一下倉頡語言的主要技術特性,可以概括為智慧化、全場景、高效能和強安全。

在效能方面,與目前安卓開發主流的 Java 語言相比,倉頡具有先天優勢。倉頡是 AOT 到原生機器碼的,相比 Java 需要透過虛擬機器解釋或 JIT,減少了執行時的翻譯開銷,因此執行速度更快。

傳統的編譯器,如 GCC、Clang 等,通常採用一體化設計,從預處理、詞法分析、語法分析、語義分析到最終程式碼生成,整個流程是耦合在一起的。這種方式的侷限性在於,為不同語言開發編譯器都需要從頭構建整個系統。倉頡的編譯器架構是基於 LLVM 的模組化設計。它將編譯過程劃分為前端、IR 和後端。前端負責將不同語言的原始碼轉換成統一的中間表示(IR),類似於將各種食材加工成半成品。中端 IR 層是核心,它是一種與具體語言和目標機器無關的表示形式,可以在這個層面上進行各種通用的最佳化。後端則負責將最佳化後的 IR 生成特定目標機器的機器碼。理論上,IR 到機器碼的後端部分可以直接複用開源的 LLVM。不過,由於倉頡語言有一些獨特的設計,比如參考了 Go 的協程,以及 Actor 併發模型,以及它的一些內建型別和記憶體管理特性,我們需要對 LLVM 的某些部分進行改造和擴充來適配這些需求(CJNative LLVM)。

在垃圾回收方面,倉頡採用了全併發分代垃圾回收機制。相比於傳統的標記-清除演算法可能導致的長時間 stop-the-world,分代 GC 能更有效地管理記憶體,減少 GC 停頓時間,從而降低應用程式的卡頓感,提升使用者體驗。鴻蒙原生 Markdown 元件(倉頡實現)渲染效果優於安卓版(Kotlin),且不掉幀。在 IO 密集型場景(如網路請求載入圖片)下,倉頡的協程能夠充分發揮優勢,避免執行緒阻塞,提高吞吐量和響應速度。(倉頡與 ArkTS 對比影片)和 ArkTS 的對比測試中,使用倉頡實現的版本在啟動速度和滑動流暢度上均優於 ArkTS 版本。

倉頡語言的另一個重要特性是「天生全場景」,在執行態是指有輕量物件佈局、輕量執行時庫、輕量使用者執行緒、輕量回棧的特性。

天生全場景另一方面在於語言層面。由於技術變化,透過語法擴充,倉頡可以更好地適應各種新的硬體或軟體架構。以及不同的領域對於不同的需求是不一樣的。一個簡單的例子是,透過給變數增加一個類似 @state 的修飾符,就可以讓這個變數具有響應狀態變化的能力。當它的值改變時,自動觸發 UI 更新,而不需要編寫額外的監聽或回撥程式碼。

倉頡語言還積極擁抱 AI Agent(智慧體)開發。使用 AgentDSL,開發者可以藉助運算子,用非常簡潔、接近自然語言的方式來與智慧體的對話,而無需編寫大量複雜的底層程式碼。

在安全性方面,倉頡也做了很多設計,例如編譯期空安全檢查、預設資料不可變性、陣列越界檢查等等。這些特性旨在減少開發過程中的常見錯誤,提高程式碼的健壯性和安全性。倉頡語言及其執行時已經獲得了業界權威的安全認證。

如果大家想學習和了解倉頡,可以透過以下途徑獲取資源。倉頡專案目前主要託管在 Gitcode 平臺,包括編譯器、標準庫、文件以及第三方庫等。官方網站也提供了豐富的學習資料和開發者社羣入口。我們非常歡迎同學們未來能參與到倉頡的開源社羣中,貢獻程式碼和應用案例。

從 PL 領域看倉頡

剛剛提到了很多倉頡的特性,從程式語言(PL)領域的角度來看,語言設計是一個核心話題。國內高校很多課程側重於編譯器實現,這是一個偏工程的領域,但也涉及到一些理論,比如形式語言、自動機理論等。今天我嘗試從另一個維度,即領域特定語言(DSL)的視角,來通俗地解讀一下語言設計的一些趨勢,以及倉頡在這方面的考慮。

程式語言的發展,從機器語言、組合語言到高階語言,本質上是一個抽象層次不斷提高的過程。抽象層次越高,語言表達能力越強,越接近人類自然語言,開發效率通常也越高。但代價是可能損失一些底層的控制力和效能,同時,構建更高層次抽象所需的技術和時間成本也可能更高。

當通用程式語言用於解決特定領域的問題時,往往需要編寫很多與領域核心邏輯無關的「模板程式碼」或「膠水程式碼」。為了提高特定領域的開發效率和表達力,就產生了 DSL。DSL 是專門為某個特定領域設計的語言,它的語法和語義都緊密圍繞該領域的需求。例如,SQL是資料庫查詢的DSL,HTML 是網頁結構的 DSL。DSL 的優點是在其適用領域內非常高效、簡潔、易於理解和維護,但缺點是通用性差,無法用於其設計領域之外的問題。

業界實踐中,我們觀察到一種趨勢:在各種領域,都存在對DSL的需求。

  1. 資料庫互動:早期使用 JDBC 等 API,需要手動編寫連線管理、SQL 語句構造、結果集對映等大量程式碼。後來出現了 ORM 框架,透過註解提供了一種 DSL。開發者只需在程式碼實體類和成員變數上新增註解,就能描述程式實體與資料庫表、欄位之間的對映關係,框架會自動處理底層的資料庫操作。這種基於註解的 DSL,相比於純 Java 程式碼,極大地簡化了資料持久化操作。
  2. 程序間通訊:傳統的 IPC 方式需要開發者處理複雜的序列化、反序列化、協議定義、連線管理等細節。後來出現了像 Android 的 AIDL(Android Interface Definition Language)這樣的 IDL(介面定義語言)。開發者使用 AIDL 這種 DSL 來定義介面,工具鏈會自動生成底層的 IPC 程式碼。但這樣只能透過在 Java 的基礎上外掛實現,而非語言本身。
  3. UI 開發:傳統的 UI 開發(如早期使用 C++ 的 MFC 或 Qt)需要編寫大量命令式程式碼來建立、佈局、設定樣式和處理事件,UI 結構、樣式和邏輯程式碼常常混雜在一起。後來發展出宣告式 UI 框架,如 XML 佈局,以及現代的 SwiftUI 等。這些框架提供了一種 DSL,讓開發者能夠更直觀地描述 UI 的最終狀態和結構,框架負責渲染和狀態管理,遮蔽了無關的底層細節,顯著提高了程式碼的可讀性和定製化能力。
  4. 互操作:例如 JS 呼叫 C 時需要去寫 NAPI,需要調系統介面,做各種型別轉換、異常處理。現代框架通常使用註解或更簡潔的語法。相比複雜的 JNI/JNA,一些現代語言提供了更簡潔的 FFI 機制。

總結這些案例,我們可以看到 DSL 在提升特定領域開發效率上的巨大價值。倉頡語言針對這種趨勢,主要透過兩種方式來支援 DSL:

  1. 原生整合:對於一些非常通用且重要的領域,倉頡在語言層面直接內建了相應的語法和語義支援。這可以看作是一種高度最佳化的、內建的 DSL。
    • 在 C 語言互操作方面,倉頡透過宣告式的語法,允許開發者以類似本地函式呼叫的方式直接宣告和使用C函式。呼叫一個 C 函式只需一行宣告,極大地簡化了開發流程。
    • 在併發方面,傳統語言通常透過呼叫第三方庫或標準庫實現併發,缺乏語言層面的支援。而倉頡參考了 Go 語言的協程框架,將併發機制內建於語言中,透過關鍵詞 spawn 實現輕量級執行緒的自動管理和排程。
  2. 擴充機制:對於像資料庫、化工這些不那麼通用,或者需要高度定製化的領域,倉頡提供了語言和編譯器的擴充機制。開發者可以透過宏或其他語法擴充方式,在倉頡語言內部定義新的語法結構,實現所謂的 EDSL,即「嵌入式 DSL」。這種方式的好處是,開發者不需要編寫獨立的編譯器或解析器,可以直接利用倉頡的基礎設施進行擴充,並且擴充後的 DSL 可以與倉頡程式碼無縫整合。這與像 Android 在 Java 外掛外掛實現 DSL 的方式有所不同。

所以,從 DSL 的視角來看,倉頡的設計哲學是在通用層面提供強大的基礎能力和內建 DSL 支援,同時賦予開發者透過 EDSL 機制為特定領域量身定製高效表達方式的能力。這種設計不僅滿足了現代軟體開發對領域專用表達的訴求,也體現了倉頡作為下一代程式語言的前瞻性,在實際應用中展現了顯著優勢。有一個國產適配專案需要遷移 4000 多個 C 介面,倉頡透過宣告式的互操作機制,將這一過程簡化為簡單的函式宣告,大幅提高了遷移效率。Agent DSL為智慧體開發提供了簡潔的表達方式,開發者可以透過接近自然語言的語法描述智慧體行為,無需深入編寫複雜邏輯,從而進一步降低了開發門檻。

動手實踐環節

第一個實踐題目:併發與系統呼叫

這個題目與我們剛剛討論的併發和系統呼叫(特別是 FFI)有關。程式執行後會彈出一個空白的 Windows 視窗。程式碼內部已經使用倉頡呼叫 GDI 註冊了視窗類、建立了視窗,並處理了訊息迴圈,畫圖的基礎框架已經搭好。

任務:在這個視窗裡畫出一條正弦曲線。具體來說,你需要找到程式碼中預留的位置,新增幾行倉頡程式碼,呼叫 Windows 的 SetPixel 函式來繪製點。SetPixel 函式的原型可以在微軟的 MSDN 文件中查到,或者參考程式碼中已有的其他 API 呼叫示例。你需要關注它的引數:第一個是裝置上下文控制代碼(hDC),可以模仿現有程式碼獲取;後面兩個是 座標(整型);最後一個是顏色值(COLORREF,也是一個整型)。

Cangjie

你需要編寫一個迴圈,計算 對應的 值,注意可能需要進行型別轉換(比如從浮點數轉為整型),然後呼叫 SetPixel 在視窗的對應位置畫點。程式碼中已經匯入了所需的 Windows API 函式,可以直接呼叫。倉頡呼叫 C 函式時,通常需要將呼叫程式碼放在一個用 @ccall 修飾的程式碼塊中,這是一種語法標記,提示開發者這裡可能涉及不安全的記憶體操作,並指導編譯器進行一些檢查。

最終完成的程式碼量大概只需要幾行。這個練習旨在讓大家體驗倉頡 FFI 簡潔性以及基本的程式設計。


查 M$ 文件:

COLORREF SetPixel(
  [in] HDC      hdc,
  [in] int      x,
  [in] int      y,
  [in] COLORREF color
);

寫出:

src/api.cj
  foreign func BeginPaint(hWnd: Handle, ps: CPointer<PAINTSTRUCT>): Handle
  foreign func EndPaint(hWnd: Handle, ps: CPointer<PAINTSTRUCT>): Bool
  foreign func GetClientRect(hWnd: Handle, rc: CPointer<RECT>): Bool
  foreign func Ellipse(hDC: Handle, left: Int32, top: Int32,
      right: Int32, bottom: Int32): Bool
  // 提示1:在這裡宣告繪圖所需的 SetPixel 函式原型
+ foreign func SetPixel(hDC: Handle, x: Int32, y: Int32, color: UInt32): UInt32

這個檔案裡面還有些下面會用到的定義:

@C
struct POINT {
    public var x: Int32 = 0
    public var y: Int32 = 0
}
 
@C
struct RECT {
    public var left: Int32 = 0
    public var top: Int32 = 0
    public var right: Int32 = 0
    public var bottom: Int32 = 0
}
 
@C
struct PAINTSTRUCT {
  public var hDC: Handle = NULL
  public var fErase = true
  public var rcPaint = RECT()
  // 以下欄位保留,系統在內部使用
  public var fRestore = false
  public var fIncUpdate = false
  public var rgbReserved = VArray<Byte, $32> { _ => 0 }
}
src/main.cj
// 基於 CFFI 的 Windows GUI 程式設計
package windows
import std.math.sin // 匯入標準庫中的 sin 數學函式
 
unsafe main() {
    let instance = GetModuleHandleA(EMPTY_STRING)
    // 註冊視窗類
    let className = LibC.mallocCString('Cangjie Window')
    var windowClass = WNDCLASSEX(lpszClassName: className,
        hInstance: instance,
        lpfnWndProc: onMessage,
        hbrBackground: CreateSolidBrush(0x0095D6C0) // 中國傳統色 歐碧
    )
    if (RegisterClassExA(inout windowClass) == 0) {
        println('RegisterClass Failed: ${GetLastError()}')
        return
    }
    // 建立視窗例項
    let windowName = LibC.mallocCString('Cangjie')
    let window = CreateWindowExA(
        0,                                   // 擴充樣式
        className,                           // 視窗類名
        windowName,                          // 視窗標題
        WS_OVERLAPPEDWINDOW,                 // 視窗風格
        CW_USEDEFAULT, CW_USEDEFAULT,        // 視窗位置
        365, 365,                            // 視窗大小
        NULL,                                // 父視窗控制代碼
        NULL,                                // 選單控制代碼
        instance,                            // 例項控制代碼
        NULL                                 // 附加引數
    )
    if (window.isNull()) {
        println('CreateWindow Failed: ${GetLastError()}')
        return
    }
    // 顯示視窗
    ShowWindow(window, SW_SHOWNORMAL)
    UpdateWindow(window)
    // 啟動訊息迴圈
    var message = MSG()
    while (GetMessageA(inout message, NULL, 0, 0)) {
        TranslateMessage(inout message)
        DispatchMessageA(inout message)
    }
    // 退出訊息迴圈
    println('Out of Message Loop')
    LibC.free(className)
    LibC.free(windowName)
}
 
func paint(hWnd: Handle, draw: (hDC: Handle) -> Unit) {
    var ps = PAINTSTRUCT()
    let hDC = unsafe { BeginPaint(hWnd, inout ps) }
    draw(hDC)
    unsafe { EndPaint(hWnd, inout ps) }
}

DefWindowProcA 是 Windows 提供的預設視窗過程,當視窗大小改變後,DefWindowProcA 通常會使視窗的客戶區無效,從而導致 Windows 傳送 WM_PAINT 訊息。此時獲取 hDC 後使用一個迴圈(比如 for 迴圈)遍歷視窗的寬度作為 座標。

  unsafe func process(hWnd: Handle, msg: UInt32,
          wParam: UInt64, lParam: UInt64) {
      var result = 0
      if (msg == WM_PAINT) {
          paint(hWnd) { hDC =>
              var rect = RECT()
              GetClientRect(hWnd, inout rect)
              // 提示2:在這裡新增繪圖程式碼,繪製正弦曲線 y = 60 * sin(0.1 * x)
+             for (x in rect.left..rect.right) {
+                 let y = 60.0 * sin(0.1 * Float64(x))
+                 SetPixel(hDC, x, Int32(y) + rect.bottom / 2, 0x000000)
+             }
          }
      } else if (msg == WM_KEYDOWN && wParam == UInt64(VK_ESCAPE)) {
          DestroyWindow(hWnd)
      } else if (msg == WM_DESTROY) {
          PostQuitMessage(0)
      } else {
          result = DefWindowProcA(hWnd, msg, wParam, lParam)
      }
      return result
  }

需要調整 座標使其適應視窗座標系,例如,將原點移到視窗中央。

畫出的正弦函式
畫出的正弦函式

最後,在 @ccall 塊內呼叫 SetPixel(hdc, x, y, color)

@C
@CallingConv[STDCALL]
func onMessage(hWnd: Handle, msg: UInt32,
        wParam: UInt64, lParam: UInt64): Int64 {
    unsafe { process(hWnd, msg, wParam, lParam) }
}

這裡的 hDC 型別在倉頡中可能用一個別名(如 Handle)表示,它本質上是一個指標或整數。整型引數可以直接傳遞。顏色可以用 RGB(r, g, b) 宏(如果匯入了)或者直接用整型。

第二個實踐題目:倉頡智慧體框架

倉頡的智慧體框架 AgentDSL 的核心思想是將大型語言模型(LLM)的「說話」能力轉化為「做事」能力。LLM 理解自然語言、知識和推理能力很強,可以規劃任務步驟。我們只需要編寫程式,提取 LLM 輸出的文字中的意圖和引數,然後呼叫實際的裝置驅動或 API,就能讓 LLM「指揮」程式執行任務。

業界已有一些框架(如 LangChain)透過 API 呼叫的方式實現類似功能。倉頡 AgentDSL 的特色在於它採用了「宣告式」的方式。開發者不需要編寫大量的介面呼叫和初始化程式碼,而是透過註解來定義智慧體及其能力。

例如,在一個類上使用 @Agent 註解,這個類就具備了與 LLM 互動的基礎能力。你可以直接呼叫它的 chat 方法進行對話。如果在類中定義一些屬性或方法,可以設定 Agent 的角色或初始狀態。定義函式時可以用 @Tool 註解修飾,包含兩個引數:一個是描述這個工具(函式)能做什麼,另一個是描述它的引數各自代表什麼。

當使用者與 Agent 互動時,框架會自動將這些 @Tool 的描述資訊整合到傳送給 LLM 的 prompt 中。LLM 在理解使用者意圖後,會決定呼叫哪個工具以及傳遞什麼引數,並以特定格式返回給框架。框架解析LLM的回覆,然後實際執行對應的函式呼叫。

例子:一個智慧家居助手,透過 @Agent 定義助手,用 @Tool 定義控制燈光、空調等的函式。使用者說「把客廳燈開啟」,LLM理解後指示框架呼叫「開燈」函式,並附帶引數「客廳」。

任務:使用 AgentDSL 來控制一個模擬的魔方。我們提供了一個基礎的魔方程式(在控制檯列印魔方的狀態),它有一個 Cube 類,可以透過呼叫其成員函式(如 turn(face, direction))來轉動不同的面。引數 face 用字母表示(如 F, B, L, R, U, D),direction 表示順時針或逆時針。

期望:執行程式後,在控制檯輸入指令,程式能正確解析並呼叫對應的魔方轉動函式,並列印出轉動後的魔方狀態。

  package agent
  
  import magic.dsl.*
  import magic.prelude.*
  import magic.config.Config
  
  @agent[model: "ark:deepseek-v3-250324"]
  class CubeAgent {
      // 提示1: 呼叫 cube.transform('F', true) 可以將魔方正面逆時針旋轉 90 度
      // 字母 F, B, L, R, U, D 分別表示魔方的前、後、左、右、上、下 6 個面
      let cube = Cube()
 
      @prompt(
          // 提示2: 在這裡新增 Agent 提示詞,讓 AI 熟悉業務場景,例如它的職責和魔方各面的字元定義等
+         "你是一個魔方大師專家,負責控制魔方的旋轉和輸出魔方的展開圖。\n魔方的各個面用字母表示:字母 F, B, L, R, U, D 分別表示魔方的前、後、左、右、上、下 6 個面"
      )
 
      @tool[description: "獲取魔方當前狀態下的展開圖"]
      func now(): String {
          cube.toString()
      }
 
      // 提示3: 在下面新增兩個 @tool 修飾的函式,讓 AI 可以控制魔方的旋轉
      // 兩個函式分別控制順時針和逆時針旋轉,函式引數指定具體旋轉哪個面
+     @tool[description: "順時針旋轉魔方的某個面"]
+     func rotate(face: String) {
+         cube.transform(face, false)
+     }
+
+     @tool[description: "逆時針旋轉魔方的某個面"]
+     func rotateCounter(face: String) {
+         cube.transform(face, true)
+     }
  }
 
  main() {
      Config.env["ARK_API_KEY"] = "[redacted]"
      Config.maxReactNumber = 100
      let agent = CubeAgent()
      agent.chat("順時針旋轉正面 2 次,逆時針旋轉頂面 1 次,輸出魔方展開圖") |> println
  }

根據演講者,部分 LLM 對於一些布林引數支援得不是很好,所以推薦寫兩個函式。

第三個實踐題目:併發網路程式設計與 AI 結合

最後一個題目結合了網路程式設計、併發和AI。倉頡編寫 TCP 通訊和建立協程(用於處理併發連線或任務)的程式碼非常簡潔。提供的程式碼支援多客戶端連線一個服務端。服務端接收控制檯輸入,並將訊息廣播給所有連線的客戶端。客戶端接收控制檯輸入,並將訊息傳送給服務端。同時還提供了一個獨立的倉頡模組(llm.cj),封裝了與大語言模型聊天的功能。這個模組提供了一個類,可以建立例項並呼叫其 chat 方法(一次性獲取回覆)或 stream_chat 方法(流式獲取回覆)來進行對話。可直接執行這個模組體驗與 AI 聊天。

任務:修改客戶端和服務端程式,讓它們不再接收控制檯的使用者輸入,而是各自建立一個 LLM 例項,然後透過網路互相傳送訊息,實現兩個 AI 自動聊天的效果。

步驟

  1. 在服務端和客戶端的程式碼中,import 我們提供的 llm.cj 模組。
  2. 在各自的程式初始化部分,建立LLM類的例項。可以給它們設定不同的角色(比如一個扮演賈寶玉,一個扮演林黛玉),透過初始化時的 prompt 來實現。
  3. 修改網路訊息處理邏輯:
    • 當客戶端收到服務端的訊息後,不再列印到控制檯,而是將這個訊息作為輸入,呼叫自己的 LLM 例項的 chat 方法獲取回覆。然後將 LLM 的回覆透過網路傳送給服務端。
    • 當服務端收到客戶端的訊息後,同樣呼叫自己的 LLM 例項獲取回覆,然後將回復透過網路傳送回該客戶端(或者廣播給所有客戶端,取決於你想要的效果)。
  4. 需要一個啟動機制,比如讓客戶端在連線成功後,主動傳送第一句話(可以是一個固定的問候語,或者呼叫 LLM 生成一句開場白)給服務端,來觸發對話的開始。
  5. 編譯時需要將 llm.cj 檔案與客戶端、服務端程式碼一起編譯,因為它們現在互相依賴。執行命令可能需要指定主模組入口:
    build.bat
    cjc client.cj llm.cj -o client.exe
    cjc server.cj llm.cj -o server.exe

期望:啟動服務端和客戶端後,它們能夠透過TCP連線,自動地進行一輪又一輪的對話,並將對話內容列印在各自的控制檯上(或者你可以選擇不列印)。

這個題目的核心在於將原來處理標準輸入輸出的地方,替換成處理網路收到的訊息和呼叫LLM獲取回覆。在客戶端,收到網路訊息後,reply = llm.chat(received_message),然後 socket.send(reply)。在服務端,收到某個客戶端的訊息後,reply = llm.chat(received_message),然後 client_socket.send(reply)。需要注意處理好非同步接收和傳送的邏輯,以及對話的啟動。

package chat
 
import std.console.Console
import std.socket.*
 
func startInputListener(client: TcpSocket) {
    spawn { // 在新執行緒中接收控制檯輸入併傳送到對端
        while (true) {
            // ...
        }
    }
}
 
main() {
    const IP = "127.0.0.1"
    const PORT: UInt16 = 23456
    const BUFFER_SIZE = 1024
 
    // 使用 SiliconFlow 提供的服務介面
    let robot = LLM(
        url: 'https://api.siliconflow.cn/v1/chat/completions',
        key: 'sk-[redacted]',
        model: 'Pro/deepseek-ai/DeepSeek-V3',
        memory: true
    )
 
    robot.preset([(System, '我會用林黛玉的風格回覆哥哥的所有問題')])
 
    let client = TcpSocket(IP, PORT)
    client.connect() // 和服務端建立連線
    startInputListener(client)
    while (true) { // 在迴圈中不斷接收服務端發來的訊息並列印
        let data = Array<Byte>(BUFFER_SIZE, item: 0)
        client.read(data)
        println(String.fromUtf8(data))
        let res = robot.chat(String.fromUtf8(data))
        client.write(res.toArray())
    }
}

獎品

寫得最快的同學可以獲得一個華為手環
寫得最快的同學可以獲得一個華為手環

不過博主獲得的是一個比較有崛起風格的U盤:

正面
正面
背面印有 logo
背面印有 logo

另外,反饋說倉頡的鴻蒙部分即將開源。© 新世界的大門

評論

評論將在稽覈後顯示,閣下可以在本部落格的 Github 倉庫的 拉取請求列表 中檢視。提交成功後會自動跳轉。

本站不支持 Dark Reader 的暗色模式,请对本站关闭后再访问。
(亮色模式的对比度、亮度等选项不受影响)