iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Claude AI

買了 Claude Code,然後呢?系列 第 11 篇

Day 11|計畫寫好了,怎麼讓 Claude 自己改、自己驗?

  • 分享至 

  • xImage
  •  

「自己改、自己驗」是三方接力,不是模型自己宣布通過:判準由人先確認,Claude 只提出修改、碰不到檔案,套用與測試都由程式執行

Day 9 把 PM 的兩句話變成能開工的規格,Day 10 定落點與驗法,今天動手寫程式。

要改的是同一個訂單取消功能:已付款、尚未出貨,而且還沒取消的訂單,取消後要提出退款要求。 舊程式固定回傳 false,現在要讓它依規則決定。

只是,如果每次都要我喊「寫好了嗎?跑一下測試。失敗了,再改一次」,程式雖然交給 Claude,我還是坐在旁邊當傳話的人。 我想交出去的不只是寫程式,還有「改完、檢查、沒過再修」這段反覆工作。

今天想做到的是:把完成條件與修改範圍交代清楚,讓檢查結果能回到 Claude 手上;它能修的繼續修,需要決定的才停下來。 先看已經跑過的兩次開發呼叫,再把它接成有停止條件的小型回饋迴路。

先說一件我做完才知道的事:那個「它能修的繼續修」,到今天為止一次都沒有真的發生過。 四次實跑全停在第一輪,我連換小模型、抽掉規格都試過。後面會講為什麼,以及它反過來證明了什麼。

先約好完成條件,不是每件事都先寫測試

這次採測試先行,是因為規則的變動很明確:已付款的首次取消要提出退款要求,再取消一次則不能重複提出。先把這個差別寫下來,就能檢查程式是不是真的改到那裡。

這是教學訂單的規則變更,不是正式公司的退款決策。rules-v2.md 裡的 Owner 也是案例角色;RefundRequested 只是表示「需要退款」的旗標,不會真的執行退款。

Anthropic 的 AI-native SDLC playbook 與課程,把重點放在讓 Claude 取得可執行的回饋。修 Bug 適合先重現失敗,再修正;UI 工作則可以實作後操作畫面、比對與調整。不是每種工作都照同一個順序,但每種工作都需要知道做對了沒有。官方課程

把常見工作放在一起看,差別會更清楚:

工作類型 從哪裡開始 怎麼確認做對了?
修 Bug 先把問題重現成失敗測試 修正後通過,既有測試也沒壞
修改業務規則(本篇) 先確認新規則與驗收情境 新行為符合要求,該保留的行為不變
重構 先留下既有行為的檢查 程式結構改了,對外行為不變
開發 UI 依操作情境做出畫面 實際操作、比對預期,再修正

這次改的是訂單取消規則,屬於第二種。 我先確認「哪些結果要改、哪些不能變」,再讓 Claude Code 把它們寫成測試,作為修改程式時的核對依據。

Day 6 約好的完成條件,到了今天要變成能執行的檢查。Day 10 的計畫在這裡先取兩項:建立單元測試、修改訂單取消邏輯。API、安全性與通知設計沒有因為寫進計畫,就在今天一起完成。

起手式:先決定 Claude 能碰到哪裡

規則與計畫準備好了,接下來是一件很容易被跳過的事:這次修改要放在哪裡?

人自己開發的時候,這個問題沒什麼重量。你一次只在一個地方工作,切錯分支自己會發現。換成 AI 驅動,同一個問題的性質就變了:

人驅動 AI 驅動
同時進行 一次一件,你知道自己在哪 可以同時開兩個 session,各自在改檔
位置感 記得自己在哪個分支 只拿到一個工作目錄,沒有你的心智模型
出錯時 覺得不對會停下來問 不會停,會照著錯的前提往下做

第三列才是重點。隔離不是禮貌,是煞車。出錯時你要還有一個沒被動過的地方可以回去。Claude Code 把 --worktree 做成原生旗標,就是承認了這個需求。

這是今天的第一道圍欄,圍的是檔案系統這一層。後面還會再圍兩層:能用哪些工具、誰有權寫入。三層都不是為了防它使壞,是為了出事時查得出是哪一層出的問題。

