用Superpowers自動調試:讓AI自己找bug、自己修、自己驗證

作者:AI智聞說
日期:2026年5月23日 上午9:46
來源:WeChat 原文

整理版優先睇

速讀 5 個重點 高亮

用systematic-debugging同verification-before-completion兩個Skill,將AI修bug從猜謎變成系統定位-修復-驗證閉環

整理版摘要

呢篇文章係講Superpowers嘅調試體系,重點介紹兩個Skill:systematic-debugging(系統調試)同verification-before-completion(完成前驗證)。作者指出AI修bug成日有三個壞習慣:一係見到錯誤就估住改,唔追根因;二係一次改多個地方,搞到唔知邊個改錯;三係冇驗證就話搞掂咗。呢啲習慣令到修bug變成猜謎遊戲,愈修愈多新bug。

為咗解決呢個問題,Superpowers提出咗一套系統化嘅調試方法。systematic-debugging將調試分成四個階段:根因調查、模式分析、假設驗證、實施修復,每個階段都要完成先可以進入下一個。而verification-before-completion就係一個五步門禁,要求一定要跑完驗證命令、睇過輸出,先可以話搞掂。兩個Skill配合,形成定位→修復→驗證嘅完整閉環。

整體結論係:用呢套系統調試方法,可以將首次修復率從40%提升到95%,引入新bug嘅概率接近零。即使喺時間好急嘅情況下,都應該用系統調試,因為估嘅代價更大。文章仲提供咗兩個真實案例,一個係React白屏,另一個係Node.js工具鏈嘅路徑問題,示範點樣逐步揾根因、修復同驗證。最後講咗三個輔助技術:根因追蹤、縱深防禦、條件等待,幫手應付複雜場景。

  • 結論:系統調試比估住改效率高得多,首次修復率升到95%,新bug接近零。
  • 方法:systematic-debugging必須跟足四個階段——根因調查、模式分析、假設驗證、實施修復,唔準跳步。
  • 差異:verification-before-completion要求一定要跑完整命令先可以話搞掂,用證據代替「應該冇問題」。
  • 啟發:修復完之後要加縱深防禦,喺數據流每一層都加校驗,防止同類bug再出現。
  • 可行動點:遇到bug時,要逼AI行曬四個階段同五步門禁,唔好畀佢估住改。
結構示例

內容片段

內容片段 text
TypeError: Cannot read properties of undefined (reading 'map')  at OrderList (src/components/OrderList.tsx:23:
18) at renderWithHooks (react-dom.js:16175:
18) at mountIndeterminateComponent (react-dom.js:20919:18)
整理重點

AI修bug嘅三大壞習慣同Superpowers嘅鐵律

你畀AI修bug,好大機會係咁:貼個錯誤,AI話「試嚇改呢度」,改完仲係錯;再貼,AI話「試嚇改嗰度」,改完出新錯誤。來回幾輪,bug未修好,反而多咗幾個新bug。

呢個唔係AI能力問題,係AI冇調試紀律。

  • 估住改,唔揾根因:見到錯誤關鍵字就直接畀方案,結果治標唔治本,根因繼續存在。
  • 一次改多個地方:順便「優化」其他位,改完唔知邊個改啱邊個改錯,引入新bug。
  • 冇驗證就話搞掂:AI改完話「應該冇問題」,但冇跑測試,部署上去先發現bug仲喺度。

系統調試15-30分鐘搞掂嘅問題,估-改-估-改要2-3小時;首次修復率從40%升到95%。

整理重點

systematic-debugging:四個階段,唔準跳步

systematic-debugging將調試拆成四個階段,每個階段一定要完成先可以進入下一個。

注意階段3同階段4嘅分別:階段3係驗證假設,階段4先係正式修復。

  1. 1 階段1:根因調查 - 讀錯誤、復現、查變更、多組件收集證據、追蹤數據流。成功標準:理解「咩壞咗」同「點解壞」。
  2. 2 階段2:模式分析 - 揾正常代碼、對比參考實現、對比差異、理解依賴。成功標準:揾到正常同異常嘅差異。
  3. 3 階段3:假設驗證 - 提出一個假設,做最小改動驗證,確認後進入階段4,推翻就重新假設。成功標準:假設被確認或推翻。
  4. 4 階段4:實施修復 - 寫失敗測試、修根因、驗證通過;如果失敗就重新分析或質疑架構。成功標準:bug修復、測試通過。

重要原則:唔確定就承認。Skill原文講明「Say 'I don't understand X'」——唔肯定嘅時候要去問人、研究,唔好裝知然後亂估。

咩時候最需要用systematic-debugging?唔係閒嗰陣,係最急嗰陣。原文話「Use this ESPECIALLY when under time pressure.」因為愈急愈想估,但估嘅代價最大。

系統調試15-30分鐘搞掂嘅問題,估-改-估-改要2-3小時;首次修復率從40%升到95%。

整理重點

verification-before-completion同閉環配合

systematic-debugging修完bug之後,點確認真係修好?靠verification-before-completion。呢個Skill嘅鐵律係:未跑過驗證命令,唔準聲稱完成;「啱先跑過」唔算,因為可能改咗代碼;「應該冇問題」唔算,一定要有命令輸出做證據。

鐵律重點:命令必須新鮮、完整,唔可以用上次結果。

  1. 1 IDENTIFY:咩命令可以證明呢個聲明?
  2. 2 RUN:執行完整命令(要新鮮嘅、完整嘅)。
  3. 3 READ:睇完整輸出,檢查退出碼,數失敗數。
  4. 4 VERIFY:輸出係咪確認咗聲明?如果冇,用證據講實際狀態;如果確認咗,聲明要附帶證據。
  5. 5 ONLY THEN:先可以作聲明。

常見錯誤聲明例如「應該冇問題」、「測試應該能通過」,正確做法係附上命令輸出。

代理報告唔可信,必須自己獨立驗證。

整理重點

三個輔助技術:根因追蹤、縱深防禦、條件等待

根因追蹤:從報錯位置逐層向上追,唔好只修報錯位置,要修源頭。如果追唔通,可以加棧追蹤(new Error().stack)打印完整調用鏈。

縱深防禦:修完bug後,喺數據流經過嘅每一層都加校驗,形成四層模型:入口校驗、業務邏輯校驗、環境守衞、調試日誌。四層一齊,同類bug極難再發生。

條件等待:用「等條件滿足」代替「等固定時間」,例如用waitFor(() => getResult() !== undefined)取代setTimeout。要留意輪詢頻率同超時設定。

三個輔助技術分別對應唔同場景:根因追蹤對付深層錯誤,縱深防禦防止重複,條件等待解決時靈時不靈測試。

systematic-debugging迫AI先揾根因先至改bug,verification-before-completion要求AI要執行完命令先可以話「搞掂曬」。兩個Skill配合,由「估-改-估-改」變成「定位-修復-驗證」三步閉環

寫喺前面

你用AI改bug,多數都係咁:

