AI 工程

Coding agent 修 bug 只補 null check?4 步讓它先追到根因再動手

你叫 coding agent 修一個 TypeError: 'NoneType' object is not subscriptable,它五秒後回報:「已修好,測試全數通過。」你打開 diff,只多了一行:

if data is None:
    return []

測試確實綠了。但 data 為什麼會是 None?沒人回答這個問題。

r/ChatGPTCoding 上有一篇討論串,標題一句話就講完這個現象:Coding agents fix bugs where the error shows up, not where it starts. agent 修 bug,修在錯誤「冒出來」的地方,而不是錯誤「開始」的地方。

這篇不談趨勢,只談一件事:怎麼讓 Claude Code、Codex、Cursor 這類 agent 先追到根因、再動手修。後面會給你可以直接貼的 prompt、CLAUDE.md/AGENTS.md 規則、一支 Stop hook 範例,以及 review diff 時的檢查清單。

一、為什麼這件事值得花時間處理

症狀修補(symptom fix)最麻煩的地方,是它看起來完全像一個好的修正:diff 很小、測試通過、錯誤訊息消失。但它其實做了三件壞事:

  1. 把大聲的錯誤變成安靜的錯誤。 原本會 crash 的地方現在回傳空陣列、預設值或 NaN,下游拿著錯的值繼續跑,等到報表數字不對才有人發現。
  2. 源頭還在繼續產生壞值。 下一個讀到這個值的函式會再爆一次,agent 再補一個 null check,codebase 慢慢長滿 ?. 和 ?? default。
  3. 測試通過給了錯誤的安全感。 你以為「測試綠 = 修好」,但測試只驗證了「不再拋錯」,沒驗證「行為正確」。

第三點不是直覺而已,有研究量化過。下面「數據與限制」那一段會細講。

二、為什麼 agent 特別容易修在報錯點

這不是模型「笨」,而是它拿到的線索和它被獎勵的方式共同造成的。

線索只指到最後一站。 stack trace 告訴你「第 212 行爆了」,那是 agent 手上最具體、最確定的資訊。壞值真正誕生的地方可能在三個檔案之外的設定解析、API 回應轉換、或資料庫查詢,那些地方不會出現在錯誤訊息裡。

往上追很貴,就地補很便宜。 要找源頭,agent 得讀呼叫端、讀資料流、可能還得跑程式看中間狀態,每一步都吃 context。就地加一個 guard 只要改一行,而且馬上就能讓錯誤消失。

完成訊號太弱。 如果你給的「完成條件」是「錯誤不要再出現」或「測試通過」,那加 guard 就是滿足條件最短的路徑。agent 沒有做錯,它只是精準地完成了你(不小心)指定的目標。

Anthropic 在 Claude Code 官方的 Best Practices 文件裡,把這件事直接寫成一條 prompt 對照:不要只說「the build is failing」,要說「貼上錯誤、修好它、驗證 build 成功,address the root cause, don't suppress the error」。官方會特地列出來,代表這是常見到值得寫進文件的失敗模式。

三、運作原理:壞值的一生

報錯點 vs 源頭:壞值在上游誕生,在下游爆炸;症狀修補修在報錯點,根因修正修在產生點

把一個 bug 拆開看,幾乎都是這三站:

  • ① 源頭(產生點):某個函式在某個條件下產生了不合法的值。例如設定檔缺欄位時,load_config() 默默回傳 timeout = None。
  • ② 中繼(傳遞點):沒有人檢查,值被原樣傳下去。Client(timeout=cfg.timeout)。
  • ③ 報錯點:真正拿它做運算的地方才爆掉,TypeError: '>' not supported between 'NoneType' and 'int'。

stack trace 通常只清楚指向 ③。agent 如果只看 ③,最自然的修法就是在 ③ 補 if timeout is None: timeout = 30。這有兩個問題:第一,使用者設定檔漏寫的錯誤被吞掉了,他永遠不知道自己設定錯;第二,其他讀 cfg.timeout 的地方還是會爆。

根因修正則是回到 ①:缺欄位時要嘛丟出清楚的錯誤(「config 缺少 timeout」),要嘛在唯一的地方明確定義預設值。然後補一支測試,專門重現「缺欄位」這個情境,讓它回不來。

一個好用的判斷句:「修完之後,這個壞值還會被產生嗎?」 如果答案是「會,只是沒人會爆了」,那就是症狀修補。

真實例子:SWE-Review 抓到的 NaN guard

