AIDive

Claude Code 出了 bug,我用 4 個修正讓它不可能再發生

AIDive · 發布

編碼 agent

出了 bug 的 session,和說「很順利」的 retro

Claude Code 在一個真實的 repo 上發佈了一個編號 bug,而四個經過批准的修改讓這個 bug 再也不可能發佈出去。這次 session 為一個小型請款 API 寫了貸項通知單,測試套件全過,但事後手動執行的一項檢查發現,一個被拒絕的請求燒掉了一個發票號碼。接著我們對那次 session 跑了 retro,逐一批准修正,再把同一個任務重跑一次:這次乾淨地完成了。

retro 是團隊在事故之後召開的檢討,這裡則是對 Claude Code 自己的 session 紀錄來做。我們的第一次 retro 開頭就是 "This session was smooth",而它的發現裡沒有一項是那個 bug。解法是七個步驟,依序是:安裝、執行任務、執行 retro 並告訴它哪裡失敗、追溯每個候選項目、把每個倖存者批准進一個載體、commit 並審核,最後重跑任務。

retro 是什麼,以及安裝 /retro

/retro 是 Matt Pocock 做的一個 skill,它檢討的是一次 coding session,而不是你的程式碼。它會讀取 session 紀錄,也就是 Claude Code 為每個 prompt 和每次工具呼叫保存的檔案,並在七個類別中尋找候選項目,例如「本來可以抓到錯誤的檢查」。它依嚴重程度列出這些候選項目,而且依照它的文件,它只會提出建議:在你挑選候選項目之前,什麼都不會改變。

安裝是第一個步驟:在你的 repo 裡,先更新官方 marketplace,再用專案範圍安裝 skills 套件,讓它只存在於這個 repo。我們的 marketplace 還停在不含 retro 的舊版本,所以要先更新。在新的 session 裡輸入斜線,retro 指令就會出現在清單中。

有一個限制:雲端 session 讀不到你機器上的紀錄,所以要在 session 發生的地方執行 retro。

執行你的任務並讓它跑完:我們的 session 把 bug 發佈出去了

第二個步驟是你每天本來就在做的事:在一般的 session 裡輸入你真正的任務,讓它跑完,並且保持 session 開著,因為 retro 要讀的就是它。

我們用的是一個 Node 和 TypeScript 寫的小型請款 API,有自己的測試套件,沒有任何指示檔,沒有 pipeline,也沒有 commit hook。任務很短,就像你自己會輸入的那樣:為已付款的發票加入依序編號的貸項通知單,並且允許以抵銷剩餘金額的方式作廢一張已付款發票。

這次 session 寫了 route、一份 migration 和它自己的測試,並在停下來之前跑了測試套件。接著我們執行財務部門會手動執行的檢查,也就是事先寫好的隱藏測試,結果有一項需求沒過。

訊號 結果
測試套件 42 個測試通過
型別檢查 乾淨
隱藏的財務檢查 失敗:拿到的貸項通知單號碼比預期的更後面

原因是:handler 在驗證請求之前就從計數器取下一個號碼,而且計數器在被讀取的那一刻就已經儲存。金額太大的貸項通知單被拒絕了,這是對的,但號碼還是消失了,這就不對。這次 session 是從現有的開立發票 route 抄來這個順序的,而那個 route 也以同樣的方式洩漏發票號碼。

我們把這個任務跑了兩次,另一次是乾淨的,所以這是一般 session 有時候會發生的事。

執行 /retro,然後告訴它哪裡失敗

第三個步驟是在同一個 session 裡輸入 retro 指令,然後再執行一次,並在後面寫上失敗的情況。我們的那次開頭是:"This session was smooth."

它列出了幾個項目,沒有一個是那個 bug:缺少護欄、沒人檢查的 migration 規則,以及沒有任何東西讓審查者去執行。retro 讀的是 session 紀錄,而 session 從來不知道自己失敗了。

所以要由你告訴它:把失敗當作參數,也就是指令後面的文字,再執行一次指令。我們寫的是:財務部門測試了這個版本,太大的貸項通知單被拒絕了,下一張有效的通知單卻晚了一個號碼,而且 retro 不應該修改任何東西。

