Debug AI 的 code — 錯誤訊息閱讀術 2.0
這章結束你會面對 AI code 報錯不慌,能用錯誤訊息、二分隔離與重開規格把問題拉回可控範圍。
開場 · AI code 壞掉時
你請 AI 幫你做一個倒數計時器。它看起來很有自信,貼出一大段 code,還說已經完成。你按下執行,畫面卻沒有倒數,只跳出一串紅字。很多新手到這裡會開始亂問:幫我修、還是不行、再修一次。結果 AI 像不熟業務的實習生,越補越亂。真正的除錯不是跟它賭運氣,而是把現場整理成它能處理的任務。
AI code 壞掉,通常不是同一種壞
除錯(debug)不是看到紅字就把整段丟回去。第一步是先判斷這份食譜是哪一類出問題。AI(artificial intelligence)像很快的實習生,會照你的話做,但它不一定知道你沒說出口的限制,也不一定真的檢查過環境。把所有錯都當成同一種錯,只會讓你一直要求它重寫,卻不知道重寫方向。
第一種原因是需求沒講清。你說做倒數計時器,但沒有說按鈕要能暫停、時間到要不要發出提示、輸入空白時怎麼辦。AI 會自己補空白,補出來的版本可能跟你心裡想的不一樣。這不是它故意亂寫,而是你給的規格像缺步驟的食譜,實習生只好猜。
第二種原因是幻覺(hallucination)。AI 有時會寫出看起來像真的函式名稱、套件名稱或設定名稱,但瀏覽器裡其實不存在。錯誤訊息常會出現 is not defined 或 is not a function。這時不要急著學那個名詞,先問:這個名字在哪裡被建立?瀏覽器真的認得它嗎?
第三種原因是環境不合。AI 可能寫了需要伺服器、需要外部套件、需要特定檔案路徑的 code,可是本課是零安裝,只有瀏覽器 console 和頁面 playground。第四種原因是改 A 壞 B,也叫回歸(regression)。它修好了倒數,但按鈕不能再按;修好了按鈕,時間到又不會停。你要先分型,才知道下一句 prompt 要補需求、抓幻覺、換環境說法,還是縮小範圍測改動。
const failures = [
{ symptom: '按鈕沒反應', likelyCause: '需求沒講清:沒有說按下後要改變哪個狀態' },
{ symptom: 'ReferenceError: timer is not defined', likelyCause: 'AI 幻覺或漏建立變數' },
{ symptom: 'Cannot use import statement outside a module', likelyCause: '環境不合:瀏覽器 console 不吃這種寫法' },
{ symptom: '修好倒數後,暫停壞掉', likelyCause: '改 A 壞 B:需要回頭測舊功能' }
];
failures.forEach((item, index) => {
console.log((index + 1) + '. ' + item.symptom + ' -> ' + item.likelyCause);
});
餵回錯誤訊息,要給完整現場
錯誤訊息(error message)不是垃圾文字,它是瀏覽器留下的現場紀錄。很多人只貼一句「壞了」或只貼最後一行紅字,AI 就只能猜。正確做法是把錯誤全文貼回去,並加上三段式:我做了什麼、我期待什麼、實際發生什麼。這三段像把食譜、目標成品、廚房現場一起交給實習生。
第一段「做了什麼」要具體到可重演。例如:我把你給的 code 貼到瀏覽器 console,按 Enter 後再按頁面上的 Start。不要只說「我試了」。第二段「期待什麼」要描述可觀察結果。例如:畫面上的數字應該從 10 每秒減到 0。第三段「實際發生什麼」要貼錯誤全文、畫面狀態、已試過的動作。例如:console 出現 ReferenceError: countdown is not defined,畫面數字停在 10。
貼錯誤時不要自己改寫成比較像人話的摘要,除非你同時保留原文。紅字裡的行號、名稱、冒號後面的句子,都是 AI 判斷病灶的位置。也不要把金鑰、密碼、私人資料貼出去;如果錯誤訊息裡有這些,先用 [已遮蔽] 取代。除錯力不是把所有東西交出去,而是交出足夠資訊又守住邊界。
一個好回報會長這樣:我把你上一版 code 貼到 console。期待按 Start 後從 10 開始倒數。實際上按鈕沒有反應,console 顯示完整錯誤如下。請先判斷最可能原因,不要直接重寫整份,先給最小修改。這句最後很重要,因為它把 AI 從「大改一輪」拉回「小步修正」。
const report = {
did: '我把上一版 code 貼到瀏覽器 console,按 Enter,接著按 Start。',
expected: '畫面數字應該從 10 每秒減 1,最後停在 0。',
actual: '按鈕沒有反應,console 顯示 ReferenceError: countdown is not defined。',
request: '請先判斷最可能原因,只給最小修改,不要重寫整份。'
};
console.log('做了什麼:' + report.did);
console.log('期待什麼:' + report.expected);
console.log('實際發生:' + report.actual);
console.log('請 AI 做的事:' + report.request);
二分隔離:不要在整鍋湯裡找一粒鹽
二分隔離(binary isolation)是 ch08 學過的二分法升級版。當一大段 code 壞掉,你不需要一次看懂全部。你先把問題切成兩半:前半段是否正常?後半段是否正常?如果前半段正常,病灶大概在後半段;如果前半段也壞,先看前半段。這像檢查一份長食譜,先確認備料有沒有錯,再確認烹調步驟。
在頁面 code 裡,常見切法是「資料」、「畫面」、「事件」、「計時」。資料是現在秒數、是否正在跑。畫面是把秒數顯示出來。事件是按鈕被按下後會做什麼。計時是每秒更新一次。倒數計時器壞掉時,不要從第一行讀到最後一行再祈禱。先問:資料有變嗎?如果資料會變但畫面不變,問題在顯示。資料根本沒變,問題在事件或計時。
新手可以用最笨但最穩的做法:加 console.log。這不是低級技巧,而是把暗處的狀態打開。按下按鈕時印出「按到了」,每秒印出目前秒數,更新畫面前印出要顯示的字。只要某一句 log 沒出現,你就知道程式沒有走到那一步。這比叫 AI 猜快很多。
要 AI 幫你二分時,也要給它明確任務。不要說「幫我找 bug」。改說:請幫我在不改功能的前提下,加上 4 個 console.log,分別檢查按鈕事件、秒數變化、畫面更新、停止條件。這樣 AI 做的是加檢查點,不是重新發明整份程式。等你找到病灶,再要求它只改那一小段。
let seconds = 3;
function tick() {
console.log('[檢查點 A] tick 被呼叫,seconds = ' + seconds);
seconds = seconds - 1;
console.log('[檢查點 B] 扣完秒數,seconds = ' + seconds);
const displayText = seconds + ' 秒';
console.log('[檢查點 C] 準備顯示:' + displayText);
if (seconds <= 0) {
console.log('[檢查點 D] 到 0,應該停止');
}
}
tick();
tick();
tick();
偵測迴圈地獄:修三次還壞,就不是努力問題
迴圈地獄不是 ch03 學過的迴圈語法,而是你和 AI 的對話進入壞循環:AI 修 A,B 壞掉;AI 修 B,A 又壞掉。你每次都貼一句「還是不行」,它每次都自信改一大片。這時問題通常不在某一行 code,而在規格、測試案例、邊界情況沒有被固定下來。
判斷標準可以很務實:同一個功能修了三輪仍沒有穩定通過,就停止加碼。三輪不是魔法數字,而是提醒你成本已經升高。第一輪可以修明顯錯字。第二輪可以補漏掉的變數或事件。第三輪還在破壞舊功能,就代表 AI 沒有一張清楚地圖,只是在局部猜測。
遇到這種情況,不要再要求「再幫我修」。你要改成整理戰場。列出目前已知事實:哪一版還能跑、哪個錯誤訊息最早出現、哪些功能必須保留、哪些做法已經試過但失敗。這些資訊比新的抱怨更有價值。對 AI 來說,這等於把散落的食材重新分類,讓它回到可執行的食譜。
也要小心 AI 給兩個修法時的選擇。大改版通常看起來乾淨,但會把你已經驗證過的部分一起換掉。最小修改通常比較無聊,卻最適合除錯階段。只要目標是救回一個正在壞的功能,優先選能保留原本結構、只動病灶的方案。等功能穩了,再討論整理 code。
你還可以替自己設一張停止表。每修一輪就記三件事:這輪改了哪一小段、哪個測試通過、哪個舊功能壞掉。如果表上連續出現「同一類功能反覆壞」,就不要再把下一輪交給運氣。先把表貼給 AI,要求它根據表格判斷共同原因,並限制它只能提出一個最小假設。這會讓對話從情緒性的催促,回到可驗證的工程步驟。
const attempts = [
{ round: 1, fixed: '紅字消失', broke: '倒數不會停止' },
{ round: 2, fixed: '倒數會停止', broke: 'Start 可以連按造成加速' },
{ round: 3, fixed: 'Start 不能連按', broke: 'Reset 壞掉' }
];
const stillBreakingOldFeatures = attempts.some(item => item.broke.length > 0);
console.log('已修 ' + attempts.length + ' 輪');
console.log('是否仍在破壞舊功能:' + stillBreakingOldFeatures);
if (attempts.length >= 3 && stillBreakingOldFeatures) {
console.log('停止連續修補:改整理規格與已知失敗案例');
}
砍掉重練不是放棄,是帶著邊界重開局
砍掉重練不是把整段刪掉再祈禱 AI 變聰明,而是帶著這次學到的邊界情況重寫 prompt(提示詞)。你已經知道哪些需求一開始沒說,知道哪種環境不能用,知道哪些錯誤修法會破壞功能。這些就是新版規格的材料。重開時要把它們寫進去,不然你只是把同一份缺步驟食譜再交給同一位實習生。
好的重開 prompt 會包含四塊。第一塊是目標:做一個可在瀏覽器 console 示範邏輯的倒數計時器。第二塊是限制:不用外部套件、不用伺服器、不使用不存在的 API。第三塊是功能清單:Start 從 10 倒數到 0、不能連按加速、Reset 回到 10、到 0 停止。第四塊是驗證:請附上用 console.log 可看到的測試流程與預期輸出。
在還沒有 ch17 的版本控制(version control)工具前,你也要有保底的存檔觀念。每次 AI 給你一版可以跑的 code,先留下「可跑版」。下一輪改壞時,至少能回到上一個可用狀態。這就是版本回退(rollback)的概念:不是每次都從廢墟上加蓋,而是回到最近一個穩定存檔再往前走。
實作上可以很樸素:把可跑版貼到一個暫存區,標上日期、通過的測試、還沒做的功能。不要只標「最新版」,因為最新版不一定最好。你要能說出哪一版是穩定版,哪一版正在實驗。這個習慣會讓你敢請 AI 小步修改,因為失敗時有地方可以退。
你真正要養成的行為是:小步改、小步測、留下可跑版。AI 的速度很快,所以更需要你當現場負責人。它負責產出候選食譜,你負責判斷這份食譜是否符合需求、是否能在你的廚房執行、是否破壞已經通過的測試。會除錯的人不是永遠不遇到錯,而是遇到錯時知道怎麼縮小、回報、停止、重開。
const restartPromptParts = {
goal: '做一個倒數計時器邏輯示範',
limits: ['純 JavaScript', '不使用外部套件', '可在瀏覽器 console 執行'],
features: ['Start 從 10 到 0', '不能連按加速', 'Reset 回到 10', '到 0 停止'],
verification: '請附 console.log 測試流程與預期輸出'
};
console.log('新版 prompt 檢查:');
console.log('目標:' + restartPromptParts.goal);
console.log('限制:' + restartPromptParts.limits.join('、'));
console.log('功能:' + restartPromptParts.features.join('、'));
console.log('驗證:' + restartPromptParts.verification);
除錯道場:救回倒數計時器
依序選擇你的除錯動作;走最穩路徑可在 4 步內通關,錯路會看到功能越修越亂。 操作方式:閱讀目前線索,選一個除錯動作;每次選擇後觀察回饋、破壞清單與信任度。
初始情境
需求是 Start 從 10 每秒到 0、Stop 可暫停、Reset 回 10、不能連按加速。錯誤訊息顯示:TypeError: Cannot set properties of null (setting 'textContent')。
let seconds = 10;
function start(){
setInterval(() => {
seconds--;
document.querySelector('#time').textContent = seconds;
}, 1000);
}
目前線索
#time 找不到,畫面停在 10。
已破壞功能
- 尚未破壞舊功能。
信任度
3 / 3,現場仍可控。
重開 prompt
請用純 JavaScript 寫倒數計時器,不用外部套件;Start 從 10 到 0,不能連按加速;Stop 暫停;Reset 回 10;請附 console.log 測試流程;保留 id 為 timer 的元素。
let intervalId = null;
let seconds = 10;
function start() {
if (intervalId) return;
intervalId = setInterval(() => {
seconds--;
document.querySelector('#timer').textContent = seconds;
if (seconds <= 0) {
clearInterval(intervalId);
intervalId = null;
}
}, 1000);
}
function stop() {
clearInterval(intervalId);
intervalId = null;
}
function reset() {
stop();
seconds = 10;
document.querySelector('#timer').textContent = seconds;
}
純 vibe coder 只會一直說「再修一下」,你會先分型、貼完整現場、要求最小修改。它修一輪你測一輪,舊功能壞掉就立刻記錄,不讓對話漂走。三次還不穩,你會停下來重開規格,帶著已知邊界重新請 AI 寫。這些動作讓你成為現場負責人,而不是等 AI 猜中。
檢查一下這章
下一章預告
下一章會把這套除錯 SOP 往前推一步:學會把大任務拆成小步,讓 AI 每一步都能交付、驗證、再前進。