跳至主要內容
蒼時弦也
蒼時弦也
資深軟體工程師
發表於

Sumitsubo:把規格做成 Linter

開發 Kobako 的過程到中後期,一直讓我對於「規格有沒有對齊」這樣的問題困擾著我。理論上規格驅動開發(Spec-Driven Development)是很理想的,但語言模型本身就不是確定性的設計,即使有規格參照也還是會偏移,或者遺漏。

因此 Sumitsubo 這個專案就在最近被設計出來,作為一個通用的檢查工具。

理想跟現實

想要證明一個規格被實作,除了實際跑過跟閱讀原始碼之外,基本上很難有效率的確認,尤其是規模變大、變複雜之後更是如此。

如果想要讓這種無法機械化的工作有機會機械化處理,結構化的資料是一個很不錯的選項,在去年 Anthropic 的 Effective harnesses for long-running agents 剛好展示了一種不錯的可能性,用 JSON 登記規格,然後讓 Claude Code 去把規格實作。

實際上這仍不能保證完美的覆蓋,但至少證明維護 JSON 對 AI 已經不再困難,按照 JSON 的內容維護功能,表現的基本上也算穩定,至少可以順利的運作到大部分的功能都被實作出來。

理想上是有一種神奇的方法可以把一段自然語言,對應到某段實作,但現實很難做到這點,往後退一步用「聲明(Claim)」的方式,讓 AI 宣稱某段規格有被實作,倒是在「完全不檢查」跟「完美檢查」之間權衡下,可接受的考量。

妥善引導

在 Sumitsubo 推出之前,Kobako 會用「錨點」的機制來檢查,像是規格定義行為 B-25 時,至少要在測試的註解看到一次 B-25 才算通過,這樣就可以一定程度促進 AI 去確認為什麼某個規格提過的功能沒有測試覆蓋。

轉換到 Sumitsubo 後,採用的是「行為標記」的方式,利用 BDD(Behavior-Driven Development)風格搭配編號來實作,這並不是新概念 Gherkin 也有 @B-25 的標籤機制,但我把人工維護轉為 AI 輔助處理。

 1{
 2  "name": "Behavior",
 3  "description": "The scenarios a project declares, and where each one sits.",
 4  "include": [
 5    "test/behavior_test.rb"
 6  ],
 7  "scenarios": [
 8    {
 9      "id": "B-001",
10      "title": "What the directory declares, and where",
11      "given": [
12        "a behavior directory holding more than one specification"
13      ],
14      "when": "the directory is loaded",
15      "then": "every scenario answers with the line of the file that declares it"
16    }
17  ]
18}

上方是 Sumitsubo 自己維護的一條行為規則,放在 .spec/behavior/ 目錄下,編號 B-001 會確認所有的規格都有被載入,因此在 behavior_test.rb 中可以看到像這樣的標記。

 1# @behavior B-001
 2puts "--- what the directory declares, and where ---"
 3Sumitsubo::Behavior.load("test/fixtures/behavior/.spec/behavior").each do |feature|
 4  puts "#{feature.name} #{feature.includes.inspect}"
 5  feature.scenarios.each do |scenario|
 6    puts "  #{scenario.path}:#{scenario.line} #{scenario.id} #{scenario.title}"
 7    scenario.given.each { |state| puts "    given #{state}" }
 8    puts "    when  #{scenario.action}"
 9    puts "    then  #{scenario.outcome}"
10  end
11end

這樣就讓 AI 自己證明了「我有測試過這條規則」而不是毫無根據,如果沒有做行為標記,就會在 sumi verify 看到這樣的訊息

1$ sumi verify
2.spec/behavior/behavior.json:6 @behavior B-001 is claimed nowhere in test/*_test.rb
31 differences

那麼實作過程中,AI 就會自己發現少了對應的處理,進一步的補上一條測試,那就可以一定程度的確認某條規格,至少被測試過一次。

實際上標記只說明測試宣稱涵蓋了這條規格,並不保證測試內容真的驗證了那件事,仍然是「聲明」而非證明,但比起沒有對應關係,落差至少變得看得見。

上方的測試是因為使用 Matz 今年在日本 RubyKaigi 公開的新專案 Spinel 採用預先編譯 Ruby 為 C 後,轉成靜態執行檔的設計,因此測試是真實跑執行檔輸出比對,而不是使用斷言驗證

自主規格化

開發 Sumitsubo 的過程中,讓我懷疑規格驅動開發是否還是必要的,或者真的會隨著駕馭工程(Harness Engineering)的成熟而消失,因為當 Sumitsubo 用來自我檢查時,我發現了蠻有趣的現象。

原本我們走向先規格後實作的理由,是因為只靠程式碼太難理解,而且在過去軟體開發的經驗中,文件跟實作是很難對齊的,至少在 AI 時代這件事情一定程度可以做到,所以先規格後實作確實變成很合理的機制,尤其是在 AI 能力較差時可以發揮引導的作用。

然而 Sumitsubo 在開發時,我為了讓他用自己來維護自己的規格,就沒有刻意從 SPEC.md(我偏好的規格文件)開始,而是完全的從頭進行,當「行為標記」的機制搭配 Claude Code 的 Hook 被設定到專案上後,AI 自己就足夠發現規格差異進行修正。

例如 Sumitsubo 有一個「標記」的機制記載了「註解後面沒有程式碼,則不看作有標記」的規則,當我開始為 Kobako 增加 Rust 支援後,抽象化了 Language(語言)的概念,此時 AI 自己注意到這條規則跟著「語言行為」而非「標記行為」更合理,就自己完成標記的調整、行為的修訂等等。

當工具到位(適當的設計)時,也許原本很多依靠提示詞的機制對 AI 來說才是限制,假設我們提供一套有系統的規格維護工具,也許只需要用「使用 sumi verify 檢查規格」這樣簡單的指示,就可以把大部分規格維護好。

雖然我是蠻堅定的規格驅動開發支持者,但是當我把規格變成一種可驗證工具(Linter)時,我確實對此開始懷疑,即使我還不確定未來會怎麼發展,但也確實是一條值得嘗試的新路線。