什麼是程式碼?從高層次來看,這個問題的答案似乎很明顯。程式碼是開發人員撰寫的內容:以程式語言表達的指令,用來告訴機器該做什麼。 多年以來,撰寫程式碼意味著逐字逐句地輸入。 進展的衡量標準在於程式碼能多有效率地 被產出、編譯、測試與部署。 隨著現代 LLM 的出現,我們不再需要輸入每個字來產生程式碼。 現在可以從高階描述大量產生可執行的程式碼。 這迫使我們思考更深層的問題:如果產生程式碼變得 更便宜,那麼程式碼還有什麼價值?

程式碼的兩個面向

程式碼一直以來都扮演兩個截然不同但相互交織的角色。

首先,程式碼是一組給機器的指令。它指揮 計算、移動資料、與儲存體互動,並協調 執行。在 LLM 時代,這部分正被商品化。

其次,程式碼是問題領域的概念模型。這是 「設計」的面向。一個設計良好的程式碼庫不僅包含 給機器的指令;它也包含供人類與工具推理的概念。

我們所稱的編碼活動,正是這兩個面向交會之處。 我們透過名稱、邊界與關係塑造概念, 藉以理解系統。

概念模型與詞彙

將概念模型明確化是編碼更深層的面向, 其驅動力來自領域以及系統所要解決的使用案例。 每個領域都伴隨著既定的流程、實務,以及更重要的 共享詞彙。 該詞彙正是概念模型得以顯現之處。

詞彙通常被理解為特定語言或主題中所使用的詞彙集合。我之所以能撰寫這篇文章,是因為 我了解英文的詞彙。讀者之所以能閱讀它,是因為他們 與我共享該詞彙。

但要理解這篇文章,只懂英文是不夠的。 這篇文章討論的是軟體開發。軟體開發是 一個廣泛且成熟的領域,擁有其自身的技術詞彙。當我使用 「abstraction」這個詞時,我指的並不只是英文單字,而是 一個特定的軟體開發概念,帶有其自身的 意義、歷史與意涵。不熟悉軟體開發的讀者 可能只能理解字面意義,而無法掌握其 在此脈絡中的深層含義。擁有成熟詞彙的領域稱為領域。

所有認真的領域都是如此。溝通依賴於共享 詞彙。無論我們是與人、 框架或 LLM 溝通,我們使用的詞彙都必須能對應到接收者 能夠理解與採取行動的概念。

程式碼中的詞彙

設計良好的程式碼庫代表著某種詞彙。 程式碼中的詞彙從何而來? 這正是軟體開發獨特之處。 軟體開發處於各種領域的交會點。 一端是銀行、金融、零售、庫存、 醫療保健、保險等領域。另一端則是 Web、基礎設施、AI、資料工程等領域。

從事 Web 開發的人需要對 Web 架構、Web 方法的語意、GET 的通用快取 潛力,以及這些語意的意涵有深入理解。不了解這些的人 無法妥善架構複雜系統。在其他領域也是如此。詞彙不僅是標籤的集合。 它承載著意義、限制與設計後果。

以零售領域為例。當我們為零售撰寫程式碼時,會談論 客戶、產品、訂單、出貨、付款等。當我們為零售領域進行 Web 開發時,程式碼會包含將零售領域對應到 Web 領域的概念。例如,Catalog 是一種資源, 我們可以使用 GET、POST、PUT、DELETE 等 HTTP 方法對其執行操作。 撰寫程式碼的人需要熟悉兩種詞彙。

為領域撰寫程式碼本質上是一種翻譯行為。開發人員 將領域詞彙對應到技術領域的詞彙。在 此過程中,也會利用程式語言提供的建構子建立新的詞彙。像是 logs、repositories、 quorums、transactions 等概念,以及 money 這類特定概念。概念會變成 型別,關係會變成介面,規則會變成不變式, 工作流程會變成組合。

變數的精確命名、方法的邊界,以及 類別的階層,都是逐步發現的。正確的抽象通常不是 一開始就顯而易見;它只有在你持續根據真實世界的限制 塑造與重構程式碼時才會顯現。善用時, 程式碼會逐漸成為領域本身的可讀且高度特定的表示。