這不是假想情境。Huawei、NTU、HKU 的研究團隊(Ruoyu Wang 等人)在 2026 年的論文 SWE-Review 裡,分析了 sympy 的一個 issue(sympy-13877):AI 產生的 PR 加了一個 NaN guard,讓 crash 消失了,但函式仍然回傳錯誤的 nan。真正的缺陷在上游,是某個函式的結果沒有被正確賦值。論文中能抓到這個問題的,是會主動探索 repo、往上游追執行路徑的 agentic reviewer,而不是只看 diff 的單輪 reviewer。

論文裡還有一個值得抄的設計:reviewer 的 prompt 要求先理解 issue、先追出根因,再去看候選 PR。先獨立推理出「應該修在哪」,再拿來對照 PR,就不容易被一個「看起來合理」的 diff 帶著走。這個順序等一下會直接變成我們的招式。

四、數據與限制:「測試通過」到底有多不可靠

以下數字都來自 SWE-bench 相關研究,要注意它們衡量的是「AI 產生的 patch 有多少其實不對」,不是直接衡量「有多少是症狀修補」。症狀修補是其中一類成因,但不是唯一一類。

1. Wang、Pradel、Liu,《Are "Solved Issues" in SWE-bench Really Solved Correctly?》(arXiv 2503.15223)

他們檢查了三個 issue-solving 工具在 SWE-bench Verified 上「通過」的 patch,用自己開發的差異測試技術 PatchDiff 去比對 AI patch 和人類 ground truth patch 的行為:

  • 7.8% 的 patch 被算成正確,但其實沒通過開發者寫的完整測試套件(SWE-bench 的驗證機制只跑部分測試)。
  • 29.6% 的「看似正確」patch,行為和人類的修正不一樣。
  • 這些行為差異中,人工檢查確認 28.6% 是確定錯誤的。
  • 綜合起來,回報的解題率被灌水約 6.2 個百分點。

2. Aleithan、Song Wang 等人,《SWE-Bench+》(arXiv 2410.06992)

他們人工檢查了 SWE-Agent + GPT-4 在 SWE-bench 上「成功」的 patch:31.08% 屬於「可疑 patch」,原因是測試案例太弱,不足以驗證 patch 是否正確。再加上解答直接寫在 issue 裡的「solution leakage」問題(32.67%),兩類都過濾掉之後,解題率從 12.47% 掉到 3.97%。注意這是 2024 年、GPT-4 世代的數據,今天的模型強很多,數字不能直接套用;但「測試弱,patch 就能蒙混過關」這個機制沒有變。

3. Majgaonkar、Sarro、He Ye 等人,《Understanding Code Agent Behaviour》(arXiv 2511.00197)

他們分析 OpenHands、SWE-agent、Prometheus 三個 agent 的執行軌跡,發現就算是失敗的軌跡,也有 72–81% 正確找到了有問題的檔案。換句話說,agent 常常找對了地方,卻沒改對東西。這很支持本文的做法:與其讓 agent 自己一路衝,不如在「定位」和「修改」之間插一個人工確認點。

同一篇也提到,防禦性程式設計(defensive programming)是某些成功案例的策略之一。所以不要矯枉過正把所有 null check 都當成罪。重點不是「禁止 guard」,而是「guard 不能取代源頭修正」。

限制要講清楚:

  • 以上都是 benchmark 研究,場景是開源 Python 專案的 issue,不一定能代表你公司的 codebase。
  • 沒有一篇研究直接統計「agent 有多少比例只補 null check」,那個比例目前沒有可靠數字,本文也不會編一個給你。
  • 我沒有對下面每一招做過量化 A/B 測試;它們是根據官方文件和研究設計整理出來的做法,值得在你自己的 repo 上實測。

五、實戰:讓 agent 先追根、再動手的 4 步

讓 agent 先追根再動手的 4 步:重現、往上追、修產生點、驗證與第二意見,加上自動把關清單

核心概念只有一個:把「找產生點」和「改程式」拆成兩個回合,中間由你把關。 以下用 Claude Code 示範,Codex、Cursor、Cline 的對應做法附在後面。

Step 1:先重現,不要先修

給 agent 完整的錯誤訊息、stack trace、觸發條件,然後明確要求它先寫會紅的測試:

這是錯誤訊息和完整 stack trace:
[貼上]

觸發方式:用缺少 timeout 欄位的 config.yaml 啟動服務。

先不要修。寫一支能穩定重現這個錯誤的測試,跑給我看它是紅的。