我的選擇是功能分支搭配 worktree。分支保存這件工作的提交;worktree 提供獨立工作目錄,讓原本的工作可以留在原地。同一個任務的修改、測試與修正留在同一份 worktree,不必每一步都另開一份。Git 官方說明

main 與功能 worktree 共用 Git 歷史但分開工作檔案;本機測試與提交之後,PR、CI、審查及合併仍是後續步驟
在已確認基準版本的 Git repo 根目錄,可以這樣建立:

git worktree add -b feat/order-cancel ../order-cancel-work main
cd ../order-cancel-work

這會從本機 main 建立 feat/order-cancel,並把它簽出到旁邊的新目錄。實際團隊若以其他分支或 commit 為基準,就換成那個已確認版本;尚未提交的規格與 plan.md 不會自動跟過去,開工前要先確認它們已包含在基準中。

也可以讓 Claude Code 建立自己的 worktree:在 repo 根目錄執行 claude --worktree order-cancel。這是另一種建立方式,不用接著上面兩行再做一次;起始版本與目錄行為依工具設定確認。Claude Code 官方文件

這裡補做了一次獨立驗證:把已備好的新測試與舊 Domain 放進教學 Git repo,再開功能分支與 worktree,讓 Claude Code 只改 Domain,外層執行測試。

核對位置 實際結果
開工基準 SC-03 失敗,符合舊實作尚未支援新規則的預期
功能 worktree Claude 只改 Domain,七項通過,保存本機 commit
原本的 main commit 未移動、工作目錄乾淨,仍在 SC-03 失敗

這證明的是本次工作目錄與分支的修改有分開。它不是下面那兩次歷史呼叫的 Git 紀錄,也沒有開遠端 PR 或合併 main。後面的回饋迴路示範仍使用獨立資料夾快照,不會因為加了這段就變成 worktree 實跑。

Worktree 也不會自動分開資料庫、連接埠或外部服務。這次只跑本機領域測試;多個 AI 同時啟動服務時,還要另外安排執行環境。只有一個任務依序做,使用一般功能分支也可以。

還沒驗過的一段:同樣的隔離在多人協作時只會更需要。兩個人各自帶著 AI 改同一個 repo,衝突不只是 Git 層的。但本篇沒有第二個人(第三幕會再回到這件事),所以這是延伸推論,不是這裡驗出來的結果。

第一層圍好了,回到開發本身:Claude 要依什麼開始改?

第一次呼叫:讓 Claude 把規則寫成測試

前面 Day 6–9 花時間釐清的,是需求為什麼要做、怎樣才算完成、現有程式有哪些限制,以及哪些決定還不能由 Claude 自己猜。到了這個訂單案例,確認後的規則整理在 rules-v2.md,要保留的理由與限制放進 CLAUDE.md;Day 10 再用 plan.md 排好修改範圍、順序與驗證方式。前面不同案例練習的方法,到這裡才接成同一份開發交付。

所以,這一次不是重新請 Claude 猜需求,而是讓它拿著已確認的規則與計畫,把驗收條件寫成可執行的測試,接棒進入開發。

輸入是 CLAUDE.md、plan.md、rules-v2.md 與現有程式。下面是原始提示的重點節錄:

只修改 tests/DomainTests/Program.cs。
依規格建立 SC-01 至 SC-07 的斷言,每個 Then 條件都要檢查。
與舊預期不同的地方,附上規則來源。
不要改 Domain、API 或規格;列出預期哪個情境失敗。

看第一行和最後一行。前者把可改的檔案鎖成一個;後者要它在跑之前先預言哪一格會紅——有了這句,下面那個紅燈才算得上證據,而不是碰巧失敗。

這次工具提供 Read、Grep、Glob、Edit、Write,沒有 Bash。Claude 寫測試,外層 Python 執行器負責實際跑 .NET。這是本次實驗的分工,不代表平常使用 Claude Code 必須拿掉執行工具。

Claude 回報了規則版本,也說明不連金流的理由。不過,念得出 CLAUDE.md 裡的字,不等於做對;接著看它寫出的測試與執行結果。

