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 很小、測試通過、錯誤訊息消失。但它其實做了三件壞事:
- 把大聲的錯誤變成安靜的錯誤。 原本會 crash 的地方現在回傳空陣列、預設值或
NaN,下游拿著錯的值繼續跑,等到報表數字不對才有人發現。 - 源頭還在繼續產生壞值。 下一個讀到這個值的函式會再爆一次,agent 再補一個 null check,codebase 慢慢長滿
?.和?? default。 - 測試通過給了錯誤的安全感。 你以為「測試綠 = 修好」,但測試只驗證了「不再拋錯」,沒驗證「行為正確」。
第三點不是直覺而已,有研究量化過。下面「數據與限制」那一段會細講。
二、為什麼 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」。官方會特地列出來,代表這是常見到值得寫進文件的失敗模式。
三、運作原理:壞值的一生

把一個 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 步

核心概念只有一個:把「找產生點」和「改程式」拆成兩個回合,中間由你把關。 以下用 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 又回來了。
九、對工程團隊的意義:三件這週就能做的事
- 把 Debugging 規則加進 CLAUDE.md/AGENTS.md。 四行,五分鐘。這是成本最低、影響面最廣的一步。
- 把「先追根」做成可重用的 skill 或 slash command。 在
.claude/skills/root-cause/SKILL.md寫下 Step 1–4 的 prompt,設disable-model-invocation: true,之後用/root-cause觸發。團隊每個人拿到的都是同一套流程。 - 在 code review 規範裡加一條:AI 修 bug 的 PR 必須在描述中寫出產生點和呼叫鏈。 寫不出來就不收。這條規則同時管人和 agent。
agent 修 bug 的速度已經不是瓶頸,瓶頸是它修的是不是對的地方。你能加的最有價值的那一步,就是在它動手之前問一句:「這個壞值是從哪裡來的?」
來源
- r/ChatGPTCoding 討論串:「Coding agents fix bugs where the error shows up, not where it starts」— https://www.reddit.com/r/ChatGPTCoding/comments/1wri3ux/coding_agents_fix_bugs_where_the_error_shows_up/
- Anthropic,Claude Code Best Practices(Address root causes、plan mode、Stop hook、adversarial review)— https://code.claude.com/docs/en/best-practices
- You Wang、Michael Pradel、Zhongxin Liu,〈Are "Solved Issues" in SWE-bench Really Solved Correctly? An Empirical Study〉— https://arxiv.org/abs/2503.15223
- Reem Aleithan、Haoran Xue、Mohammad Mahdi Mohajer、Elijah Nnorom、Gias Uddin、Song Wang,〈SWE-Bench+: Enhanced Coding Benchmark for LLMs〉— https://arxiv.org/abs/2410.06992
- Oorja Majgaonkar、Zhiwei Fei、Xiang Li、Federica Sarro、He Ye,〈Understanding Code Agent Behaviour: An Empirical Study of Success and Failure Trajectories〉— https://arxiv.org/abs/2511.00197
- Ruoyu Wang 等人(Huawei、NTU、HKU),〈SWE-Review: Closing the Loop on Issue Resolution with Agentic Code Review〉— https://arxiv.org/abs/2607.06065
整理:DataAgent · Coding Agent 實戰教學
文章裡這些把關做法,要在一個團隊裡真的落地、而不是只有你一個人在用,通常卡在流程與共識。我把這部分整理成企業內訓:
coding agent 導入與治理 — 企業內訓與顧問 →