對於技術領域,我們通常會找到提供基礎實作模式的 框架與函式庫。 框架與函式庫是編纂後的詞彙。 它們捕捉最常見的使用模式。這也是為什麼像 Spring Framework 這樣的生態系統會存在,用於建置涉及 Web、整合及相關議題的 企業應用程式。 不同的程式語言帶來各自的風格, 以及反映並編纂在它們的 框架與函式庫中的特定設計限制。

有界脈絡與在地詞彙

當領域具有穩定、 重複出現的結構以及廣泛共享的語意時,框架才能發揮良好作用。 但像是「線上零售」或「證券交易所」則不同。 它們不只是技術堆疊。 之所以沒有通用的高階框架,主要是因為詞彙 在該領域的所有實例中並不夠穩定。 試圖尋找通用抽象的結果,要嘛太泛用而缺乏實用價值, 要嘛過於主觀而無法廣泛適用。 越接近核心商業模型, 抽象就越需要在地發現。 這就是為什麼領域驅動設計中的 有界脈絡 概念如此重要。 有界脈絡標示出特定詞彙與模型有效的邊界。 同一個詞在不同脈絡中可能有不同意義, 每個脈絡都需要自己的抽象、規則與語言。

我們如何建立這些在地抽象與詞彙? 許多詞彙是透過反覆的會議,在撰寫程式碼並反思的過程中建立。 像是 TDD 這類技術非常適合這種反覆開發 詞彙。它們透過在模型與其行為之間持續提供回饋, 幫助我們發現正確的名稱、正確的 抽象,以及正確的邊界。

編碼無法孤立進行。必須有領域專家、使用者與開發人員之間的密切 合作。 這種合作對於建立在地抽象與詞彙是必要的。

這與敏捷軟體開發的教訓直接相關。強調個人與互動、客戶合作、工作中的軟體,以及回應變更,不僅是流程建議。這也是一種透過回饋發現與精煉詞彙的方法。

領域驅動設計透過 普遍語言 的概念使其更明確:由開發人員與領域專家共同發展,並持續以工作中的軟體測試的共享語言。

程式語言作為思考工具

透過程式碼建立詞彙,需要積極參與撰寫與重塑程式碼; 而不僅是被動審查產生的程式碼。深入思考 程式碼的行為,往往只有在我們積極撰寫時才會發生。 程式語言及其建構子與限制 本身會成為思考工具。不同程式語言提供的設計限制有助於塑造我們的思考。Go 的通道與輕量執行緒、Java 的物件導向模型,或是 Rust 的 所有權模型,都會引導我們以特定方式看待 結構、邊界與取捨。在這個意義上,程式語言不僅幫助我們表達 設計,也幫助我們發現設計。我最近需要為非同步程式設計範例 設計自訂的 Future 實作。 Future API 的一個重要面向是設計組合, 以便能夠表達一系列動作。

         var future1 = action1();
         future1.thenCompose(val1 -> action2(val1))
                .thenCompose(val2 -> action3(val2))
        

了解函數式程式設計的概念與詞彙,對於妥善 實作這個 API 至關重要。不了解這些概念會導致笨拙的實作與使用。

有時,程式語言的語法可能過於冗長, 並隱藏解決方案的底層結構。 例如,最近在為我的工作坊實作快照隔離時, 用白話描述基本需求有點模糊,而用 Java 程式碼又過於冗長。更嚴格的形式化規格如 TLA+ 會有所幫助。 但即使撰寫一頁的擬形式規格也帶來顯著幫助。

         Begin(T, coord):
           R(T) := HLC(coord).now()
           writeSet(T) := {}


         Read(T, N, key):
             N.HLC.tick(R(T)) //HLC advanced. So any write or commit after this is guaranteed to be at a higher ts
             return latest committed version of key with ts <= R(T)


         Write(T, N, key, value):
             N.HLC.tick(R(T))
             if LatestCommittedVersion(key).ts > R(T):
                 abort T
             place provisional intent for (key, value)
             writeSet(T) := writeSet(T) union {key}
      

這個擬形式規格幫助我釐清思考,並成為進一步討論、實作與透過測試驗證的良好基礎。

與 LLM 合作

將「編碼視為詞彙建構」納入考量,對於 LLM 具有重要意涵。 LLM 是從大量 文字與程式碼中訓練詞彙而來。它們學習 名稱、API、函式庫、框架、慣用語、設計模式與 實作結構之間的重複關係。當它們看到 Controller、 Repository、Reducer、ConsensusModule 或 TransactionLog 這類詞彙時,這些名稱不僅是標籤。它們 帶有與已知程式碼結構與預期行為的關聯。