情境 出貨/付款/取消 規格要求的結果 舊程式會怎樣
SC-01 否/否/否 取消成功(驗 Cancelled) 一樣
SC-02 是/否/否 回傳原訂單 一樣
SC-03 否/是/否 取消成功,要求退款 回 false,對不上
SC-04 否/否/否 不要求退款(同輸入,驗另一條規則) 一樣
SC-05 是/是/否 原訂單,不要求退款 一樣
SC-06 否/否/是 原訂單,不要求退款 一樣
SC-07 否/是/是 原訂單,不要求退款 一樣

SC-01 與 SC-04 的輸入欄完全相同(否/否/否),不是填表填重複了:同一組輸入要驗兩條不同的規則,SC-01 驗 BR-01「未出貨可以取消」,SC-04 驗 BR-03「未付款不提出退款」。代價是七格實際只涵蓋六種布林組合,「已出貨+已取消」那兩種沒有被測到。

其中 SC-03 檢查兩件事:

var r = Cancellation.Cancel(
    new Order(Shipped: false, Paid: true, Cancelled: false));
return r.Order.Cancelled && r.RefundRequested;

訂單要變成已取消,而且要提出退款要求。不是只確認程式執行時沒有發生例外。

外層執行器跑 dotnet run --project tests/DomainTests,得到:

PASS SC-01 未出貨可取消,不要求退款
PASS SC-02 已出貨不可取消,回傳原訂單
FAIL SC-03 已付款取消要提出退款要求
PASS SC-04 未付款取消不要求退款
PASS SC-05 已出貨已付款不可取消,不要求退款
PASS SC-06 已取消再次取消維持原狀(未付款)
PASS SC-07 已取消再次取消不重複退款(已付款)
FAIL: 1 項條件未通過

這個紅燈有意義,是因為它失敗在預期的 SC-03。 編譯失敗、專案路徑錯了,也會失敗,但不能拿來證明測試抓到了規則差異。

SC-06、SC-07 是新增情境,卻一開始就綠。舊程式固定回 false,剛好符合「再次取消不重複提出退款要求」。所以不要要求每條新測試都必須先紅,要看它是否真的區分新舊行為。

第二次呼叫:把失敗結果交回去,改程式

這次讀同一份規格,再加上剛剛的測試輸出:

讀取 runs/tests/tests.txt,核對紅燈原因。
只修改 src/Domain/Cancellation.cs,符合 rules-v2.md。
不得修改測試、API、規格或新增金流。
不限定分支順序,以規格要求的結果為準。

看第四行——它是刻意寫的。

Claude 保留「已出貨不可取消」的判斷,將原本固定 false 的地方改成:

var refundRequested = order.Paid && !order.Cancelled;
var cancelledOrder = order with { Cancelled = true };
return new CancellationResult(cancelledOrder, refundRequested);

它也更新了過期註解,將規則來源指向 v2。接著由外層重跑同一套測試,七項通過。

第四行是為 Day 9 那場「要不要先判斷已取消」的爭論收尾:規格限制的是結果,不是寫法,這次的組合條件也能得到要求的結果。

執行器另外比對追蹤檔案的 SHA-256:第一次呼叫只有測試檔變動,第二次只有 Domain 變動。這是檔案最終內容的事後核對,不是作業系統沙箱,也不能證明執行途中從未改過又還原。

一次做完也成功,那還需要拆成兩次嗎?

既有實驗還有一組對照:同樣的起點與規格,允許 Claude 一次修改測試和 Domain。它也交出七項通過的結果。

我把那組產生的測試抽出來,放回舊 Domain,再跑一次,SC-03 確實失敗。這表示至少在本案例中,一次完成也能產生抓得出舊行為的測試,不能因為它同時寫了程式與測試,就認定結果不可信。

既有實跑 最終內容改變的檔案 如何核對測試有效?
寫測試那次 測試檔 對舊 Domain 執行,SC-03 失敗
改程式那次 Domain 同一套測試七項通過,測試檔雜湊不變
一次完成組 測試檔與 Domain 將新測試放回舊 Domain,確認 SC-03 失敗