這一步的價值是:它逼 agent 描述「在什麼條件下會出錯」,而那個條件往往就是指向源頭的第一條線索。這也跟官方文件的建議一致:描述症狀、可能位置,以及「修好長什麼樣子」,「write a failing test that reproduces the issue, then fix it」。

Step 2:往上追,只准讀、不准改

在 Claude Code 裡按 Shift+Tab 切到 plan mode(狀態列會顯示 ⏸ plan mode on),或直接用 claude --permission-mode plan 開新 session。plan mode 下 Claude 只讀檔、回答問題,不會改東西。然後下這段 prompt:

從報錯點開始,沿著呼叫鏈往上追這個壞值。回答這三件事:
1. 這個值是在哪個檔案、哪一行「第一次」變成不合法的?
2. 它經過哪幾個函式傳到報錯點?列出呼叫鏈。
3. 為什麼在產生點沒被擋下來?是缺驗證、錯誤處理吞掉例外、還是假設錯了?

每個結論都要附上檔案路徑和行號。不確定的地方直接說不確定,不要猜。

如果呼叫鏈很長、要讀很多檔案,可以叫它開 subagent 去查,避免主 session 的 context 被塞爆:

用 subagent 調查 cfg.timeout 在整個 codebase 裡是怎麼被產生和讀取的,回報產生點和所有讀取點。

這一步你要親自看。 研究顯示 agent 常常找對檔案、改錯地方,所以最值得你花 30 秒的就是在這裡確認「它指的源頭是不是真的源頭」。如果它說的源頭就是報錯那一行,追問一句:「為什麼這個值在進入這個函式之前就已經是 None?」

Step 3:修在產生點,而且 diff 越小越好

確認根因後,退出 plan mode 讓它實作:

修在你剛找到的產生點(load_config),不要改報錯點。
要求:
- 缺欄位時丟出清楚的錯誤訊息,說明缺哪個欄位
- 不要在 Client 或下游加 None 檢查
- 跑 Step 1 的測試,確認從紅變綠,並貼出測試輸出

什麼時候可以保留報錯點的防禦性檢查?當那個函式是公開 API、會接收外部輸入時,邊界驗證本來就該做。規則是:guard 可以有,但必須跟源頭修正一起出現,不能單獨出現。

Step 4:驗證 + 第二意見

叫 agent「給證據,不要給宣告」,也就是貼出跑過的指令和輸出,而不是說「已修好」。然後開一個乾淨 context 的 reviewer:

用 subagent 審這份 diff,只回答:
1. 這是症狀修補還是根因修正?判斷依據是什麼?
2. 修完之後,原本的壞值還會在任何地方被產生嗎?
3. 還有哪些其他地方讀取同一個值、可能有同樣問題?
只回報影響正確性的問題,不要提風格建議。

最後一句很重要。Claude Code 文件特別提醒:叫 reviewer「找問題」,它通常一定會找出一些,就算工作沒問題也一樣;照單全收會導致過度工程,包括多餘的防禦性程式碼。所以要限定只回報影響正確性的問題。

Claude Code 內建的 /code-review skill 也能在 fresh subagent 裡審目前的 diff,可以直接用。

六、把規則寫死:CLAUDE.md、AGENTS.md 與 hook

每次都手打上面那些 prompt 太累。把規則寫進專案設定,讓 agent 每個 session 都讀到。

CLAUDE.md(Claude Code)/AGENTS.md(Codex、Cursor 等)

# Debugging
- 修 bug 前,先指出壞值的「產生點」(檔案:行號)和呼叫鏈,再動手改。
- IMPORTANT: 禁止只在報錯處加 null check、預設值、try/except 或型別忽略來讓錯誤消失。
- 防禦性檢查只能跟源頭修正一起出現,並在回報中說明為什麼需要。
- 先寫會重現錯誤的 failing test,修完貼出測試由紅轉綠的輸出。

官方建議 CLAUDE.md 要短,每一行都問自己「拿掉這行,Claude 會不會犯錯?」上面四行就是會犯錯的那種。如果發現它還是常常跳過,文件的建議是只在那一行加 IMPORTANT,不要每行都加。

Codex 讀 repo 根目錄的 AGENTS.md,Cursor 也支援 AGENTS.md 或 .cursor/rules/;Aider 可以把規則寫在 CONVENTIONS.md,再用 --read CONVENTIONS.md 載入。內容可以共用同一份。

加一道 Stop hook:自動擋掉「只有 guard 的 diff」