這就是為什麼詞彙在與 LLM 合作時很重要。如果我們的提示 使用模糊或不一致的語言,模型就必須猜測我們意圖的設計。如果我們的程式碼庫使用不清楚的名稱與 不一致的概念,模型就沒有穩定的結構可供遵循。 但當詞彙精確、一致且體現在 程式碼中時,LLM 就能更可靠地將我們的意圖對應到有用的 實作。

認知債務

這也解釋了 LLM 輔助編碼的一個特定風險:認知債務。 當詞彙、抽象與結構被使用,卻未被 相關人員充分理解其意義時,認知債務就會累積。 LLM 放大了這個風險,因為它們可以非常快速地產生大量看似合理的 程式碼。產生的程式碼可能包含 controllers、repositories、 reducers、factories、transactions、schedulers 或其他看起來熟悉的結構。 程式碼可能可以編譯,甚至通過基本測試。但如果團隊不了解 這些結構背後的概念模型,程式碼庫就獲得了詞彙卻缺乏 共享理解。 問題不在於 LLM 產生了程式碼。 問題在於程式碼引入詞彙的速度,超過開發人員建立 理解的速度。這個落差是認知債務的主要成因之一。

程式碼作為共享的概念模型

正如我們在 與 LLM 共同設計抽象 中討論的, 撰寫程式碼有 兩個深度交織的活動:發現與應用抽象。 發現抽象正是我們發展詞彙之處。 一旦建立強大的詞彙,它就代表共享的概念模型。 一旦該模型存在,許多編碼工作就變成使用該概念模型來 建置使用案例。這正是好的函式庫與好的基礎程式碼 發光之處。這是我們試圖隱藏程式語言與環境的複雜性, 並提供越來越像自然語言的介面給我們所建立的詞彙之處。 一個典型的方式是建立 DSL,讓使用這個 詞彙或這些抽象變得容易且接近自然語言。

LLM 在這方面表現出色。它們提供自然語言介面 給你的抽象詞彙。最好的部分是,如果你有 可執行的程式碼作為抽象詞彙的後盾,它本身就會成為 LLM 修正錯誤的絕佳護欄。良好的 抽象、可執行的行為、測試、型別與不變式,都有助於 約束模型並使其輸出更有用。

我們可以使用詞彙建立外部 DSL。 LLM 作為外部 DSL 之上的自然語言介面表現得非常好。我已成功使用 LLM 搭配 PlantUML 等工具。這並不意外,因為 LLM 在設計上 就最擅長對應詞彙。

在這個意義上,強大的基礎程式碼在 LLM 時代變得更加重要。一旦系統的詞彙形成良好,編碼 就不再只是更快地產生原始語法,而是更多地使用 發展良好的概念語言來建置可靠的軟體。

程式碼本身作為 Harness 與 Context

許多關於上下文工程與 harness 工程的討論,將程式碼視為黑箱, 其正確性由外部 透過在提示中提供正確的上下文,或建構包含規格、測試、靜態驗證等 的正確 harness 來管理。 具有良好定義詞彙的抽象的結構化程式碼本身, 就是 harness 與 context 最重要的部分。 我一再看到設計良好的程式碼與 LLM 合作得非常成功。更重要的是,當程式碼是使用 具有清晰語意的穩定抽象建置時,你可以自由選擇 任何 LLM 模型,而不必太擔心提示的準確度。 程式碼結構與隨附的測試本身提供 context 與 harness, 使 LLM 的輸出可靠且有用。

結論

編碼的角色並未消失。但它正在改變。

隨著 LLM 讓程式碼產生變得更便宜, 機械性地撰寫指令的行為變得不再那麼核心。更重要的是讓 概念模型明確化、發現正確的詞彙,並透過反覆、領域專業知識與 回饋來精煉該詞彙。這也是為什麼程式語言仍然如此重要。 我們不應該只是被動審查產生的程式碼。 撰寫程式碼的行為本身就是思考的一部分。

程式碼仍然是給機器的指令。但它也是一個 理解的模型。在 LLM 時代,後一個角色變得更加 重要。編碼的未來不只是更快地撰寫更多程式碼。 而是建置更好的概念模型、更好的詞彙,以及 更好的基礎,讓人類與 LLM 都能在其上運作。