跳到主要內容

【2026 最新】AI 錯誤排查怎麼做?用四種假回應分清 HTTP、串流、工具與回答文字

最後更新: ·
AI 錯誤排查教學:先查 HTTP、串流、工具與回答文字四層

你請 Agent 讀一份文件,它卻回答「API 壞了,帳號被擋」。畫面還附上一段像官方格式的 JSON。這時做 AI 錯誤排查,第一步該換金鑰、重啟,還是先問:這段話究竟從哪裡來?

這篇寫給第一次查看 Agent 紀錄的讀者。前半用包裹比喻說明,後半只要已安裝 Python 3.7 以上,就能複製一個本機練習,親眼看到四種相似的「錯誤」如何出現在不同位置。完成後,你會留下一份狀態、事件、工具結果與回答文字分開的紀錄。

AlphaLab 於 2026 年 11 月 9 日執行下方四個固定測例,保存客戶端與服務端紀錄。它們是我們指定內容的假回應,未呼叫模型、Hermes 或付費 API;驗證範圍是訊息所在層次與讀取方式。

先說結論:AI 錯誤排查=先認封套,再核對收據

錯誤的證據力,要看它在哪一層被記錄。把一次任務想成包裹:HTTP 是外層配送回條,串流事件是分批送來的通知,工具結果是代辦工作的回報,assistant 文字則是助理口頭說明。口頭說「配送失敗」,和回條上真的蓋了失敗章,應該分開保存。

  • HTTP 503:這次收到的 HTTP 回應確實用了伺服器錯誤狀態。
  • HTTP 200+串流錯誤:外層成功傳回回應,裡面的應用事件仍可能失敗。
  • HTTP 200+工具錯誤:訊息送到了,工具執行的工作被拒絕或失敗。
  • HTTP 200+回答內的錯誤字串:目前證明的是回答含這段文字;成因仍需另找紀錄。

一、四層怎麼分?同樣的 error,位置改變就換了問題

① HTTP 狀態:「這次回應用了哪個狀態?」

HTTP(客戶端與伺服器交換請求、回應的協定)有自己的狀態碼。RFC 9110 對 503 的定義是服務暫時無法處理,可能因過載或維護;回應可附 Retry-After 提示等待。本文選 503 當第一個測例。你要找的是實際回應狀態,不是畫面文字裡碰巧出現「503」。

即使收到 503,也先記下你連到的主機;回應可能經過代理或閘道。這份回應證明了觀察到的 HTTP 失敗,尚不足以單獨判定是哪一家服務的哪個元件故障。

② SSE 事件:「回應開始後,後面送來什麼?」

SSE(Server-Sent Events,伺服器持續傳送文字事件)像店員先接起電話,再一段一段報進度。WHATWG 的 SSE 規範把 event: 定義為事件名稱、data: 定義為內容,空白行用來結束一筆事件。text/event-stream 則是這種回應的內容類型。

HTTP 200 不等於整個生成任務做完。Hermes 官方 FAQ明確提到,OpenRouter 可能在 HTTP 200 串流裡轉送上游帳戶政策拒絕。這是機制的官方例子,不能拿來替某張社群截圖判定原因。本文的 event: error 與 demo_busy 是練習自訂格式;各家 API 的事件名稱與完成條件要各自讀文件。

③ 工具結果:「代辦的工作完成了嗎?」

Agent 常把搜尋、讀檔交給工具。MCP(讓應用程式連接工具與資料的協定)的 工具錯誤規範區分兩件事:協定錯誤可用 JSON-RPC 的 error;工具執行錯誤則可放在 result 裡,以 isError: true 表示。合法地收到一個結果封套,仍可能收到「工作失敗」的結果。

檢查時要沿著真實工具呼叫的回傳位置找,並核對呼叫識別碼。任意文件中寫著 isError: true,不會因此變成 MCP 的執行紀錄。下方測例只模擬一個這種形狀的工具回傳,沒有啟動完整 MCP 連線。

④ assistant 文字:「這是紀錄,還是對紀錄的描述?」

助理能在正常回答裡引用錯誤 JSON,也可能給出錯誤的原因解釋。Hermes FAQ 提醒:只有助理敘述時,不宜把它歸因給隱藏的執行政策。缺少獨立紀錄,代表原因尚未確認;並不等於已證明助理在編造。它也可能在轉述真正的工具錯誤。

四層是觀察位置,不是四個互斥的根本原因。一次上游拒絕可能先成為串流事件,被工具包成失敗結果,最後又被助理轉述。你要找最早留有證據的環節,再追上游。想先認識模型與執行層的分工,可接著讀 Agent Harness 白話教學。

AI 錯誤排查四層判讀:HTTP、串流事件、工具結果與回答文字
四個固定測例的判讀圖。200 可承載應用錯誤;只見文字時保留未知。圖/AlphaLab。

二、AI 錯誤排查動手做:一個檔案跑出四張收據

你可能擔心測試會用掉 API 額度。這裡先做本機假回應:程式只監聽 127.0.0.1,由作業系統挑可用連接埠,再自己讀取四個端點。需要的是 Python 3.7 以上;不必輸入帳號、金鑰或模型名稱。Python 官方的 http.server 文件說明了設定狀態、標頭與回應內容的方法。