CLAUDE.md 是建議,hook 是強制。Claude Code 的 Stop hook 會在 agent 準備結束回合時執行你的腳本;腳本用 exit code 2 結束時,會阻止 agent 停下,並把 stderr 的訊息回饋給它。為了防止無限迴圈,hook 輸入裡有 stop_hook_active 欄位(已經因 Stop hook 而繼續時為 true),Claude Code 也內建連續阻擋 8 次就強制結束的上限。

.claude/settings.json:

{
  "hooks": {
    "Stop": [
      { "hooks": [ { "type": "command", "command": ".claude/hooks/symptom-guard.sh" } ] }
    ]
  }
}

.claude/hooks/symptom-guard.sh(範例。我只在一個最小的 Python diff 上確認過它會觸發、會在 stop_hook_active 時放行;pattern 請依你的語言調整,並先在自己的 repo 試跑):

#!/usr/bin/env bash
# 已經被擋過一次就放行,避免無限迴圈
input=$(cat)
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then exit 0; fi

added=$(git diff -U0 | grep '^+' | grep -v '^+++')
[ -z "$added" ] && exit 0

guards=$(echo "$added" | grep -cE '\?\.|\?\? |is None:|== null|except[^:]*:\s*pass|@ts-ignore|# type: ignore')
total=$(echo "$added" | wc -l)

# 新增行裡超過一半是 guard,視為疑似症狀修補
if [ "$total" -gt 0 ] && [ $((guards * 2)) -ge "$total" ]; then
  echo "這份 diff 主要是 null/例外防護。請說明壞值的產生點(檔案:行號),並確認源頭已修正;若 guard 確有必要,說明原因。" >&2
  exit 2
fi
exit 0

這是粗略的啟發式規則,會有誤判(例如你本來就在寫輸入驗證)。它的目的不是精準判決,而是逼 agent 多回答一次「為什麼」。答得出來就放行,答不出來就代表還沒修到根。

七、Review 檢查清單:看到這些就先擋

不管 agent 用什麼工具,review diff 時用這張表快速過一遍:

diff 裡出現 追問
只新增 ?.、?? default、if x is None: return 這個值為什麼會是 None?誰產生的?
except: pass、catch (e) {} 吞掉的是什麼例外?它原本想告訴我們什麼?
@ts-ignore、# type: ignore、as any 型別系統抓到的是真問題嗎?
放寬 assert、修改測試的預期值、刪除測試 是測試錯了,還是程式錯了?證據呢?
修改位置 = stack trace 最上面那一行 源頭是不是就在這裡?怎麼確認的?
沒有新增重現 bug 的測試 怎麼保證它不會回來?

八、什麼時候不必這麼麻煩

根因優先有成本:多一個回合、多讀很多檔案、多花 token。以下情況可以直接讓 agent 修:

  • 錯誤本來就在報錯點。 例如 typo、off-by-one、明顯寫錯的條件。如果你能一句話描述 diff,官方文件也說可以跳過 plan。
  • 外部輸入的邊界。 處理使用者輸入、第三方 API 回應的地方,驗證和 guard 本來就是正確做法,這裡的「報錯點」就是「源頭」。
  • Production 著火的止血。 先上 guard 止血完全合理,但要同時開一張 ticket 追根因,並在 commit message 寫明「這是暫時止血」。

真正該用完整 4 步的,是這些情況:壞值跨模組傳遞、同類錯誤在不同地方反覆出現、或者 agent 已經修過一次同一個 bug 又回來了。

九、對工程團隊的意義:三件這週就能做的事

  1. 把 Debugging 規則加進 CLAUDE.md/AGENTS.md。 四行,五分鐘。這是成本最低、影響面最廣的一步。
  2. 把「先追根」做成可重用的 skill 或 slash command。 在 .claude/skills/root-cause/SKILL.md 寫下 Step 1–4 的 prompt,設 disable-model-invocation: true,之後用 /root-cause 觸發。團隊每個人拿到的都是同一套流程。
  3. 在 code review 規範裡加一條:AI 修 bug 的 PR 必須在描述中寫出產生點和呼叫鏈。 寫不出來就不收。這條規則同時管人和 agent。

agent 修 bug 的速度已經不是瓶頸,瓶頸是它修的是不是對的地方。你能加的最有價值的那一步,就是在它動手之前問一句:「這個壞值是從哪裡來的?」

來源

整理:DataAgent · Coding Agent 實戰教學

文章裡這些把關做法,要在一個團隊裡真的落地、而不是只有你一個人在用,通常卡在流程與共識。我把這部分整理成企業內訓:
coding agent 導入與治理 — 企業內訓與顧問 →

發表迴響

%d 位部落客按了讚: