跳至主要內容

驗證、安全與工程紀律

agent 不管實際上有沒有成功,都會回報成功。它會說「完成了,所有測試都通過」,而且不管測試真的通過了、測試根本沒跑過,還是測試被偷偷弱化到通過為止,它講這句話的自信程度都一模一樣。對 agent 輸出的信任不是你可以憑空假設的特質;它是你透過驗證一點一滴賺來的特質。這份文件是整章的護欄層:讓 agentic 迴圈spec-driven development(規格驅動開發)能夠在高速運轉下依然安全的紀律。

它刻意在工作流程文件略過的地方深入探討。它不會重新解釋 agentic 迴圈,也不會重講 context 檔案裡該放什麼;它要講的是如何讓 agent 輸出值得信任,以及如何讓 AI 輔助的變更保持紀律

測試是驗證的骨幹

要讓「成功」這個宣稱變得可檢查,最可靠的方法就是讓它變成可執行。測試驅動開發(test-driven development)給你的正是這個。

Red、Green、Refactor

這套經典的 TDD 循環,在 Kent Beck 的《Test-Driven Development: By Example》(2002)中被正式定型,有三個步驟:

  • Red:為一個還不存在的行為寫一個小測試,然後看著它失敗。
  • Green:寫出能讓測試通過的最少量正式程式碼。
  • Refactor:既然有通過的測試把行為釘住了,就把程式碼整理乾淨。

Robert C. Martin(「Uncle Bob」)把這套濃縮成 TDD 三大定律

  1. 在你寫出一個會失敗的測試之前,不要寫任何正式程式碼。
  2. 一個測試只要寫到剛好會失敗就好,不要多寫。
  3. 正式程式碼只要寫到剛好能讓那個失敗的測試通過就好,不要多寫。

回報是雙重的。在程式碼之前寫的測試是一份可執行的規格:它用機器能檢查的形式,講清楚「正確」到底是什麼意思。同一個測試保留下來,就是一張回歸測試的安全網:一旦後續的變更弄壞了那個行為,它馬上就會失敗。正是這種雙重角色,讓 agent 能夠為自己打分數。

把 TDD 套用到 agentic 開發

對一個自主 agent 來說,測試、build、lint 不是雜事,而是驗證關卡。它們是一個機器可檢查的通過/失敗訊號,讓 agent 能在內層迴圈不需要人介入的情況下,自己把迴圈收尾。沒有關卡,就沒有信任。

先重現,再修復

修 bug 的時候,要求 agent 先寫一個重現該 bug 的失敗測試,然後改程式碼直到那個測試通過。這會逼著修復去處理根本原因,而不是壓掉症狀。放任不管的 agent 常常會把那個失敗的呼叫包進 tryexcept 裡,把錯誤吞掉,然後宣告勝利。重現測試讓這條捷徑走不通:在底層行為真的正確之前,測試都會一直失敗。

把測試當成已提交的規格

把測試先行的循環當成一份 agent 不能偷偷重談的合約:

  1. 先寫測試(或讓 agent 寫),把想要的行為描述清楚。
  2. 跑這些測試,並確認它們是因為正確的原因而失敗
  3. 把測試當成獨立的一筆變更提交
  4. 實作到測試通過為止,並指示 agent 不要修改已提交的測試

如果 agent 為了硬湊出綠燈而弱化測試(放寬一個 assertion、刪掉一個 case、加一個提早的 return),diff 就會把它揪出來。一筆和實作出現在同一個 commit 裡的測試變更,是個值得逐行檢視的警訊。

要證據,不要宣稱

「所有測試都通過」是一句宣稱。指令和它的輸出才是證據。要求 agent 把它實際跑的指令和真正的結果秀出來,而不是給你一段摘要:

$ pytest -q tests/test_auth.py
.... [100%]
4 passed in 0.62s

一個沒有附上輸出的宣稱,按定義就是未經驗證的。讓貼上證據成為常態,並把缺少證據視為一道沒過的關卡。

讓獨立的檢查者來認證

寫出這筆變更的 agent,不應該是替它認證的那一個。會產生某個 bug 的盲點,在審查那個 bug 時往往會產生同一個盲點。讓認證走一道獨立檢查:一個全新 context、對實作時那些合理化說詞毫無記憶的審查者,或者(更可靠的)一個從乾淨 checkout 重新跑一遍關卡的 CI。獨立檢查不需要更聰明;它需要的是分離

文件與靜態網站的關卡

這份寶典是一個 Docusaurus 網站,不是一個應用程式,但原則完全一樣,只是「測試」換了個樣子。先定義關卡,再實作到綠燈:

  • build 通過npm run build 沒有任何錯誤);
  • 連結檢查找不到任何失效的內部或外部連結;
  • **i18n parity(對等性)**成立:每一份英文文件都有對應的譯本,而且沒有孤兒 key;
  • lint 與格式化通過(Markdown lint、Prettier);
  • 效能預算達標(例如某個 Lighthouse 門檻)。

寫文件之前就決定好這裡面哪些必須是綠的,靜態網站就能得到和一個有測試的程式碼庫一樣、一點一滴賺來的信任。實際怎麼設定這些,請看程式碼生成

自主 agent 的安全

一個能編輯檔案、能跑指令的 agent,同樣也能刪檔案、把祕密外洩出去。驗證證明的是工作正確;安全控制則框住 agent 在過程中能做什麼。

權限與沙箱