拆成兩次能先留下新測試對舊行為失敗的紀錄;一次完成則可以事後交叉核對。兩種方式都有檢查成本。本次各只有一次對照,不能據此說哪種普遍更快,更不能宣稱多一道流程一定值得。

Anthropic 的應用開發實驗,也把實作與評估分工,讓實際操作結果回到下一輪;但隨模型能力提高,又移除了部分不再必要的流程。值得借鏡的是持續核對哪些檢查有用,而不是把整套多 agent 架構照抄過來。Anthropic 工程實驗

OpenAI 公開的內部開發經驗,則把應用啟動、介面、日誌與指標提供給 agent,讓它能驗證修正。這是特定環境投入後的經驗,並非裝好工具就會自動成立。OpenAI 工程經驗

今天這個小任務,不需要先成立一支 agent 團隊。先讓一份提案能被套用、檢查與退回,就已經走出只等一句「完成了」的開發方式。

接成回饋迴路,才不用每次由人傳話

前面花時間約定「怎樣才算完成」,到這裡才真正派上用場。我想接起來的是:提出修改 → 執行檢查 → 回傳結果 → 修正或停止。

控制理論管這叫閉環:輸出被量測之後回頭調整下一次輸入,而不是做完就結束。以下稱回饋迴路。

回饋迴路的關鍵,是讓它拿得到結果,也知道結果代表什麼。 完成條件決定何時可以結束這輪工作;可執行的檢查讓它知道哪裡沒做好;明確的修改範圍與停止條件,則決定哪些失敗可以繼續修、哪些問題必須交回人。

在事先允許的範圍內,修改、測試與回饋可以接著走,不必每一步都等人說「繼續」。工具操作仍依授權設定執行;需要改需求、擴大權限或接受核心風險時,仍然要停下來。本機檢查通過,也不代表 PR 已獲准合併。

這裡開始是另一套實驗,原件也分開放。 上面那兩次呼叫留在 runs/tests 與 runs/impl:各做一件事、由我在中間轉貼結果。接下來這四次留在 runs/feedback-loop-*:同一個起點,但失敗結果由程式自動送回去。

這次要改變的是:失敗結果由程式送回,不再等人轉貼。原執行器遇到實作測試失敗就停止,它不會把失敗自動送回下一次,所以這次另外補做了一個小型 runner;下面講的「輪」都是指這條回饋迴路內部的重試次數。

它沿用「新測試+舊 Domain」快照,不覆蓋前面的原件。

怎麼改、怎麼驗,分工是這樣: Claude 讀規格與失敗結果,提出 Cancellation.cs 的修改;執行器把它套進指定檔案,執行七個測試,再核對修改範圍。情境失敗就把實際輸出交回 Claude;通過就留下 diff 與測試紀錄。需要改規則或用完三輪,就停下來交給人決定。

實際執行順序如下:

  1. 先跑既定測試,確認起點仍是 SC-03 失敗。
  2. Claude 讀規格、程式與失敗輸出,提出完整的 Domain 檔案內容。
  3. 執行器只將提案寫到 src/Domain/Cancellation.cs,再跑測試。
  4. 若仍有情境失敗,把結果送回 Claude,最多三輪。
  5. 需要改驗收條件、其他檔案,或超過三輪時停止。編譯/環境錯誤也先停止,不把它當成一般規則失敗繼續重試。

三次模型提案經套用與訂單測試後通過,另一次在套用前格式拒收;虛線重試與停止分支由模擬回覆驗證

這次 Claude 只有讀取與搜尋工具,不能直接寫檔,也不能跑命令,它回傳 JSON 提案,由程式套用與檢查。讀者平常也可以讓 Claude Code 直接執行測試;這裡把寫入與測試留在外層,是為了讓「誰做了什麼」在紀錄上分得開,出事時才查得出是提案錯了、還是套用錯了。

第一次真的跑,先卡在另一個地方:Claude 在 JSON 前面加了一段說明,解析器拒收,程式尚未套用。修正為可接受一個明確的 JSON 程式區塊後,另開新目錄重跑。

修正解析器後重跑,Claude 第一輪修改就通過七項檢查,測試與規格保持不變。

我逼不出第二輪

