劉俊傑是華為倉頡編譯與執行時團隊的核心成員,曾參與倉頡編譯器和執行時的設計與開發,目前主要負責倉頡的生態建設。以下內容為其發言概要。
不知道大家是否聽說過,中國早期有一家做手機的公司叫做波導。那時候華為甚至還沒開始做手機。波導公司的創始人及總工程師趙建東先生今天也來到了現場。
趙總之所以來到這裡,是因為他認為中國自主研發程式語言這件事情非常有價值和意義。趙老師現在也是一家晶片公司——視芯科技的老闆。他平時並未停歇,還在業餘時間開發了自己的程式語言,叫做「發言」語言。趙老師認為做這件事的長期價值在於,透過我們未來的努力,去打破西方在一些核心技術上的壟斷,培養我們自己的人才,同時這對大家未來的工作發展也非常有幫助。他懷著一種公益的心態來做這次分享,也想借此機會鼓勵同學們,因為大家未來都可能成為非常優秀的工程師和創業者。
趙建東自述
按東南商報 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的需求。
- 資料庫互動:早期使用 JDBC 等 API,需要手動編寫連線管理、SQL 語句構造、結果集對映等大量程式碼。後來出現了 ORM 框架,透過註解提供了一種 DSL。開發者只需在程式碼實體類和成員變數上新增註解,就能描述程式實體與資料庫表、欄位之間的對映關係,框架會自動處理底層的資料庫操作。這種基於註解的 DSL,相比於純 Java 程式碼,極大地簡化了資料持久化操作。
- 程序間通訊:傳統的 IPC 方式需要開發者處理複雜的序列化、反序列化、協議定義、連線管理等細節。後來出現了像 Android 的 AIDL(Android Interface Definition Language)這樣的 IDL(介面定義語言)。開發者使用 AIDL 這種 DSL 來定義介面,工具鏈會自動生成底層的 IPC 程式碼。但這樣只能透過在 Java 的基礎上外掛實現,而非語言本身。
- UI 開發:傳統的 UI 開發(如早期使用 C++ 的 MFC 或 Qt)需要編寫大量命令式程式碼來建立、佈局、設定樣式和處理事件,UI 結構、樣式和邏輯程式碼常常混雜在一起。後來發展出宣告式 UI 框架,如 XML 佈局,以及現代的 SwiftUI 等。這些框架提供了一種 DSL,讓開發者能夠更直觀地描述 UI 的最終狀態和結構,框架負責渲染和狀態管理,遮蔽了無關的底層細節,顯著提高了程式碼的可讀性和定製化能力。
- 互操作:例如 JS 呼叫 C 時需要去寫 NAPI,需要調系統介面,做各種型別轉換、異常處理。現代框架通常使用註解或更簡潔的語法。相比複雜的 JNI/JNA,一些現代語言提供了更簡潔的 FFI 機制。
總結這些案例,我們可以看到 DSL 在提升特定領域開發效率上的巨大價值。倉頡語言針對這種趨勢,主要透過兩種方式來支援 DSL:
- 原生整合:對於一些非常通用且重要的領域,倉頡在語言層面直接內建了相應的語法和語義支援。這可以看作是一種高度最佳化的、內建的 DSL。
- 在 C 語言互操作方面,倉頡透過宣告式的語法,允許開發者以類似本地函式呼叫的方式直接宣告和使用C函式。呼叫一個 C 函式只需一行宣告,極大地簡化了開發流程。
- 在併發方面,傳統語言通常透過呼叫第三方庫或標準庫實現併發,缺乏語言層面的支援。而倉頡參考了 Go 語言的協程框架,將併發機制內建於語言中,透過關鍵詞
spawn
實現輕量級執行緒的自動管理和排程。
- 擴充機制:對於像資料庫、化工這些不那麼通用,或者需要高度定製化的領域,倉頡提供了語言和編譯器的擴充機制。開發者可以透過宏或其他語法擴充方式,在倉頡語言內部定義新的語法結構,實現所謂的 EDSL,即「嵌入式 DSL」。這種方式的好處是,開發者不需要編寫獨立的編譯器或解析器,可以直接利用倉頡的基礎設施進行擴充,並且擴充後的 DSL 可以與倉頡程式碼無縫整合。這與像 Android 在 Java 外掛外掛實現 DSL 的方式有所不同。
所以,從 DSL 的視角來看,倉頡的設計哲學是在通用層面提供強大的基礎能力和內建 DSL 支援,同時賦予開發者透過 EDSL 機制為特定領域量身定製高效表達方式的能力。這種設計不僅滿足了現代軟體開發對領域專用表達的訴求,也體現了倉頡作為下一代程式語言的前瞻性,在實際應用中展現了顯著優勢。有一個國產適配專案需要遷移 4000 多個 C 介面,倉頡透過宣告式的互操作機制,將這一過程簡化為簡單的函式宣告,大幅提高了遷移效率。Agent DSL為智慧體開發提供了簡潔的表達方式,開發者可以透過接近自然語言的語法描述智慧體行為,無需深入編寫複雜邏輯,從而進一步降低了開發門檻。
動手實踐環節
第一個實踐題目:併發與系統呼叫
這個題目與我們剛剛討論的併發和系統呼叫(特別是 FFI)有關。程式執行後會彈出一個空白的 Windows 視窗。程式碼內部已經使用倉頡呼叫 GDI 註冊了視窗類、建立了視窗,並處理了訊息迴圈,畫圖的基礎框架已經搭好。
任務:在這個視窗裡畫出一條正弦曲線。具體來說,你需要找到程式碼中預留的位置,新增幾行倉頡程式碼,呼叫 Windows 的 SetPixel
函式來繪製點。SetPixel
函式的原型可以在微軟的 MSDN 文件中查到,或者參考程式碼中已有的其他 API 呼叫示例。你需要關注它的引數:第一個是裝置上下文控制代碼(hDC),可以模仿現有程式碼獲取;後面兩個是
你需要編寫一個迴圈,計算 SetPixel
在視窗的對應位置畫點。程式碼中已經匯入了所需的 Windows API 函式,可以直接呼叫。倉頡呼叫 C 函式時,通常需要將呼叫程式碼放在一個用 @ccall
修飾的程式碼塊中,這是一種語法標記,提示開發者這裡可能涉及不安全的記憶體操作,並指導編譯器進行一些檢查。
最終完成的程式碼量大概只需要幾行。這個練習旨在讓大家體驗倉頡 FFI 簡潔性以及基本的程式設計。
查 M$ 文件:
COLORREF SetPixel(
[in] HDC hdc,
[in] int x,
[in] int y,
[in] COLORREF color
);
寫出:
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 }
}
// 基於 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 自動聊天的效果。
步驟:
- 在服務端和客戶端的程式碼中,
import
我們提供的llm.cj
模組。 - 在各自的程式初始化部分,建立LLM類的例項。可以給它們設定不同的角色(比如一個扮演賈寶玉,一個扮演林黛玉),透過初始化時的 prompt 來實現。
- 修改網路訊息處理邏輯:
- 當客戶端收到服務端的訊息後,不再列印到控制檯,而是將這個訊息作為輸入,呼叫自己的 LLM 例項的
chat
方法獲取回覆。然後將 LLM 的回覆透過網路傳送給服務端。 - 當服務端收到客戶端的訊息後,同樣呼叫自己的 LLM 例項獲取回覆,然後將回復透過網路傳送回該客戶端(或者廣播給所有客戶端,取決於你想要的效果)。
- 當客戶端收到服務端的訊息後,不再列印到控制檯,而是將這個訊息作為輸入,呼叫自己的 LLM 例項的
- 需要一個啟動機制,比如讓客戶端在連線成功後,主動傳送第一句話(可以是一個固定的問候語,或者呼叫 LLM 生成一句開場白)給服務端,來觸發對話的開始。
- 編譯時需要將
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盤:


另外,反饋說倉頡的鴻蒙部分即將開源。
評論