iT邦幫忙

2026 iThome 鐵人賽

DAY 0
0
AI 自動化

你的 Agent 流程不該是一篇散文系列 第 3

Day03 沒有測試的 AI 重構:十二輪 review 教我的六種失敗形態

  • 分享至 

  • xImage
  •  

昨天寫到,一條用 skill 編排的 AI 開發 pipeline,在事故與規則不斷累積之後,從 221 行長到了 419 行。

coordinator 文件已經大到沒有人敢改,我當時選擇重構:把共用規則放進 always-on rule,各階段的操作拆進不同 skill,coordinator 只負責調度。

做法和重構程式碼很像,但我漏掉了一件事:程式碼有測試,這批 Markdown 沒有。

十二輪 review

重構後,我讓另一個 AI review 這批修改。

第一輪找到問題,我修掉;第二輪又找到新的。修到後面,review 找到的不只剩下舊問題,還包括前一輪才剛製造出來的問題。

從 Git 紀錄回頭看,8 月 19 日到 9 月 1 日之間,相關修改累積了約 40 個 fix commit,其中有十二輪明確標記的 Codex review。

幾個 commit message 已經很像事故摘要:

address review round 2 — every finding was a second copy of the old rule
retarget the 6 remaining dead citations
reconcile the last two rule copies
enforce the lifecycle guard in code
correct 8 false claims, including a regression introduced in round 11

一開始我覺得,能 review 十二輪代表檢查很仔細。

但修到第十二輪,我開始覺得不對:如果每輪修改都會再製造新的 finding,我缺的可能不是下一輪 review,而是一個能判斷行為是否正確的基準。

測試領域把這種基準叫作 test oracle:給定一個輸入,它能告訴你正確結果應該是什麼。

而這次重構裡,唯一的 oracle,是另一個 AI 再讀一次同一批散文。

第一種:第二份副本

第二輪 review 的 commit message 寫得很直接:

every finding was a second copy of the old rule

那一輪找到六個問題。每一個都是舊規則沒刪乾淨,還留在其他文件裡。

例如,新的流程已經決定不再把 mock 頁面提交進 Git,但 always-on rule 裡還留著「必須提交」的舊指示。

又例如,實際 gate 已經允許 src/ 以外的幾種有效變更,reference 文件卻仍然寫著只有 src/ 才算 implementation。

新規則和舊規則都寫得通順。單獨打開任何一個檔案,都不一定看得出問題。

我把整個 repository 搜尋一遍,才看到同一條規則的不同版本正在互相衝突。

更麻煩的是,這個問題沒有在第二輪結束。到了第八輪,commit message 仍然是:

reconcile the last two rule copies

檔案拆開了,同一條規則還是散落在不同地方。

第二種:死引用

重構時,我刪掉或合併了一批舊的 *-mode.md 文件。

檔案本身不見了,但其他文件裡還有六個地方叫 agent 去讀它們。

所以後來出現了這個 commit:

retarget the 6 remaining dead *-mode.md citations

這種錯誤很難靠閱讀發現。

Reviewer 必須剛好注意到某段文字是一個路徑,再真的沿著路徑走一次,才會知道目標已經不存在。

但對工具來說,這件事其實很簡單:把 Markdown 裡的本地引用抓出來,逐一檢查檔案是否存在就好。

這件事跑一次 link checker 就能確認,我卻叫 AI 靠語意理解去找。

第三種:文件對世界說錯話

第十二輪 review 一次修正了八個錯誤陳述。

其中一份文件說,teardown 會刪除 worktree 裡的 mock;程式碼裡的註解卻寫著,為了保留已發布的 artifact,這些 mock 會刻意留下來。

另一份文件說,某個 archive command 遇到失敗時仍會回傳成功;當時安裝的版本實際上會回傳非零 exit code。

還有一段把一個簡單的正規表示式描述成「驗證 OpenSpec 語意」,但那段程式根本沒有解析 OpenSpec,只是在比對文字。

這次的矛盾發生在文件與實際環境之間。

AI 可以把句子改得很流暢,但光讀文件,不會知道 CLI 實際回傳什麼 exit code、程式怎麼執行,也不知道機器上裝的是哪個版本。