報咗個錯,將錯誤信息貼畀AI,AI話「試下改呢度」,改咗,又係錯。再貼,AI話「咁試下改嗰度」,改咗,出咗新錯誤。來回幾輪,bug未整好,反而多咗3個新bug。

呢個唔係AI能力唔得,而係AI欠缺調試紀律。

冇紀律嘅調試就係靠估。估一次兩次可能撞啱,但係估嘅效率遠低過系統排查——根據實際使用經驗:系統調試15-30分鐘搞掂嘅問題,估-改-估-改要2-3個鐘;首次修復率由40%提升到95%;系統調試引入新bug嘅機率接近零,估-改-估-改引入新bug係家常便飯。

呢篇文章講Superpowers嘅調試體系:systematic-debugging(系統調試)同verification-before-completion(完成前驗證)點樣配合,將「AI估bug」變成「AI定位bug→修bug→驗證修復」嘅閉環。我會用兩個真實場景行一次完整流程,每一步嘅輸入輸出都寫出嚟。

圖片

一、點解AI修bug容易越修越爛

AI修bug有3個天生嘅壞習慣:

1. 估住嚟改,唔揾根因

你貼咗一行報錯,AI就直接畀方案。佢冇先讀曬完整嘅錯誤信息、冇重現、冇追蹤數據流,就係見到報錯關鍵字就畀修改建議。

結果:只係整咗病徵,根因仲喺度。換個場景,同一個bug會用另一種方式走返出嚟。

2. 一次改多個地方

AI覺得「既然要改,順便將呢幾個相關嘅都優化埋」。一次改3個地方,有一個改啱咗,另外兩個引入咗新bug。但係你唔知邊個改啱、邊個改錯,因為改咗太多。

結果:舊bug未整好,新bug就嚟咗。

3. 冇驗證就宣佈整好

AI改完code,話「應該冇問題㗎喇」。「應該」兩個字係關鍵——佢冇跑測試,冇確認報錯消失,只係覺得code邏輯上冇問題。

結果:你部署咗上去,發現bug仲喺度。

Superpowers嘅systematic-debugging同verification-before-completion兩個Skill就係針對呢3個壞習慣。三條鐵律:

1
未揾到根因之前,唔畀出修復方案
2
一次只改一個地方
3
未跑過命令,唔可以話「搞掂曬」

二、systematic-debugging:4個階段,唔可以跳步

systematic-debugging將調試拆成4個階段,每個階段一定要完成咗先可以進入下一個:

階段
做什麼
成功標準
階段1:根因調查
讀錯誤、重現、查變更、多組件蒐集證據、追蹤數據流
理解咗「乜嘢壞咗」同「點解壞咗」
階段2:模式分析
揾正常code、對比參考實現、對比差異、理解依賴
揾到正常同異常嘅差異
階段3:假設驗證
提出一個假設、做最小改動驗證、確認咗就入階段4,或者推翻再嚟過
假設被確認或者推翻
階段4:實施修復
寫失敗測試、修根因、驗證通過;修復失敗就重新分析或者質疑架構
bug整好咗、測試通過


圖片

留意階段3同階段4嘅分別:階段3係驗證假設(改完睇效果),階段4係正式修復(寫測試、改code、跑驗證)。

階段3驗證之後有兩條路:


假設確認 → 進入階段4正式修復

假設推翻 → 形成新假設,返去階段3重新驗證(唔係喺失敗嘅修改上面疊加更多修改)

仲有一條原則好多人忽略:唔知就認。Skill原文明確講「Say 'I don't understand X'」——唔確定嘅時候,去問人、去研究,唔好扮識然後亂估。

幾時最應該用systematic-debugging?

唔係得閒嘅時候,係最急嘅時候。Skill原文寫得好清楚:「Use this ESPECIALLY when under time pressure.」(有時間壓力嘅時候尤其要用呢個Skill。)

緊急嘅時候,靠估嘅誘惑最大。但係靠估嘅代價都最大——估一次5分鐘,估錯一次5分鐘加回滾10分鐘,3次估錯就已經1個鐘。系統排查15-30分鐘搞掂,慳返嘅時間遠超「試咗先」慳到嘅時間。

三、調試流程速查

入實戰之前,先畀一個完整嘅流程概覽,方便之後對照:

發現bug嘅時候:

1
完整讀錯誤信息,唔好跳過
2
穩定重現,記錄步驟
3
檢查最近變更,睇嚇改咗啲乜
4
多組件系統:喺每個組件邊界加診斷日誌,跑一次收集證據
5
追蹤數據流,由報錯位置向上追到源頭
6
揾正常運作嘅類似code,對比差異
7
如果係實現某個已知模式,完整閲讀參考實現(唔好略讀)
8
理解依賴:組件依賴啲咩其他組件、配置、環境變數
9
提出一個具體假設,做最小改動驗證
10
假設確認之後,寫失敗測試
11
修根因,跑測試
12
跑全量測試,確認冇迴歸

宣稱整好嘅時候:

1
咩命令可以證明?
2
跑完整命令
3
讀完整輸出
4
輸出確認咗聲明?如果冇,用證據說明實際狀態;如果確認咗,聲明要附帶證據
5
確認咗,先可以話「整好咗」

修復失敗嘅時候:

1
數嚇試咗幾多次
2
少過3次:返去階段1,用新資訊重新分析
3
3次或以上:停低質疑架構——每次修復都喺唔同位置暴露新問題、修復需要大規模重構、每次修復喺其他地方產生新症狀,呢啲都係架構問題嘅信號,同人討論完再決定

揾唔到根因嘅時候:

系統性調查之後的確可能會遇到環境因素、時序依賴或者外部原因導致嘅bug。呢個時候應該:記錄調查過程、實現防禦性處理(例如重試機制、超時兜底、友善嘅錯誤提示)、加監控日誌等陣好查。但要留意:絕大多數「揾唔到根因」其實係調查得唔夠徹底。

四、實戰1:報錯位置同根因好近

場景:React + TypeScript項目,用戶反映「提交表單之後頁面白屏」

階段1:根因調查

1.1 完整讀錯誤信息

AI嘅第一反應唔係畀方案,而係先讀錯誤:




TypeErrorCannot read properties of undefined (reading 'map')
  at OrderList (src/components/OrderList.tsx:23:18)
  at renderWithHooks (react-dom.js:16175:18)
  at mountIndeterminateComponent (react-dom.js:20919:18)

錯誤在 OrderList.tsx 第23行,對 undefined 調用了 .map()。呢個表示某個array變數係 undefined,唔係空array。

1.2 穩定重現

AI嘗試重現:

1
打開訂單列表頁 — 正常顯示
2
提交新訂單 — 頁面白屏
3
刷新頁面 — 訂單列表恢復正常

重現條件確認:只有提交新訂單之後先會白屏,刷新之後恢復。

1.3 檢查最近變更




git diff HEAD~3 -- src/components/OrderList.tsx