把能力約束在任務範圍內:

  • 限制檔案系統:把 agent 的活動範圍框在工作目錄裡;別讓它碰到家目錄、credentials 和系統路徑。
  • 限制網路:預設不給對外存取,除非某個步驟真的需要。
  • 使用核可模式:在執行破壞性或不可逆的動作之前,要求人類確認;至於例行的讀取和編輯操作,就讓它自由跑。
  • 把完全存取、免核可的模式保留給隔離、用完即丟的環境:一個用完即丟的 container 或 VM,爆炸半徑有界,沒有任何有價值的東西會外洩。

各工具專屬的控制方式,請看 Claude CodeOpenAI Codex 指南;不論你用哪一個,都套用同一條最小權限原則。

為了安全而審查 AI 生成的程式碼

生成的程式碼需要一次安全檢查,而不只是一次正確性檢查。至少要檢查:

  • 沒有寫死的祕密:金鑰、token 和密碼該放在設定或祕密管理工具裡,絕不能放在原始碼中;
  • 輸入驗證:不受信任的輸入要在使用前先檢查過,而不是假設它格式良好;
  • 沒有不安全的處理方式:沒有未消毒的 shell 或 SQL 字串拼接、沒有未驗證的反序列化、沒有把失敗藏起來的吞錯。

這份清單和安全與倫理最佳實務那份文件互補:把那份當成更深入的參考,把這份清單當成每一筆變更的關卡。

避免過度生成與過早抽象

沒有引導的話,LLM 會過度設計。它們會吐出多餘的檔案、臆測性的抽象、為不可能發生的情況所寫的防禦性檢查,還有沒人要求的可設定彈性。這是 AI 輔助開發最常見的失敗模式之一,而它之所以特別有腐蝕性,正是因為每一處新增單獨看起來都很合理。

對抗這些的原則既老牌又有好名字:

  • YAGNI(「You Aren't Gonna Need It」,由 Martin Fowler 和 Extreme Programming 推廣):不要為了假設性的未來需求而開發。
  • Rule of Three(Fowler 和 Don Roberts):重複兩次先忍著;只在第三次出現、共用的形狀真的看得出來時,才重構成一個抽象。
  • AHA(「Avoid Hasty Abstractions」,Kent C. Dodds):寧可選你看得懂的重複,也不要選你還在猜的抽象。
  • Sandi Metz 的經驗法則:「重複遠比錯誤的抽象便宜」;錯誤的抽象要拆解回去代價高昂,而重複日後要合併起來卻很便宜。

把這些化成 agent 一開始就收到的護欄:

  • 要求滿足需求的最小變更,不要任何臆測性的東西。
  • 延後抽象,直到重複經由 Rule of Three 得到證實。
  • 講清楚系統的不變條件(invariants),這樣 agent 就不會再為不可能發生的情況加上多餘的防禦性程式碼。
  • 把審查者的範圍框窄:「只標出會影響正確性或既定需求的缺口」,這樣審查本身才不會變成另一個鍍金(gold-plating)的來源。

讓變更保持小而乾淨

小而範圍明確的變更比較容易驗證,而可驗證性正是整件事的重點。

Small CL、Small PR

Google 的工程實務主張採用 small changelist(小變更清單):一筆只做單一一件事、自成一體的變更。小變更被審查得更快、更徹底,比較容易理解,回滾起來也比較安全。粗略地說,一筆大約一百行的變更,要好好審查起來很舒服;一筆接近一千行的,通常就大到沒辦法仔細審查了。就算沒有精確的數字,這個方向性的發現依然站得住腳:比較小的 diff 會得到比較好的審查注意力,而大的 diff 每一行抓到的 bug 比較少。當工作真的很龐大時,就把它拆成堆疊起來、可獨立驗證的變更,每一筆都自己過得了關卡。

Boy Scout Rule

Robert C. Martin 的 Boy Scout Rule(「讓程式碼比你發現它時再乾淨一點點」)容許你在已經動到的檔案裡做一些小而隨手的整理:一個更清楚的名字、移掉一段死分支、修掉一個錯字。紀律就在那條界線上:整理要侷限在動到的範圍內,絕不溢出成沒人要求的重構。

它們如何相互搭配

這些實務彼此強化:

  • TDD 的 refactor 步驟正是 Boy-Scout 整理該待的地方:通過的測試把行為釘住了,所以整理是安全的。
  • 每一個工作單位都是一筆由測試把關的 small CL,能夠獨立驗證。
  • 護欄在於:整理和新增都留在動到的範圍內,它們絕不膨脹成沒人要求的重構,或上一節講的那種過度生成的程式碼。

agent 在一個小的、有把關的、有範圍的盒子裡快速移動。快是沒問題的;無界的爆炸半徑才不行。

驗證檢查清單

在接受 agent 的工作之前,先確認以下每一項:

  • 測試通過且有證據:指令和它真正的輸出都秀出來了,而不是給一段摘要。
  • diff 是最小且在範圍內的:沒有超出需求的新檔案、抽象或防禦。
  • 沒有新的祕密:沒有任何寫死的東西;輸入有經過驗證;沒有不安全的處理方式。
  • 跑過一道獨立檢查:全新 context 的審查,或從乾淨 checkout 跑的 CI,而不是只憑實作 agent 自己一句話。
  • 這筆變更是一筆自成一體的 CL:單一目的、可審查、可安全回滾。

只要有任何一格沒打勾,這件工作就還沒完成:它是未經驗證的,而那跟完成不是同一回事。

它在本章的定位

這份文件是本章其餘部分的護欄層。把它和 Vibe Coding 總覽(決策模型)、spec-driven development(規格驅動開發)(把意圖變成可檢查的規格)、以及 agentic 工作流程(這些關卡所保護的那個迴圈)搭配起來看。