只要環境改變,寫死在散文裡的「事實」就會開始過期。

第四種:只寫了,卻沒有擋住

當時的 Trello 操作文件已經寫著:不帶 lifecycle 參數的 remove-label,只能用來移除一般標籤,不能移除流程狀態標籤。

看起來規則已經存在。

但 CLI 並沒有執行這條規則。

Agent 仍然可以直接移除卡片最後一個 lifecycle label,讓卡片離開整個狀態機。文件會說這個操作不應該發生,程式卻照樣執行。

第十一輪 review 才把它改成真正的 runtime guard:不合法的操作直接失敗。

那個 commit message 是:

enforce the lifecycle guard in code

我到這裡才分清楚:「文件有寫」不等於「系統會擋」。

安全性如果還要靠 agent 記得那句話,這條規則頂多算建議,稱不上 guard。

第五種:名稱碰撞

有一輪 review 發現,同一個變數名稱 $T,在不同指令範例裡分別代表 Trello token 和 script path。

人讀上下文,大概猜得出每一段想表達什麼。但 agent 把幾段指令組合起來執行時,同名變數就可能覆蓋掉前面的值。

第一次修正把部分 $T 改名,後來又出現一個 commit,專門「完成 $T$TCO 的改名」。

第一次改名也沒改乾淨,舊的 $T 又留在其他地方。

如果這些值來自有 schema 的設定,或至少經過一個檢查重複名稱的 lint,這種問題可以在執行前就被擋下來。

但當變數名稱散落在 Markdown code block 裡,它只是另一段需要 reviewer 記得搜尋的文字。

第六種:修 A,壞 B

第十一輪有一個 finding:檢查 mock 檔名時,只要檔名包含卡片 slug 就算通過。

假設正確 slug 是 checkout-flow,那麼 checkout-flow-old.html 也會被誤判成正確檔案。

第十一輪把 substring match 改成精確比對,要求檔名 stem 必須完全等於 slug。

結果這個修正擋掉了另一種原本合法的結構:

public/mock/<slug>/SC1-happy-path.html

Repository 裡當時已經有 99 個這樣的巢狀頁面。

第十二輪 review 才發現,第十一輪的修正讓它們全部失效。最後規則又改成:可以是根目錄下以 slug 命名的檔案,也可以是 slug 目錄裡的頁面。

這就是 regression:一個案例修好了,原本正常的案例卻壞了。

那 99 條真實路徑本來可以直接變成測試 fixture。每次修改 gate 時跑一次,就會立刻知道哪些既有行為被改壞。

但我沒有先把它們變成測試,而是等下一個 AI reviewer 重新發現。

十二輪 review 真正告訴我的事

這十二輪 review 當然有價值。它們找到的問題大多是真的,也攔下不少原本會進入 pipeline 的錯誤。

但 review 被迫同時扮演太多角色:

失敗形態 更適合的檢查方式
第二份副本 單一資料來源、規則 ID、重複定義 lint
死引用 link checker
對世界的錯誤陳述 從程式或設定產生文件、執行 live check
只寫不擋 可執行的 gate 與明確 exit code
名稱碰撞 schema、命名空間、lint
修 A 壞 B characterization test 與真實 fixture

表裡這些檢查都不用猜:檔案是否存在、指令是否成功、輸入有沒有通過,以及既有案例的結果是否改變,都能直接驗證。

AI review 更適合處理需要判斷的問題,例如規則合不合理、有沒有漏掉重要情境,或是否符合人的意圖。

它不應該被拿來代替檔案存在檢查、名稱唯一性、狀態機約束和 regression test。

能重現的 review finding,最後就該變成測試。別再多補一段文件。

我原本以為

只要讓另一個 AI 多 review 幾輪,就能安全地重構這條由 Markdown 組成的 pipeline。

實際上

沒有可執行的預期行為,review 就無法證明重構前後的行為相同。

十二輪 review 的另一面,是十二次用新問題換掉舊問題的機會。

下次重構,我會先列出不能改變的行為,再決定要用哪些測試證明它們還在。


上一篇
Day2 它是怎麼長胖的:一次事故一條規則
系列文
你的 Agent 流程不該是一篇散文3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言