發現最近一次提交將訂單接口嘅返回類型由 Order[] 改成了 { orders: Order[]; total: number },但 OrderList 組件仲係直接對成個響應做 .map()

1.4 多組件系統收集證據

呢個bug涉及前端同BFF兩層。AI喺組件邊界加診斷日誌:




// 在API調用處
console.error('DEBUG fetchOrders:', { url, params, response: data });

// 在Hook處
console.error('DEBUG useOrders:', { rawData: data, extractedOrders: data?.orders });

// 在組件處
console.error('DEBUG OrderList:', { orders, typetypeof orders, isArrayArray.isArray(orders) });

跑一次,收集到嘅證據:




DEBUG fetchOrders: { url: '/api/orders'params: {page: 1}, response: {orders: [...], total: 42} }
DEBUG useOrders: { rawData: {orders: [...], total: 42}, extractedOrders: undefined }
DEBUG OrderList: { orders: {orders: [...], total: 42}, type: 'object'isArray: false }

證據顯示:API返回格式正確,但useOrders冇從返回值提取 .orders,直接將成個對象傳畀組件。

1.5 追蹤數據流

AI追蹤數據由接口到組件嘅完整路徑:

1
api/orders.ts 的 fetchOrders() 返回 { orders: Order[], total: number } ✓
2
hooks/useOrders.ts 的 useOrders() 將返回值直接存到 state ✗(應該提取 .orders
3
OrderList.tsx 從 useOrders() 拿到 state,直接對 state 做 .map() ✗

問題喺第2步同第3步:Hook冇提取 .orders,組件對成個對象做 .map()

根因揾到咗:接口返回格式變咗,Hook同組件冇同步更新。

到呢一步,AI仲未改任何code。

階段2:模式分析

2.1 揾正常運作嘅類似code

AI搜索項目中其他使用 fetchOrders 嘅地方:




grep -rn "fetchOrders" src/

發現 Dashboard.tsx 都有呼叫 fetchOrders,但佢喺渲染時用咗 data.orders.map(),冇白屏。

2.2 對比參考實現

AI完整閲讀 Dashboard.tsx 中調用 fetchOrders 嘅部分,確認佢正確咁由返回值提取咗 .orders。呢個參考實現就係「正常運作嘅code」。

2.3 對比差異

維度
Dashboard.tsx
OrderList.tsx
呼叫方式
const { data } = useOrders()const orders = useOrders()
渲染方式
data.orders.map()orders.map()
係咪白屏
不白屏
白屏

差異確認:Dashboard 正確咁由返回值拎咗 .ordersOrderList 沒有。

2.4 理解依賴

AI檢查咗 useOrders Hook嘅依賴:佢依賴 fetchOrders 嘅返回格式。當 fetchOrders 嘅返回格式變更時,所有呼叫方都要同步更新。Dashboard 更新咗,OrderList 漏了。

階段3:假設驗證

AI提出假設:「OrderList組件需要由useOrders返回值拎 .orders 屬性」。

做最小改動驗證:




// 修改前
const orders = useOrders();

// 最小改動
const { orders } = useOrders();

淨係改咗呢一行,其他唔鬱。提交表單測試:白屏消失,訂單正常顯示。

假設確認。(呢個case比較簡單,階段3嘅驗證改動同階段4嘅正式修復剛好一樣。喺複雜場景中,階段3可能係臨時加一行日誌或者hardcode一個值嚟驗證假設,確認之後階段4先寫正式嘅修復code同測試。)

階段4:實施修復

systematic-debugging要求先寫失敗測試,再改code。

4.1 寫失敗測試




test('fetchOrders返回新格式時,OrderList正確渲染訂單列表'() => {
  // 模擬新格式的API返回
  mockFetchOrders.mockResolvedValue({
    orders: [{ id1name'測試訂單' }],
    total1,
  });

  render(<OrderList />);

  // 驗證組件能正確從 { orders, total } 中提取 orders 並渲染
  expect(screen.getByText('測試訂單')).toBeInTheDocument();
});

運行測試:失敗。因為組件仲係對成個返回值做 .map()

4.2 修復根因




// 修改前
const orders = useOrders();

// 修復後
const { orders } = useOrders();

運行測試:通過。

4.3 驗證冇其他測試被破壞




npm test

全部通過。

到呢度,呢個bug整好咗。但係呢個例子太簡單——睇報錯信息基本上就估到根因。下面睇一個報錯位置同根因遠離嘅場景,呢個先係systematic-debugging真正發揮價值嘅地方。

五、實戰2:報錯位置同根因隔咗4層

場景:一個Node.js工具鏈項目,測試環境跑CI時,git init 喺源碼目錄執行咗,而唔係喺臨時目錄。呢個令到 .git 文件夾出現喺 packages/core/ 入面,污染咗源碼倉庫。

報錯信息指向嘅係 git init 命令本身,但係根因喺4層調用之外。

階段1:根因調查

讀錯誤信息: git init 在 /Users/jesse/project/packages/core 執行咗,而唔係喺臨時目錄。

復現: 每次跑測試都會喺 packages/core/ 下生成 .git 目錄。

檢查最近變更: 最近改咗 Session.create() 嘅初始化邏輯。

追蹤數據流(5層):

1
git init 在 process.cwd() 執行 ← 空嘅 cwd 參數
2
WorktreeManager.createSessionWorktree(projectDir, sessionId) ← projectDir 係空字串
3
Session.initializeWorkspace() ← 傳咗空字串
4
Session.create() ← 傳咗空字串
5
測試code喺 beforeEach 之前就訪問咗 context.tempDir ← setupCoreTest() 返回 { tempDir: '' }

根因揾到咗:唔係 git init 本身有問題,係測試code喺初始化之前就訪問咗臨時目錄變數,拎到空字串,一路傳到落尾,git init 拎到空字串作為工作目錄,解析做 process.cwd()(源碼目錄)。

報錯位置同根因隔咗4層調用。如果淨係睇報錯位置嚟修——畀 git init 加個目錄檢查——可以整到呢一個場景,但係下次其他命令拎到空路徑,同樣問題一樣會出現。修根因(令到 tempDir 喺被提前訪問時拋錯)先可以一勞永逸。

階段2:模式分析

AI揾到項目中其他地方使用 tempDir 嘅code,發現所有正常使用嘅地方都係喺 beforeEach 之後先訪問。得呢個測試喺 beforeEach 之前訪問咗。

階段3:假設驗證

假設:「將 tempDir 改成 getter,喺 beforeEach 之前訪問就拋錯」。

最小改動:將 tempDir: '' 改成 get tempDir() { if (!this._tempDir) throw new Error('...'); return this._tempDir; }

跑測試:.git 唔再出現喺源碼目錄。假設確認。

階段4:實施修復

寫失敗測試:訪問 context.tempDir 在 beforeEach 之前,斷言拋錯。

修復根因:實現 getter。

跑測試:通過。

但systematic-debugging仲要求一步:縱深防禦。修完根因之後,喺數據流經過嘅每一層都加校驗,令到同類bug喺結構上冇可能再發生:

1
入口校驗Project.create() 校驗 workingDirectory 唔可以為空、一定要存在、一定要係目錄
2
業務邏輯校驗WorkspaceManager 校驗 projectDir 唔可以為空
3
環境守衞:測試環境下,git init 拒絕喺臨時目錄之外執行
4
調試日誌:在 git init 之前記錄目錄、cwd、調用棧

點解要4層而唔係1層?唔同嘅code路徑可能會繞過某一層。入口校驗可以攔到大多數情況,但mock同測試可能繞過;業務邏輯校驗可以攔到邊緣情況,但跨平台差異可能繞過;環境守衞可以攔到特定上下文。4層一齊,同類bug極難再發生。

如果手動追蹤行唔通點算?

上面嘅例子係手動逐層追蹤揾到根因。但係有啲調用鏈好深,手動追蹤會斷。呢個時候有兩個技術可以用:

加棧追蹤: 喺問題操作之前打印完整調用鏈:




async function gitInit(directory: string) {
  const stack = new Error().stack;
  console.error('DEBUG git init:', {
    directory,
    cwd: process.cwd(),
    stack,
  });
  await execFileAsync('git', ['init'], { cwd: directory });
}

用 console.error 而唔係 logger(logger喺測試中可能被靜默)。跑測試之後搜索 DEBUG git init 就可以睇到完整調用鏈。

二分查找污染源: 如果某個測試污染咗全局狀態導致其他測試失敗,但唔知係邊個測試,可以用二分法:逐個跑測試,揾到第一個污染源。Superpowers提供咗一個腳本 find-polluter.sh 嚟做呢件事。

六、verification-before-completion:未跑命令,唔可以話「搞掂曬」

systematic-debugging改完bug之後,點樣確認真係整好咗?靠verification-before-completion。

呢個Skill嘅鐵律:

1
未跑過驗證命令,唔可以聲稱完成
2
「頭先跑過」唔算——一定要用當前code重新跑
3
「應該冇問題」唔算——一定要有命令輸出做證據

佢定義咗一個5步門禁函數:

1
IDENTIFY:咩命令可以證明呢個聲明?
2
RUN:執行完整命令(新鮮嘅、完整嘅)
3
READ:讀完整輸出,檢查退出碼,數失敗數
4
VERIFY:輸出係咪確認咗聲明?如果冇,用證據說明實際狀態;如果確認咗,聲明要附帶證據
5
ONLY THEN:先可以做聲明

常見嘅錯誤聲明同正確聲明對比:

錯誤聲明
正確聲明
「應該冇問題㗎喇」
"運行 npm test,34/34通過,0個失敗」
「測試應該可以通過」
"運行 npm test -- --coverage,覆蓋率87%,0個失敗」
「bug整好咗」
"運行 npm test -- OrderList,白屏測試通過,迴歸測試通過」
「code改完咗」
"運行 git diff,變更2個檔案,+5行-3行」

「頭先跑過」點解唔得? 因為你可能喺兩次運行之間又改咗code。上一次嘅測試結果唔可以證明當前code冇問題。verification-before-completion要求嘅係「新鮮嘅證據」。

容易踩嘅信號

Skill原文列舉咗幾個典型嘅「就快違反鐵律」嘅信號,見到呢啲信號就應該停低:


用咗「應該」、「大概」、「好似」

喺驗證之前表達滿意(「得咗!」「完美!」「搞掂!」)

準備提交/推送/創建PR但未跑驗證

信任代理嘅報告(代理話「成功」,你自己冇驗證)

淨係做咗部分驗證(lint通過咗就以為build都冇問題)

覺得「就係得一次冇問題」

Skill原文記錄咗多次違反驗證鐵律嘅失敗案例,包括:代理報告「成功」但實際有未定義函數、功能唔完整就聲稱完成、信任代理報告導致浪費時間返工。呢啲案例嘅共同點:冇跑命令就聲稱完成,結果部署之後出問題。

七、兩個Skill點樣配合

systematic-debugging同verification-before-completion唔係孤立嘅兩個Skill,佢哋組成一個閉環:

1
systematic-debugging揾到根因
2
systematic-debugging修根因(先寫失敗測試,再改code)
3
verification-before-completion驗證修復(跑測試,確認通過)
4
verification-before-completion驗證冇迴歸(跑全量測試,確認冇破壞其他功能)

少咗第3步同第4步,改bug就少咗閉環。你可能整好咗A但破壞咗B,自己都唔知。

圖片

一個完整嘅調試閉環示例:




你:提交表單後頁面白屏,幫我修一下

AI(進入systematic-debugging階段1):
  讀錯誤信息:TypeErrorCannot read properties ofundefined (reading 'map')
  復現條件:提交新訂單後白屏,刷新後恢復
  檢查變更:最近提交改了接口返回格式
  多組件收集證據:在APIHook、組件邊界加日誌,確認數據在Hook層丟失
  追蹤數據流:接口返回{orders, total},Hook沒有提取.orders

AI(進入階段2):
  找正常代碼:Dashboard.tsx用了data.orders.map(),沒問題
  對比參考實現:完整閲讀Dashboard的調用方式
  對比差異:OrderList沒有取.orders屬性
  理解依賴:useOrders依賴fetchOrders的返回格式

AI(進入階段3):
  假設:OrderList需要從返回值取.orders屬性
  最小改動:const { orders } = useOrders()
  驗證:白屏消失
  假設確認 → 進入階段4

AI(進入階段4):
  寫失敗測試:模擬新格式API返回,斷言組件正確渲染
  運行測試:失敗(確認測試能捕獲bug)
  修復根因:改解構賦值
  運行測試:通過

AI(進入verification-before-completion):
IDENTIFY:什麼命令能證明bug修好了?
RUN:npm test
READ34/34通過,0個失敗
VERIFY:全部通過,確認修復
  聲明:運行npm test,34個測試全部通過,0個失敗。OrderList白屏bug已修復。

你:確認,提交代碼

留意最後一個聲明。AI唔係話「修好咗」,而係話「執行npm test,34個測試全部通過,0個失敗」。前者係主觀判斷,後者係客觀證據。

八、3個輔助技術

systematic-debugging帶咗3個輔助技術,佢哋唔止係「特定場景先用」嘅可選工具——根因追蹤係階段1嘅核心技術,縱深防禦係修復之後嘅必要步驟,條件等待係整時靈時唔靈bug嘅唯一可靠方法。

8.1 根因追蹤(root-cause-tracing)

適用於:錯誤發生喺調用棧深處,你唔清楚數據係由邊度開始錯。

方法:由報錯位置開始,逐層向上追蹤,揾到數據嘅源頭。如果手動追蹤行唔通,加棧追蹤(new Error().stack)打印完整調用鏈。如果係測試污染問題,用二分法逐個跑測試揾到污染源。

核心原則:永遠唔好淨係修報錯位置,要修源頭。 報錯位置係病徵,源頭係根因。修病徵,同一個根因會換個方式走返出嚟。

8.2 縱深防禦(defense-in-depth)

適用於:修完一個bug之後,想防止同類bug再出現。

方法:喺數據流經過嘅每一層都加校驗。4層模型:入口校驗、業務邏輯校驗、環境守衞、調試日誌。

點解需要4層而唔係1層?唔同嘅code路徑可能會繞過某一層。入口校驗可以攔到大多數情況,但mock同測試可能繞過;業務邏輯校驗可以攔到邊緣情況,但跨平台差異可能繞過;環境守衞可以攔到特定上下文,但正常使用唔會觸發。4層一齊,同類bug極難再發生。

8.3 條件等待(condition-based-waiting)

適用於:測試中有 setTimeoutsleep 等硬編碼等待,令到測試時得時唔得。

方法:用「等條件滿足」代替「等固定時間」。




// 錯誤:猜時間
await new Promise(r => setTimeout(r, 50));
const result = getResult();

// 正確:等條件
await waitFor(() => getResult() !== undefined);
const result = getResult();

3個常見錯誤:

1
輪詢得太快setTimeout(check, 1) 浪費CPU,應該10ms輪詢一次
2
冇超時:條件永遠唔滿足會死循環,一定要設超時同明確嘅錯誤信息
3
緩存咗過期數據:喺循環外緩存咗狀態,循環內讀到嘅永遠係舊值,一定要喺循環內呼叫getter

幾時唔應該用條件等待:測試實際嘅時序行為(debounce、throttle間隔)時,應該用固定等待,但一定要加註釋說明點解需要固定等待。

根據實際使用反映:一批時得時唔得嘅測試改成條件等待之後,通過率可以由60%左右提升到接近100%,執行時間都明顯縮短。

九、AI修bug嘅藉口,同Superpowers點樣堵

systematic-debugging嘅藉口:

AI嘅藉口
Superpowers嘅回應
「呢個bug好簡單,直接改就得啦」
簡單嘅bug都有根因,行流程比靠估更快
「我改咗先睇」
未揾到根因之前唔畀出修復方案
「我同時改咗3個地方,效率高」
一次改一個,唔係嘅話分唔清邊個改啱咗
「我試咗2次都未整好,再試多次」
少過3次返去階段1重新分析;3次以上停低質疑架構
「參考實現太長,我按理解嚟做」
冇完整理解一定出bug,一定要完整閲讀參考實現
「我唔係好肯定,但應該係呢個原因」
唔肯定就認,去問人、去研究,唔好扮識

verification-before-completion嘅藉口:

AI嘅藉口
Superpowers嘅回應
「應該冇問題㗎喇」
跑驗證命令
「我好肯定」
肯定唔等於證據
「就係得一次唔驗證」
冇例外
「lint通過咗」
lint通過唔等於build通過
「代理話成功咗」
獨立驗證,唔信代理報告
「太攰唔想驗證」
攰唔係跳過驗證嘅理由
「部分驗證就夠」
部分驗證證明唔到啲乜

第4條藉口「我試咗2次都未整好」特別值得留意。3次規則唔係「試3次就放棄」,而係:


少過3次:返去階段1,用新資訊重新分析,可以再試一次

3次或以上:停低質疑架構。如果每次修復都喺唔同位置暴露新問題、修復需要大規模重構、每次修復喺其他地方產生新症狀——呢啲都係架構問題嘅信號,唔係code層面嘅bug,繼續估只會浪費時間

寫喺最後

調試嘅本質唔係「改code令到報錯消失」,而係「揾到根因,修咗根因,驗證修復」。systematic-debugging迫AI行曬「定位-修復-驗證」嘅完整閉環,verification-before-completion迫AI用命令輸出講嘢而唔係用「應該冇問題」嚟敷衍。兩個Skill配合,將改bug由猜謎遊戲變成工程流程。

改bug嘅紀律比寫code嘅能力更容易被忽略,但大部分開發時間唔係喺寫新code,係喺修緊舊bug。令到AI改bug改得可靠,比令佢寫code寫得快更有價值。


systematic-debugging逼AI先找根因再修bug,verification-before-completion要求AI跑完命令才能說"搞定了"。兩個Skill配合,從"猜-改-猜-改"變成"定位-修復-驗證"三步閉環

寫在前面

你用AI修bug,大概率是這樣的:

報了個錯,把錯誤信息貼給AI,AI說"試試改這個",改了,還是報錯。再貼,AI說"那試試改那個",改了,新錯誤出現了。來回幾輪,bug沒修好,倒是多出來3個新bug。

這不是AI能力不行,是AI缺少調試紀律。

沒有紀律的調試就是猜。猜一次兩次可能蒙對,但猜的效率遠低於系統排查——根據實際使用經驗:系統調試15-30分鐘搞定的問題,猜-改-猜-改要2-3小時;首次修復率從40%提升到95%;系統調試引入新bug的概率接近零,猜-改-猜-改引入新bug是常事。

這篇文章講Superpowers的調試體系:systematic-debugging(系統調試)和verification-before-completion(完成前驗證)怎麼配合,把"AI猜bug"變成"AI定位bug→修bug→驗證修復"的閉環。我會用兩個真實場景走一遍完整流程,每一步的輸入輸出都寫出來。

圖片

一、為什麼AI修bug容易越修越爛

AI修bug有3個天生的壞習慣:

1. 猜着改,不找根因

你貼了一行報錯,AI直接給方案。它沒有先讀完整的錯誤信息、沒有復現、沒有追蹤數據流,就是看到報錯關鍵詞就給修改建議。

結果:修了症狀,根因還在。換個場景,同一個bug換個方式冒出來。

2. 一次改多個地方

AI覺得"既然在改,順便把這幾個相關的也優化一下"。一次改3個地方,有一個改對了,另外兩個引入了新bug。但你不知道哪個改對了、哪個改錯了,因為改了太多。

結果:舊bug沒修好,新bug來了。

3. 沒驗證就宣佈修好了

AI改完代碼,說"應該沒問題了"。"應該"兩個字是關鍵——它沒跑測試,沒確認報錯消失,只是覺得代碼邏輯上沒問題。

結果:你部署上去,發現bug還在。

Superpowers的systematic-debugging和verification-before-completion兩個Skill就是針對這3個壞習慣的。三條鐵律:

1
沒找到根因之前,不允許提修復方案
2
一次只改一個地方
3
沒跑過命令,不能說"搞定了"

二、systematic-debugging:4個階段,不能跳步

systematic-debugging把調試拆成4個階段,每個階段必須完成後才能進入下一個:

階段
做什麼
成功標準
階段1:根因調查
讀錯誤、復現、查變更、多組件收集證據、追蹤數據流
理解了"什麼壞了"和"為什麼壞了"
階段2:模式分析
找正常代碼、對比參考實現、對比差異、理解依賴
找到正常和異常的差異
階段3:假設驗證
提一個假設、做最小改動驗證、確認後進階段4或推翻重來
假設被確認或推翻
階段4:實施修復
寫失敗測試、修根因、驗證通過;修復失敗則重新分析或質疑架構
bug修復、測試通過


圖片

注意階段3和階段4的區別:階段3是驗證假設(改了看效果),階段4是正式修復(寫測試、改代碼、跑驗證)。

階段3驗證後有兩條路:


假設確認 → 進入階段4正式修復

假設推翻 → 形成新假設,回到階段3重新驗證(不是在失敗的修改上疊加更多修改)

還有一條原則很多人忽略:不知道就承認。Skill原文明確說"Say 'I don't understand X'"——不確定的時候,去問人、去研究,不要假裝知道然後瞎猜。

什麼時候最該用systematic-debugging?

不是閒的時候,是最急的時候。Skill原文寫得很明確:"Use this ESPECIALLY when under time pressure."(在時間壓力下尤其要使用這個Skill。)

緊急的時候,猜的誘惑最大。但猜的代價也最大——猜一次5分鐘,猜錯一次5分鐘加回滾10分鐘,3次猜錯就1小時了。系統排查15-30分鐘搞定,省下來的時間遠超"先試試"省下的時間。

三、調試流程速查

在進入實戰之前,先給一個完整的流程概覽,方便後續對照:

發現bug時:

1
完整讀錯誤信息,不要跳過
2
穩定復現,記錄步驟
3
檢查最近變更,看改了什麼
4
多組件系統:在每個組件邊界加診斷日誌,跑一次收集證據
5
追蹤數據流,從報錯位置向上追到源頭
6
找正常工作的類似代碼,對比差異
7
如果在實現某個已知模式,完整閲讀參考實現(不要略讀)
8
理解依賴:組件依賴什麼其他組件、配置、環境變量
9
提一個具體假設,做最小改動驗證
10
假設確認後,寫失敗測試
11
修根因,跑測試
12
跑全量測試,確認沒有迴歸

聲稱修好時:

1
什麼命令能證明?
2
跑完整命令
3
讀完整輸出
4
輸出確認了聲明?如果沒有,用證據說明實際狀態;如果確認了,聲明要附帶證據
5
確認了,才能說"修好了"

修復失敗時:

1
數一下試了幾次
2
少於3次:回到階段1,用新信息重新分析
3
3次或以上:停下來質疑架構——每次修復都在不同位置暴露新問題、修復需要大規模重構、每次修復在其他地方產生新症狀,這些都是架構問題的信號,和人討論後再決定

找不到根因時:

系統性調查後確實可能遇到環境因素、時序依賴或外部原因導致的bug。此時應該:記錄調查過程、實現防禦性處理(如重試機制、超時兜底、友好的錯誤提示)、加監控日誌以備後續排查。但要注意:絕大多數"找不到根因"其實是調查不充分。

四、實戰1:報錯位置和根因很近

場景:React + TypeScript項目,用戶反饋"提交表單後頁面白屏"。

階段1:根因調查

1.1 完整讀錯誤信息

AI的第一反應不是給方案,而是先讀錯誤:




TypeErrorCannot read properties of undefined (reading 'map')
  at OrderList (src/components/OrderList.tsx:23:18)
  at renderWithHooks (react-dom.js:16175:18)
  at mountIndeterminateComponent (react-dom.js:20919:18)

錯誤在 OrderList.tsx 第23行,對 undefined 調用了 .map()。這說明某個數組變量是 undefined,不是空數組。

1.2 穩定復現

AI嘗試復現:

1
打開訂單列表頁 — 正常顯示
2
提交新訂單 — 頁面白屏
3
刷新頁面 — 訂單列表恢復正常

復現條件確認:只有提交新訂單後才會白屏,刷新後恢復。

1.3 檢查最近變更




git diff HEAD~3 -- src/components/OrderList.tsx

發現最近一次提交把訂單接口的返回類型從 Order[] 改成了 { orders: Order[]; total: number },但 OrderList 組件還在直接對整個響應做 .map()

1.4 多組件系統收集證據

這個bug涉及前端和BFF兩層。AI在組件邊界加診斷日誌:




// 在API調用處
console.error('DEBUG fetchOrders:', { url, params, response: data });

// 在Hook處
console.error('DEBUG useOrders:', { rawData: data, extractedOrders: data?.orders });

// 在組件處
console.error('DEBUG OrderList:', { orders, typetypeof orders, isArrayArray.isArray(orders) });

跑一次,收集到的證據:




DEBUG fetchOrders: { url: '/api/orders'params: {page: 1}, response: {orders: [...], total: 42} }
DEBUG useOrders: { rawData: {orders: [...], total: 42}, extractedOrders: undefined }
DEBUG OrderList: { orders: {orders: [...], total: 42}, type: 'object'isArray: false }

證據顯示:API返回格式正確,但useOrders沒有從返回值中提取 .orders,直接把整個對象傳給了組件。

1.5 追蹤數據流

AI追蹤數據從接口到組件的完整路徑:

1
api/orders.ts 的 fetchOrders() 返回 { orders: Order[], total: number } ✓
2
hooks/useOrders.ts 的 useOrders() 把返回值直接存到 state ✗(應該提取 .orders
3
OrderList.tsx 從 useOrders() 拿到 state,直接對 state 做 .map() ✗

問題在第2步和第3步:Hook沒有提取 .orders,組件對整個對象做 .map()

根因找到了:接口返回格式變了,Hook和組件沒有同步更新。

到這一步,AI還沒有改任何代碼。

階段2:模式分析

2.1 找正常工作的類似代碼

AI搜索項目中其他使用了 fetchOrders 的地方:




grep -rn "fetchOrders" src/

發現 Dashboard.tsx 也調用了 fetchOrders,但它在渲染時用了 data.orders.map(),沒有白屏。

2.2 對比參考實現

AI完整閲讀了 Dashboard.tsx 中調用 fetchOrders 的部分,確認它正確地從返回值中提取了 .orders。這個參考實現就是"正常工作的代碼"。

2.3 對比差異

維度
Dashboard.tsx
OrderList.tsx
調用方式
const { data } = useOrders()const orders = useOrders()
渲染方式
data.orders.map()orders.map()
是否白屏
不白屏
白屏

差異確認:Dashboard 正確地從返回值中取了 .ordersOrderList 沒有。

2.4 理解依賴

AI檢查了 useOrders Hook的依賴:它依賴 fetchOrders 的返回格式。當 fetchOrders 的返回格式變更時,所有調用方都需要同步更新。Dashboard 更新了,OrderList 漏了。

階段3:假設驗證

AI提出假設:"OrderList組件需要從useOrders返回值中取 .orders 屬性"。

做最小改動驗證:




// 修改前
const orders = useOrders();

// 最小改動
const { orders } = useOrders();

只改了這一行,其他都不動。提交表單測試:白屏消失,訂單正常顯示。

假設確認。(這個case比較簡單,階段3的驗證改動和階段4的正式修復恰好一樣。在複雜場景中,階段3可能是臨時加一行日誌或hardcode一個值來驗證假設,確認後階段4才寫正式的修復代碼和測試。)

階段4:實施修復

systematic-debugging要求先寫失敗測試,再修代碼。

4.1 寫失敗測試




test('fetchOrders返回新格式時,OrderList正確渲染訂單列表'() => {
  // 模擬新格式的API返回
  mockFetchOrders.mockResolvedValue({
    orders: [{ id1name'測試訂單' }],
    total1,
  });

  render(<OrderList />);

  // 驗證組件能正確從 { orders, total } 中提取 orders 並渲染
  expect(screen.getByText('測試訂單')).toBeInTheDocument();
});

運行測試:失敗。因為組件還在對整個返回值做 .map()

4.2 修復根因




// 修改前
const orders = useOrders();

// 修復後
const { orders } = useOrders();

運行測試:通過。

4.3 驗證沒有其他測試被破壞




npm test

全部通過。

到這裏,這個bug修好了。但這個例子太簡單了——看報錯信息基本就能猜到根因。下面看一個報錯位置和根因遠離的場景,這才是systematic-debugging真正發揮價值的地方。

五、實戰2:報錯位置和根因隔了4層

場景:一個Node.js工具鏈項目,測試環境跑CI時,git init 在源碼目錄執行了,而不是在臨時目錄。這導致 .git 文件夾出現在了 packages/core/ 裏,污染了源碼倉庫。

報錯信息指向的是 git init 命令本身,但根因在4層調用之外。

階段1:根因調查

讀錯誤信息: git init 在 /Users/jesse/project/packages/core 執行了,而不是在臨時目錄。

復現: 每次跑測試都會在 packages/core/ 下生成 .git 目錄。

檢查最近變更: 最近改了 Session.create() 的初始化邏輯。

追蹤數據流(5層):

1
git init 在 process.cwd() 執行 ← 空的 cwd 參數
2
WorktreeManager.createSessionWorktree(projectDir, sessionId) ← projectDir 是空字符串
3
Session.initializeWorkspace() ← 傳了空字符串
4
Session.create() ← 傳了空字符串
5
測試代碼在 beforeEach 之前就訪問了 context.tempDir ← setupCoreTest() 返回 { tempDir: '' }

根因找到了:不是 git init 本身有問題,是測試代碼在初始化之前就訪問了臨時目錄變量,拿到空字符串,一路傳到底,git init 拿到空字符串作為工作目錄,解析為 process.cwd()(源碼目錄)。

報錯位置和根因隔了4層調用。如果只看報錯位置修——給 git init 加個目錄檢查——能修掉這一個場景,但下次其他命令拿到空路徑,同樣的問題還會出現。修根因(讓 tempDir 在被提前訪問時拋錯)才能一勞永逸。

階段2:模式分析

AI找到項目中其他地方使用 tempDir 的代碼,發現所有正常使用的地方都在 beforeEach 之後訪問。只有這一個測試在 beforeEach 之前訪問了。

階段3:假設驗證

假設:"把 tempDir 改成 getter,在 beforeEach 之前訪問就拋錯"。

最小改動:把 tempDir: '' 改成 get tempDir() { if (!this._tempDir) throw new Error('...'); return this._tempDir; }

跑測試:.git 不再出現在源碼目錄。假設確認。

階段4:實施修復

寫失敗測試:訪問 context.tempDir 在 beforeEach 之前,斷言拋錯。

修復根因:實現 getter。

跑測試:通過。

但systematic-debugging還要求一步:縱深防禦。修完根因後,在數據流經過的每一層都加校驗,讓同類bug在結構上不可能發生:

1
入口校驗Project.create() 校驗 workingDirectory 不能為空、必須存在、必須是目錄
2
業務邏輯校驗WorkspaceManager 校驗 projectDir 不能為空
3
環境守衞:測試環境下,git init 拒絕在臨時目錄之外執行
4
調試日誌:在 git init 前記錄目錄、cwd、調用棧

為什麼4層而不是1層?不同的代碼路徑可能繞過某一層。入口校驗能攔住大多數情況,但mock和測試可能繞過;業務邏輯校驗能攔住邊緣情況,但跨平台差異可能繞過;環境守衞能攔住特定上下文。4層一起,同類bug極難再發生。

如果手動追蹤走不通怎麼辦?

上面的例子是手動逐層追蹤找到的根因。但有些調用鏈很深,手動追蹤會斷。這時候有兩個技術可以用:

加棧追蹤: 在問題操作前打印完整調用鏈:




async function gitInit(directory: string) {
  const stack = new Error().stack;
  console.error('DEBUG git init:', {
    directory,
    cwd: process.cwd(),
    stack,
  });
  await execFileAsync('git', ['init'], { cwd: directory });
}

用 console.error 而不是 logger(logger在測試中可能被靜默)。跑測試後搜索 DEBUG git init 就能看到完整調用鏈。

二分查找污染源: 如果某個測試污染了全局狀態導致其他測試失敗,但不知道是哪個測試,可以用二分法:逐個跑測試,找到第一個污染源。Superpowers提供了一個腳本 find-polluter.sh 來做這件事。

六、verification-before-completion:沒跑命令,不能說"搞定了"

systematic-debugging修完bug後,怎麼確認真的修好了?靠verification-before-completion。

這個Skill的鐵律:

1
沒跑過驗證命令,不能聲稱完成
2
"剛才跑過了"不算——必須用當前代碼重新跑
3
"應該沒問題"不算——必須有命令輸出作為證據

它定義了一個5步門禁函數:

1
IDENTIFY:什麼命令能證明這個聲明?
2
RUN:執行完整命令(新鮮的、完整的)
3
READ:讀完整輸出,檢查退出碼,數失敗數
4
VERIFY:輸出是否確認了聲明?如果沒有,用證據說明實際狀態;如果確認了,聲明要附帶證據
5
ONLY THEN:才能做聲明

常見的錯誤聲明和正確聲明對比:

錯誤聲明
正確聲明
"應該沒問題了"
"運行 npm test,34/34通過,0個失敗"
"測試應該能通過"
"運行 npm test -- --coverage,覆蓋率87%,0個失敗"
"bug修好了"
"運行 npm test -- OrderList,白屏測試通過,迴歸測試通過"
"代碼改完了"
"運行 git diff,變更2個文件,+5行-3行"

"剛才跑過了"為什麼不行? 因為你可能在兩次運行之間又改了代碼。上一次的測試結果不能證明當前代碼沒問題。verification-before-completion要求的是"新鮮的證據"。

容易踩的信號

Skill原文列舉了幾個典型的"快要違反鐵律"的信號,看到這些信號就該停下來了:


使用了"應該"、"大概"、"好像"

在驗證之前表達了滿意("好了!""完美!""搞定!")

準備提交/推送/創建PR但還沒跑驗證

信任代理的報告(代理說"成功",你沒自己驗證)

只做了部分驗證(lint通過了就以為build也沒問題)

覺得"就這一次沒問題"

Skill原文記錄了多次違反驗證鐵律的失敗案例,包括:代理報告"成功"但實際有未定義函數、功能不完整就聲稱完成、信任代理報告導致浪費時間返工。這些案例的共同點:沒跑命令就聲稱完成,結果部署後出問題。

七、兩個Skill怎麼配合

systematic-debugging和verification-before-completion不是孤立的兩個Skill,它們組成一個閉環:

1
systematic-debugging找到根因
2
systematic-debugging修根因(先寫失敗測試,再改代碼)
3
verification-before-completion驗證修復(跑測試,確認通過)
4
verification-before-completion驗證無迴歸(跑全量測試,確認沒有破壞其他功能)

少了第3步和第4步,修bug就少了閉環。你可能修了A但破壞了B,自己還不知道。

圖片

一個完整的調試閉環示例:




你:提交表單後頁面白屏,幫我修一下

AI(進入systematic-debugging階段1):
  讀錯誤信息:TypeErrorCannot read properties ofundefined (reading 'map')
  復現條件:提交新訂單後白屏,刷新後恢復
  檢查變更:最近提交改了接口返回格式
  多組件收集證據:在APIHook、組件邊界加日誌,確認數據在Hook層丟失
  追蹤數據流:接口返回{orders, total},Hook沒有提取.orders

AI(進入階段2):
  找正常代碼:Dashboard.tsx用了data.orders.map(),沒問題
  對比參考實現:完整閲讀Dashboard的調用方式
  對比差異:OrderList沒有取.orders屬性
  理解依賴:useOrders依賴fetchOrders的返回格式

AI(進入階段3):
  假設:OrderList需要從返回值取.orders屬性
  最小改動:const { orders } = useOrders()
  驗證:白屏消失
  假設確認 → 進入階段4

AI(進入階段4):
  寫失敗測試:模擬新格式API返回,斷言組件正確渲染
  運行測試:失敗(確認測試能捕獲bug)
  修復根因:改解構賦值
  運行測試:通過

AI(進入verification-before-completion):
IDENTIFY:什麼命令能證明bug修好了?
RUN:npm test
READ34/34通過,0個失敗
VERIFY:全部通過,確認修復
  聲明:運行npm test,34個測試全部通過,0個失敗。OrderList白屏bug已修復。

你:確認,提交代碼

注意最後一個聲明。AI不是說"修好了",而是說"運行npm test,34個測試全部通過,0個失敗"。前者是主觀判斷,後者是客觀證據。

八、3個輔助技術

systematic-debugging帶了3個輔助技術,它們不只是"特定場景才用"的可選工具——根因追蹤是階段1的核心技術,縱深防禦是修復後的必要步驟,條件等待是修時靈時不靈bug的唯一可靠方法。

8.1 根因追蹤(root-cause-tracing)

適用於:錯誤發生在調用棧深處,你不清楚數據是從哪裏開始錯的。

方法:從報錯位置開始,逐層向上追蹤,找到數據的源頭。如果手動追蹤走不通,加棧追蹤(new Error().stack)打印完整調用鏈。如果是測試污染問題,用二分法逐個跑測試找到污染源。

核心原則:永遠不要只修報錯位置,要修源頭。 報錯位置是症狀,源頭是根因。修症狀,同一個根因會換個方式冒出來。

8.2 縱深防禦(defense-in-depth)

適用於:修完一個bug後,想防止同類bug再次出現。

方法:在數據流經過的每一層都加校驗。4層模型:入口校驗、業務邏輯校驗、環境守衞、調試日誌。

為什麼需要4層而不是1層?不同的代碼路徑可能繞過某一層。入口校驗能攔住大多數情況,但mock和測試可能繞過;業務邏輯校驗能攔住邊緣情況,但跨平台差異可能繞過;環境守衞能攔住特定上下文,但正常使用不會觸發。4層一起,同類bug極難再發生。

8.3 條件等待(condition-based-waiting)

適用於:測試中有 setTimeoutsleep 等硬編碼等待,導致測試時靈時不靈。

方法:用"等待條件滿足"替代"等待固定時間"。




// 錯誤:猜時間
await new Promise(r => setTimeout(r, 50));
const result = getResult();

// 正確:等條件
await waitFor(() => getResult() !== undefined);
const result = getResult();

3個常見錯誤:

1
輪詢太快setTimeout(check, 1) 浪費CPU,應該10ms輪詢一次
2
沒有超時:條件永遠不滿足會死循環,必須設超時和明確的錯誤信息
3
緩存過期數據:在循環外緩存了狀態,循環內讀到的永遠是舊值,必須在循環內調用getter

什麼時候不該用條件等待:測試實際的時序行為(debounce、throttle間隔)時,應該用固定等待,但必須註釋說明為什麼需要固定等待。

根據實際使用反饋:一批時靈時不靈的測試改成條件等待後,通過率可以從60%左右提升到接近100%,執行時間也明顯縮短。

九、AI修bug的藉口,和Superpowers怎麼堵

systematic-debugging的藉口:

AI的藉口
Superpowers的回應
"這個bug很簡單,直接改就行"
簡單的bug也有根因,走流程比猜更快
"我先改了再看"
沒找到根因之前不允許提修復方案
"我同時改了3個地方,效率高"
一次改一個,不然分不清哪個改對了
"我試了2次都沒修好,再試一次"
少於3次回到階段1重新分析;3次以上停下來質疑架構
"參考實現太長了,我按理解來"
不完整理解保證出bug,必須完整閲讀參考實現
"我不太確定,但應該是這個原因"
不確定就承認,去問人、去研究,不要假裝知道

verification-before-completion的藉口:

AI的藉口
Superpowers的回應
"應該沒問題了"
跑驗證命令
"我很確定"
確定不等於證據
"就這一次不驗證"
沒有例外
"lint通過了"
lint通過不等於build通過
"代理說成功了"
獨立驗證,不信任代理報告
"太累了不想驗證"
累不是跳過驗證的理由
"部分驗證就夠了"
部分驗證證明不了什麼

第4條藉口"我試了2次都沒修好"特別值得注意。3次規則不是"試3次就放棄",而是:


少於3次:回到階段1,用新信息重新分析,可以再試一次

3次或以上:停下來質疑架構。如果每次修復都在不同位置暴露新問題、修復需要大規模重構、每次修復在其他地方產生新症狀——這些都是架構問題的信號,不是代碼層面的bug,繼續猜只會浪費時間

寫在最後

調試的本質不是"改代碼讓報錯消失",而是"找到根因,修掉根因,驗證修復"。systematic-debugging逼AI走完"定位-修復-驗證"的完整閉環,verification-before-completion逼AI用命令輸出說話而不是用"應該沒問題"糊弄。兩個Skill配合,把修bug從猜謎遊戲變成工程流程。

修bug的紀律比寫代碼的能力更容易被忽視,但大部分開發時間不是在寫新代碼,是在修舊bug。讓AI修bug修得靠譜,比讓它寫代碼寫得快更有價值。