回到開頭說的那件事。回饋迴路的賣點是「失敗了會自己再修一次」,但上面那次一輪就過了。沒有失敗過的失敗分支,等於沒驗過。 所以我又跑了兩次,一次比一次難:

run 驅動模型 拿掉什麼 輪數 費用 結果
feedback-loop-01 sonnet 無 1 $0.0955 INVALID_PROPOSAL(解析器拒收,不是模型錯)
feedback-loop-02 sonnet 無 1 $0.1072 CHECKS_PASSED
feedback-loop-haiku-01 haiku 無 1 $0.0386 CHECKS_PASSED
feedback-loop-nospec-01 haiku rules-v2.md、plan.md 1 $0.0326 CHECKS_PASSED

第三次我換成小很多的模型,想看它會不會第一輪寫錯。沒有。第四次我把規格檔和計畫檔直接從工作副本刪掉,只留紅燈與程式,模擬「維運現場只有一個失敗訊息」。還是一輪就過。

能比的只有最後兩列,同一個 haiku,只差有沒有規格:模型回合(一次 run 之內模型來回的次數,不是上表的輪數)從 9 掉到 4,費用從 $0.0386 掉到 $0.0326;工具呼叫從 8 次(6 次 Read、2 次 Glob)降到 3 次,三次全是 Read,沒有一次 Glob。沒有規格可翻,它就不翻了,直接讀測試。(前兩列是 sonnet,而且 feedback-loop-01 是解析器拒收,模型與條件同時不同,不能跨列相減。兩邊也各只有一次,這是一組觀察,不是「抽掉規格比較省」的結論。)

為什麼逼不出來:測試檔就是規格

拿掉規格還能一輪過,我原本以為是模型在猜。核對 trace 後,發現它只讀了三個檔:測試、Domain、CLAUDE.md。然後看它產出的註解:

// 已取消的訂單再次取消不重複處理(BR-04)
// 已付款的訂單取消時提出退款要求(spec v2 BR-03)

規格檔已經被我刪掉了,BR-03、BR-04 這些編號它是從哪裡看到的?答案在測試檔:

// SC-03|BR-03(改)|Given 已付款、未出貨、未取消|Then Cancelled=true、RefundRequested=true
// SC-07|BR-04(新)|Given 未出貨、已取消、已付款|Then 原訂單、RefundRequested=false(不重複退款)

七個情境的每一格都帶著規則編號、Given 與 Then。這份測試檔本身就是一份規格。 所以「只給紅燈」根本沒有藏住任何東西,紅燈連答案一起遞過去了。

規格與計畫文件移除後,測試檔仍保留規則編號、訂單條件與預期結果,Claude 仍可讀取驗收資訊

這回頭解釋了 Day 6 那件事:把驗收條件寫到這個程度,規格文件對實作者就變成可有可無。好處是交付門檻很清楚;代價是我以為自己在做一個盲測,其實沒有。要真的測失敗分支,得換一個大到單靠測試講不完的任務,那不是今天這個範圍。

那三個分支(失敗回傳、三輪停止、需要人判斷)是以模擬回覆加真實 .NET 執行驗過的,驗的是執行器,不是模型。

帶回自己的專案,先跑這一小段

使用本篇實作包,需要 Python 3、.NET 9 SDK。只想看紅綠,不必呼叫模型;從包的根目錄開始:

cd runs/tests/snapshot
dotnet run --project tests/DomainTests
cd ../../impl/snapshot
dotnet run --project tests/DomainTests

第一個命令預期 SC-03 失敗、退出碼 1;第二個七項通過、退出碼 0。兩份是獨立快照,不用覆蓋檔案來猜修改前的樣子。

要讓 Claude 重新提案,回到實作包根目錄,確認 Claude Code 已安裝並登入:

python run-feedback-loop.py reader-loop-01 --max-rounds 3

想自己試試看能不能逼出第二輪,可以換小模型或把規格抽掉:

python run-feedback-loop.py reader-loop-02 --model haiku --withhold "specs/rules-v2.md,plan.md"