這一次它在自己的程式碼裡找到了原因:它先取號碼,後驗證,並且說它是從現有的開立發票 handler 抄來這個順序的。新的候選項目接著出現:它自己寫的一個測試跑了失敗的流程,卻只檢查狀態碼;一個看起來是純函式、其實不是的計數器函式;還有一條沒人寫下來的慣例。

不論你在 session 之後發現了什麼,都要輸入到參數裡,否則 retro 審核的是一次它以為一切順利的 session。

批准之前,把每個候選項目追溯到一個時刻

第四個步驟是 skill 不會自己做的:要求 retro 引述每個候選項目背後的對話紀錄片段,並刪掉所有它拿不出證據的候選項目。

文件解釋了為什麼光有排序清單還不夠:工作一完成,模型就會忘記 session 中的掙扎,轉而編造一般性的建議。所以,凡是你無法追溯到某個具體時刻的候選項目都要捨棄:一句話,要求 retro 為每一項引述對話紀錄。

在我們的第一次 retro,答案很直接:大多數候選項目在對話紀錄裡都沒有依據。它保留了缺少護欄這一項,因為它自己列出 hooks 資料夾時什麼都沒印出來,並把 migration 檢查當成假設性的而捨棄。

在第二次 retro,也就是知道 bug 的那一次,追溯保留了四個候選項目,捨棄了一個。

候選項目 追溯之後
它寫的測試(接受、拒絕、接受,卻從未檢查號碼) 保留,確定
沒有任何警告的計數器函式 保留,確定
沒寫下來的慣例 保留,較弱
缺少的護欄 保留,不過它表示護欄抓不到這個 bug
最後一個候選項目 捨棄:它把同一個時刻算了兩次

該 skill 的 repo 上有一個開放中的 issue,統計了多數 retro 執行之後,使用者都會要求它拿出證明。經過追溯的候選項目仍然是模型在讀自己的紀錄,所以標記為較弱的那一列只是一個判斷,由你決定哪些要帶著走。

把每個倖存者批准進它的載體

第五個步驟是整個循環開始見效的地方:一次批准一個倖存者,每個都進入它的載體,也就是這條規則將要存在的地方。skill 給了你一個階梯。機械性的違規得到一個確定性的檢查,沒有例外:linter 裡的一條規則、一個 commit hook,或 pipeline 裡的一個 job。需要判斷的部分則放進程式規範檔,由審查者來讀。你的指示檔,用文件的話說是「極少」使用,只用來指向其他檔案。

我們批准了四個修改:

  • 缺少的護欄 變成一個納入版本控制的 commit hook,執行型別檢查和測試,只要其中一個失敗就阻擋 commit。執行環境拒絕把這個檔案設為可執行,所以我們自己執行了那個指令。
  • 測試 變成一個鎖住規則的 vitest 測試:接受一張貸項通知單、拒絕一張貸項通知單,下一張有效的必須拿到下一個號碼。它第一次跑就失敗了,這正是重點:它抓到了這次 session 發佈出去的 bug。
  • 需要判斷的部分 變成新的 CODING_STANDARDS.md 裡的一行:只有在所有驗證都通過之後才取序號,讓被拒絕的請求不會在序列中留下缺口。
  • 導覽缺口 變成計數器函式上方的一則註解:它會立刻儲存計數器,所以即使呼叫者之後失敗,號碼也已經用掉了,因此只能在驗證之後呼叫它。

結果是四個檔案被修改,CLAUDE.md 裡一行都沒有,因為兩次 retro 都拒絕寫一行進去。第一次說,指向 steering 檔的指標每一輪都會消耗 context 並且會過時;第二次說,把它留作函式上的註解。文件也支持它們:Claude 把這些檔案當作 context,而不是強制執行的設定,要阻擋某個動作,你用的是 hook。

兩個限制:hook 只保護 commit,而註解用 retro 自己的話說,是對下一個呼叫者的賭注,不是已證明的修正。

關上循環:hook 擋下你的 commit、審核、commit、重跑

第六個步驟是 commit,最後一個步驟是重跑同一個任務,你要在這裡看出循環有沒有撐住。

我們把批准的檔案加入暫存區並 commit 時,hook 先執行,並且拒絕了這次 commit。型別檢查通過,接著新的編號測試失敗了,拿到的號碼比預期的更後面。hook 檢查的是磁碟上現有的檔案,而磁碟上仍然是有 bug 的程式碼。