先在自己的練習資料夾開終端機,執行 python3 --version 確認環境。確認版本至少是 3.7 後,建立純文字檔 error_lab.py,完整貼入以下內容。檔名不要取成 json.py 或 http.py,以免蓋過要匯入的模組。

import json
import threading
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import urlopen

# Four fixed fixtures, not a model, provider API, or complete MCP server.
fixtures = {
    "http": (503, "application/json", {"error": "demo_busy"}),
    "stream": (200, "text/event-stream",
               'event: error\ndata: {"type":"demo_busy"}\n\n'),
    "tool": (200, "application/json", {
        "jsonrpc": "2.0", "id": 7, "result": {
            "isError": True,
            "content": [{"type": "text", "text": "demo_denied"}]}}),
    "text": (200, "application/json", {
        "role": "assistant", "content": '{"error":"demo_denied"}'}),
}
server_log = []


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass

    def do_GET(self):
        name = self.path.lstrip("/")
        if name not in fixtures:
            self.send_error(404)
            return
        status, kind, value = fixtures[name]
        body = value if isinstance(value, str) else json.dumps(value)
        data = body.encode("utf-8")
        rid = "mock-" + name
        server_log.append({"request_id": rid, "case": name,
                           "status": status, "time": datetime.now(timezone.utc).isoformat()})
        self.send_response(status)
        self.send_header("Content-Type", kind + "; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.send_header("X-Request-ID", rid)
        if status == 503:
            self.send_header("Retry-After", "2")
        self.end_headers()
        self.wfile.write(data)


server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
receipts = []
try:
    for name in fixtures:
        try:
            response = urlopen("http://127.0.0.1:%d/%s" % (server.server_port, name), timeout=5)
        except HTTPError as exc:
            response = exc
        with response:
            body = response.read().decode("utf-8")
            receipt = {"case": name, "status": response.code,
                       "request_id": response.headers.get("X-Request-ID"),
                       "content_type": response.headers.get("Content-Type"),
                       "retry_after": response.headers.get("Retry-After"), "body": body}
        receipts.append(receipt)
        print(name, receipt["status"], receipt["request_id"], repr(body))
finally:
    server.shutdown()
    thread.join()
    server.server_close()
Path("receipts.json").write_text(json.dumps({"client": receipts, "server": server_log},
                                         ensure_ascii=False, indent=2), encoding="utf-8")

儲存後,在檔案所在資料夾執行 python3 error_lab.py。程式會印出四筆結果,關閉本機伺服器後,把紀錄寫入 receipts.json。下列是本次執行的狀態與識別碼摘要:

http   503 mock-http
stream 200 mock-stream
tool   200 mock-tool
text   200 mock-text

打開 receipts.json,用 request_id 把 client 與 server 配成四對。第一筆的 retry_after 是 "2";第二筆 body 含真正的 SSE event: 與 data: 分隔;第三筆在 result 內放了 isError;第四筆則把同樣像錯誤的文字放進 content。這就是可以回查的驗收結果。

第一筆為什麼還讀得到 body?Python 的 HTTPError 文件指出,這個例外也可當成可讀取的回應物件。範例捕捉示範的 HTTP 錯誤,接著保留 body;若只印例外名稱,你會漏掉診斷線索。

這份程式刻意只做單筆、有限長度的 SSE 訊息,沒有逐 token 推送、供應商完成事件、斷線續接或完整工具呼叫流程。它也沒有自動重試。若要接到實際 Agent,請把客戶端改成該服務的正式解析器,再按其事件規格驗收;不要用搜尋 error 字串取代事件解析。重新跑前先另存原本的 receipts.json,因為同名檔會被覆寫。

三、拿到收據後,沿同一條線查:主機、事件、呼叫與時間

真實系統裡,不能把本機的 mock-tool 當成跨服務通用編號。本文四個 ID 是固定測例標籤;正式系統應保留每次嘗試的識別碼,並另外把它們連回同一個任務。服務端是否提供 request ID、使用哪個標頭,依該服務文件決定。

  • 先抄外層:UTC 時間、實際主機、HTTP 狀態、Content-Type、回應中實際存在的 request ID。
  • 再留內容:事件名稱與順序、完成或失敗訊號、工具呼叫 ID 及原始結果封套。
  • 另存回答:助理說了什麼,放在文字欄;不要拿它覆蓋原始回應。
  • 最後對帳:用相同識別碼查自己的服務端日誌;沒有上游日誌時,記下可觀察到的邊界。

例如助理回答「權限被擋」,你的工具日誌卻顯示它根本沒呼叫讀檔;合理的下一步是檢查回答來源與呼叫流程。若工具結果明確拒絕某個路徑,就先查那個路徑的授權。若只有截圖而沒有以上紀錄,結論寫「來源未確認」,比換三次金鑰更有幫助。分享紀錄前,把 Authorization、cookie、個人資料與檔案內容中的秘密值遮掉,保留時間、訊息結構與必要識別碼即可。

想把這條證據線接進自己的執行迴圈,可讀 自建 Agent Harness 教學;若真正困惑的是答案品質忽然下降,Shadow Eval 回歸診斷處理的是另一題:同一任務的新舊回答怎樣公平比較。

四、什麼時候重試,什麼時候先停?

重試像重新按一次送出。如果原本只是讀資料,後果通常比較單純;若原本是寄信或建立訂單,再按一次就可能多做一次。RFC 9110 的冪等與重試規則要求,對非冪等請求,自動重試要先知道操作語意可重複,或能確認原請求未被套用。冪等就是:重複做同一操作,預期效果與做一次相同。

  • 暫時忙碌或限流:先確認操作適合重試;有 Retry-After 就按它等待,再採有上限的退避,避免同時大量重送。429 規範允許帶這個標頭,但不是每個回應都有。
  • 帳戶、權限或輸入問題:先修正授權與參數。不要把不停重送或輪換憑證當成解除拒絕的方法。
  • 串流中斷:先查最後事件、完成條件與產物;不要把已印出的半段回答當成功,也不要直接推定上游完全沒執行。
  • 工具拒絕:按明確的拒絕原因處理。需要額外權限的動作交給有權限的人決定,不能只讓模型改口說已批准。
  • 只有回答文字:先補紀錄;缺少可定位的失敗訊號,就保留未知,暫不啟動自動重試。

本文建議新手先用「首次+最多兩次重試」作練習上限,這是操作預算範例,不是任何服務的保證。若 Retry-After 超過任務可等的時間,就停止這輪並記錄原因;若發現權限拒絕或可能重複執行,也提前停手。Retry-After 可能是秒數,也可能是 HTTP 日期,正式程式要處理兩種。

最後開新 Session、保留同一個無副作用的小任務與設定,只改一個變因再比對。有既有配置的替代模型或供應商時,可作第二組觀察;結果不同,代表行為隨條件變化,還需要回應紀錄才能繼續定位。不要把「換了就好了」當成完整根因分析。

五、常見問題:八個先講答案的判斷

1. HTTP 200,就表示供應商一定沒出錯嗎?

不一定。它可以承載應用層失敗;要繼續看串流事件、工具結果與服務日誌。Hermes FAQ 已提供帳戶拒絕藏在 200 串流內的實例。

2. 回答裡有完整 error JSON,就是原始錯誤嗎?

不一定。先看它是事件資料、工具結果,還是正常回答中的字串。文字形狀相同,來源仍可能不同。

3. 只出現在助理文字裡,就能判它編造嗎?

不能僅憑這點判定。你目前確認的是助理說了這段話;它可能誤判,也可能轉述上游。先找獨立回傳紀錄。

4. 工具回傳 result,表示工具成功了嗎?

不一定。依本文採用的 MCP 工具規範,result 裡的 isError 可以標示執行錯誤;協定錯誤則有另外的 error 封套。

5. 這四個測例重現了 Hermes 的社群事故嗎?

沒有。它們是本機指定的四種假回應,用來驗證分層讀取;沒有執行 Hermes、模型推論或該使用者的原始請求。

6. request ID 可以自己編一個再查供應商嗎?

先查供應商是否接受自訂客戶端識別碼。自己編的任務標籤適合內部對帳,供應商是否能搜尋,要看它有沒有收到並記錄。

7. 逾時了,直接再送一次就好嗎?

先查副作用。客戶端等待逾時,不會自動證明服務端沒執行;寫入、寄信與付款等操作應先對帳,再使用服務支援的去重方式。

8. 我不會寫程式,今天可以先做什麼?

把同一次失敗的時間、介面入口、原始錯誤與助理解釋分成四欄。請能查看日誌的人補上 HTTP、事件與工具封套,再決定下一步。

給新手的三個重點

  • 先認封套:HTTP、串流、工具、文字,各自回答不同問題。
  • 再核對收據:同一請求的狀態、事件與日誌要接得起來;看不到就保留未知。
  • 最後才重試:先確認副作用與完成狀態,再設定等待、上限和停手條件。

接著閱讀

左右滑動查看更多推薦

下一步:今天先做出一份能對帳的錯誤紀錄

先跑一次四個測例,把四對 ID 對起來,再挑一個你遇過的失敗,把相同欄位填進紀錄。看到缺欄位,就知道下一步該補哪一層;看到明確拒絕,就知道應先修哪個條件。AI 錯誤排查=先認封套,再核對收據。想繼續練完整工作流,可到 AI 教學專區選下一篇,或在 AlphaLab 課程安排系統性的實作。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

週日 Weekly 週報+週二、週四 Alpha Signal,每週最多三封。

我們不會 spam,隨時可退訂。已訂閱?管理訂閱偏好(會寄登入連結到你的信箱)

主題篩選與退訂說明

寄送日期以台灣時間為準。主題偏好會篩選每期精選,並非每篇新文章的即時通知;當期符合主題的文章不足時會略過寄送。

未勾選任何主題會收到所有主題的精選,並不會停止寄信。

取消勾選 Alpha Signal 可停止週二、週四短報,仍保留週日週報。若要停止所有電子報,請使用信件底部的「取消訂閱」。