這會消耗模型用量,結果不保證相同。使用未用過的名稱,執行器會建立 runs/reader-loop-01,保留輸入、回覆、diff、測試輸出與 report.json。看最後的 status:CHECKS_PASSED 是本機檢查通過;NEEDS_DECISION 或 ROUND_LIMIT 則表示要停下來處理。

換成自己的工作時,先挑一個範圍小、驗收條件清楚的修改。把規格位置、可改檔案、檢查命令與停止條件一起交付。不要直接把示範的路徑、七項情境與三輪上限當成所有專案的標準。

回到一開始:計畫寫好了,怎麼讓 Claude 自己改、自己驗?

  • 完成條件也是回饋迴路的依據。 它不只用於最後驗收,還讓開發流程知道何時繼續、何時可以結束;遇到超出範圍或重試上限,則依停止條件交回人。不能為了讓實作過關,臨時放寬原本確認的條件。
  • Claude 提出修改,工具執行檢查,而且要知道何時停。 失敗結果要能回到下一輪,不能只留在終端機等人轉貼;規格衝突、超出範圍與重試上限,都要留下可接手的問題。
  • 開發分開,交付再整合。 功能分支與 worktree 留下可審查的修改;本機通過後還要經過 PR、CI 與審查。切回 main 不等於功能已合併。
  • 重試分支還沒被真實模型觸發過。 換小模型、抽掉規格都試過,都沒逼出第二輪。
  • 測試寫到帶 BR 編號與 Given/Then,它就是規格。 這是 Day 6 的紅利,也是我這次盲測失敗的原因,紅燈把答案一起遞了過去;想驗失敗分支,得換一個大到單靠測試講不完的任務。

我想交出去的,不只是打字的工作,也包括那些已有判準、可以反覆檢查的修正。但七個領域情境通過,只表示訂單取消邏輯在這些輸入下符合預期。

明天把同一個旗標送過 API:取消邏輯算出的 RefundRequested 是 true,打一次 HTTP 請求出去,回應裡還是 true 嗎?


參考資料:

本文實作說明

  • 教學訂單固定快照 885e521;開發證據包 examples/sdlc-development。同一服務的歷史快照與維運主入口分開保存,不將不同版本當成同一輪實跑。rules-v2.md 是教學規則,未串真實金流。

  • 既有 runs/tests:SC-03 失敗;runs/impl:七項通過;runs/combined-01:一次產生測試與實作、七項通過;runs/crosscheck-01:兩套測試各放回舊 Domain,皆抓到 SC-03。既有三次模型呼叫採 sonnet、medium effort、讀寫工具、不提供 Bash,由外層測試。

  • 回饋迴路實跑四次(2026-09-22/09-24),全部保留、未挑選;條件與結果見正文表格,每次的 prompt、trace、diff、測試輸出與 report.json 在各 runs/feedback-loop-* 目錄。模型工具皆為 Read/Grep/Glob,外層僅寫入 Domain。解析修正前後與不同模型之間各只一次,不構成成功率或能力比較。

  • 圖表界線:工作類型表為本文整理,非官方分類;雜湊表的檔案範圍依最終內容比對,不單靠雜湊判斷測試有效;抽掉規格仍一輪通過,不能證明開發不需要規格;三輪是教學上限,通過範圍僅限七個領域情境。

  • test-feedback-loop.py 使用模擬模型回覆與真實 .NET 檢查控制流程;紀錄為 runs/feedback-loop-02/controller-tests.txt。它驗的是執行器的失敗回傳與停止邏輯正確,不得當成 Claude 多輪修復證據。

  • 本篇開發與驗證為 AI/程式操作,人工分鐘未知。未證明省時、團隊減載、Owner 接受、API/通知/並行行為或正式部署。檔案檢查與唯讀工具配置不等於完整作業系統隔離。

  • 新增 worktree 驗證:examples/day11-worktree-lab/run-01/evidence 保存 prompt、trace、diff、三次測試輸出、分支 commit 與 report.json。Claude 實際修改、程式執行測試及本機提交;不是人工 Review 或 main 合併。

官方文件與工程經驗

實作附件


上一篇
Day 10|只改一個欄位,Claude 要先查哪些地方?
系列文
買了 Claude Code,然後呢? 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言