所以回到 session,只要求修正:在驗證通過之後再取號碼,執行檢查,不要 commit。然後再 commit 一次,hook 全綠,你會得到兩個 commit:先是 retro 的修改,再是帶著修正的功能。retro 本身什麼都沒有 commit,這一點在 repo 上有一個開放中的 issue 一字不差地描述過,所以由你來 commit。

接下來執行 /doctor prompt-audit,這是 Claude Code 自己用來檢查你的指示檔有沒有過時內容的功能。這裡有個陷阱:不給路徑的話,它審核的是你的全域檔案,而不是這個 repo。給了路徑之後,它判定新的規範檔是乾淨的,並指出較舊的開立發票 route 違反了這條規則,那是程式碼的 bug,不是 prompt 的問題。

最後一個步驟是用同一個任務開一個全新的 session。我們的那次在寫任何程式碼之前,先讀了程式規範檔、readme、hook 和紅燈的編號測試,然後在它自己的摘要裡寫道:只有在所有這些檢查都通過之後才取號碼,所以被拒絕的請求不會在序列中留下缺口。

第一次 session 循環之後的全新 session
通過的測試 42 全綠
隱藏的財務檢查 失敗 5 項中 5 項通過
被拒絕的請求之後的號碼 被燒掉,有缺口 沒有缺口

一個誠實的但書:這次全新的 session 拿到了以紅燈測試和規範檔一行的形式呈現的教訓,所以這是循環之後的一次執行,不是基準測試。

結論:什麼時候該跑 retro、什麼時候不該,以及那一條規則

retro 讀的是 session,所以 session 沒看到的東西,要由你寫進參數。這是你最容易踩到的部分。

在一次出錯的 session 之後、在你清掉它之前,或是對舊 session 的紀錄執行 retro。不要讓它自動執行,也不要接到 hook 上:這個 skill 的作者自己說過,你不會想把這件事自動化。文件補充了一個限制:它不會審核你上個月提出的檢查,所以你仍然得自己修剪它們。

要記住的那一條規則就是文件自己的那句話:把你的指示檔當成程式碼,出錯時檢討它,定期修剪,並透過觀察 Claude 的行為是否真的改變,來測試每次修改。這個循環不會讓 agent 變對,它只是讓同樣的錯更快被抓到,而你加進去的檢查就是你現在要維護的程式碼。

這支影片的免費資源包裡有 retro 檢查清單,包含每個步驟、追溯的問題和各個載體,另外還有我們這次執行用的 commit hook 腳本和編號測試,你可以從我們的版本開始。

免費資源

來源

常見問題

Matt Pocock 的 /retro skill 做什麼?
它檢討的是一次 Claude Code session,而不是你的程式碼,讀取記錄了每個 prompt 和每次工具呼叫的 session 紀錄。它在七個類別中尋找候選項目,依嚴重程度排序,而且只會提出建議:在你挑選候選項目之前,什麼都不會改變。
為什麼 retro 說這次 session 很順利?
retro 讀的是 session 紀錄,而我們的 session 從來不知道自己失敗了:它的 42 個測試和型別檢查都是綠的。只有隱藏的財務檢查抓到了 bug,所以第一次 retro 開頭是 'This session was smooth',並且漏掉了它。
如何讓 retro 找出 session 沒看到的失敗?
再執行一次 retro 指令,並把失敗寫在後面當作參數。我們寫的是財務部門發現一張被拒絕的貸項通知單和晚了一個的下一個號碼,這一次 retro 把原因追溯到它自己的程式碼在驗證之前就取了號碼。
如何讓 retro 證明它的發現?
要求它引述每個候選項目背後的對話紀錄片段,並刪掉任何它拿不出證據的項目。在我們的第一次 retro,大多數候選項目都沒有依據;第二次的追溯保留了四個,捨棄了一個。
規則應該放進 CLAUDE.md 嗎?
預設不要:我們批准了四個修改,CLAUDE.md 裡一行都沒寫。文件說 Claude 把它當作 context,而不是強制執行的設定,所以機械性的規則屬於 hook 或測試,需要判斷的部分屬於程式規範檔。
什麼時候該對 Claude Code session 執行 retro?
在一次出錯的 session 之後、在你清掉它之前,或是對舊 session 的紀錄執行。不要把它自動化,也不要接到 hook 上:它的作者說過,你不會想把這件事自動化。

相關影片