Agent 錯誤恢復與自我修復實作課:先分類、再重試、最後降級
在示範裡跑得很好的 Agent,放到生產環境常常在第三十步倒下。這不是模型不夠聰明,而是機率問題:每一步成功率 95%,三十步全程無錯的機率只有約 21%。換句話說,長任務幾乎一定會出錯——差別只在於,出錯時你的 Agent 是有計畫地恢復,還是原地崩潰、重頭來過,甚至假裝沒事繼續往下走。
這就是今天的主題:錯誤恢復與自我修復。重點不在於「讓 Agent 永不犯錯」,而在於給每一次失敗一個預先寫好的劇本:先分類,再重試,真的不行就降級,降級也要留下可稽核的痕跡。
一、第一步永遠是分類,不是重試
最常見的反模式,是對所有錯誤一律重試三次。重試 401 只會浪費額度,重試參數錯誤只會得到同一個錯誤三次,重試已經扣款成功的請求則可能重複扣款。正確的第一步,是把錯誤分成三類:
- 暫時性錯誤:值得重試。超時、429 限流、503 服務暫時不可用、短暫的網路抖動。
- 永久性錯誤:重試無用,改走修正或回退。401 認證失效、404 資源不存在、參數驗證失敗、結構化輸出驗證失敗。
- 關鍵性錯誤:不能自己決定,必須停下來保存狀態並交給人類。預算超支、不可逆的破壞性操作、連續多次驗證失敗、資料完整性受損。
這個分類必須寫成程式碼,而不是寫在提示詞裡讓模型憑感覺判斷。模型在壓力下很容易把「再試一次」當成預設答案,而分類器不會。分類的依據是錯誤碼、錯誤型別與重試次數,不是錯誤訊息的字面意思——字面意思會變,錯誤碼的語意相對穩定。
二、五層防線,由內而外疊上去
如圖一,自我修復不是一個技巧,而是五層防線的組合,順序有講究。
第一層是重試加退避與抖動。指數退避讓等待時間逐次拉長,避免對已經吃力的服務落井下石;抖動則把重試時間隨機打散,避免幾十個 Agent 同一秒一起回頭,造成第二次尖峰。實務上限通常是三次,超過就該換策略,而不是換更大的等待時間。
第二層是超時與預算。每一層呼叫都要有自己的超時:模型呼叫、工具呼叫、整體任務各設一條。沒有超時的重試等於無限迴圈的另一種寫法。預算也是同一類防線:token 預算、呼叫次數預算、金額預算,任一條觸發就停下來,不要等到帳單來了才發現。
第三層是熔斷器。連續失敗達到門檻後,熔斷器直接跳開,後續請求立刻失敗,不再打擾已經倒下的服務。經過一段冷卻時間後放行一次試探請求(半開狀態),成功才恢復,失敗就繼續跳開。這一層保護的不只是你的 Agent,更是下遊服務的恢復機會。
第四層是回退鏈。主模型失敗,換備用模型;即時資料失敗,換快取資料;完整流程失敗,交出部分結果並標註缺了什麼。回退的關鍵原則是「換一條獨立的路徑」:如果備用模型走同一個掛掉的供應商、快取資料來自同一個壞掉的上遊,那不叫回退,叫換個姿勢失敗。
第五層是冪等性與檢查點。所有可能重試的寫入都要帶冪等鍵,重試時用同一個鍵與同一份請求內容,服務端認得這是同一筆操作,不會重複執行。而檢查點讓長任務可以從上一個已確認的步驟恢復,而不是從頭重跑。LangGraph 的檢查點與 Cloudflare 的持久化工作流程走的都是這條路:狀態先落地,程序可以倒,任務不必倒。
三、最小可跑骨架
下面是一個把分類、重試、熔斷與回退串起來的骨架。重點看結構,不要看行數:錯誤先分類,暫時性錯誤才進重試迴圈,熔斷器擋在重試外面,回退鏈放在最外層。
import random, time
class CircuitOpen(Exception):
pass
def classify(error) -> str:
"""暫時性、永久性、關鍵性,三選一,寫死不憑感覺。"""
status = getattr(error, "status", 0)
if status in (429, 503, 504) or isinstance(error, TimeoutError):
return "transient"
if status in (401, 404, 422):
return "permanent"
return "critical"
def call_with_recovery(call, fallback, breaker, key: str):
if breaker.is_open():
return fallback(key) # 熔斷已開:直接走回退,不打擾下遊
attempt = 0
while True:
try:
result = call(idempotency_key=key) # 同一鍵重試,服務端不重複執行
breaker.record_success()
return result
except Exception as error:
if classify(error) != "transient" or attempt >= 3:
breaker.record_failure()
return fallback(key)
delay = min(2 ** attempt, 10) + random.uniform(0, 1) # 指數退避+抖動
time.sleep(delay)
attempt += 1
這個骨架刻意留了一個缺口:它沒有處理「結果回來了,但結果是錯的」這種失敗。那是下一節的問題,也是生產環境最難的一關。
四、自我修復不等於自我欺騙
如圖二,恢復迴路裡最危險的一步,是 Agent 在沒有驗證的情況下宣布成功。Berkeley 的 Agents' Last Exam 研究指出,最常見的失敗模式正是這一類:Agent 說「都完成了、檢查都通過」,實際上檔案缺了、數字錯了、約束被違反了。自我修復如果沒有驗證器把關,很容易變成自我安慰:換一條路徑、拿到一個看起來合理的答案,就當作修好了。
所以每一層恢復都要配一個驗證器,而且驗證器必須獨立於執行者。寫入後讀回來比對、輸出跑一次結構驗證、關鍵數字用第二個來源交叉檢查——這些都是便宜的驗證,遠比事後追帳便宜。驗證失敗時,正確的動作不是再換一個模型試一次,而是把「哪一步、哪個檢查、哪個數值」寫進日誌,連同部分結果一起交給人類。
降級也一樣需要誠實標註。回傳快取資料時要註明資料的時間戳記與來源;交出部分結果時要列出未完成的部分;換備用模型時要記錄降級的層級。一個標註清楚的降級結果是可用的,一個假裝完整的降級結果是事故。
五、上線前先量這四個數字
恢復機制上線後,真正要看的不是「有沒有重試」,而是四個指標:錯誤分類的分布(暫時性是不是異常飆高)、重試成功率(重試到底救回了多少任務,還是只是在燒錢)、熔斷器跳開的次數與持續時間、回退鏈走到的深度(是不是常常一路掉到底)。這四個數字會告訴你,系統的弱點在上遊服務、模型選擇,還是任務設計本身。
一個實用的起手清單是:分類器先寫死、重試上限三次、每個呼叫設超時、寫入一律帶冪等鍵、長任務每一步寫檢查點、降級結果一律標註、關鍵性錯誤一律留給人類。七項都做到,Agent 才算具備了在生產環境跌倒後站起來的能力。
結論:把失敗寫進設計,而不是寫進事後檢討
示範階段的 Agent 可以假設世界是友善的,生產階段的 Agent 必須假設世界會在第三十步出錯。先分類決定該不該重試,用退避與抖動保護下遊,用熔斷器防止連鎖崩潰,用回退鏈保住部分價值,用冪等性與檢查點讓重試安全,最後用獨立驗證器擋住自我欺騙。
一句話收尾:會自我修復的 Agent 不是比較聰明,而是比較誠實——它知道自己哪裡壞了、試了什麼、最後交出了什麼樣的結果。誠實的失敗,才是可以修復的失敗。
來源:
- Taskade〈AI Agent Error Handling & Self-Healing Patterns〉https://www.taskade.com/blog/ai-agent-error-recovery
- LM-Kit〈Add Resilience to Agents with Retry, Circuit Breaker, and Fallback Policies〉https://docs.lm-kit.com/lm-kit-net/guides/how-to/add-resilience-to-agents.html
- Cloudflare〈Build a Durable AI Agent〉https://developers.Cloudflare.com/workflows/get-started/durable-agents/
- Berkeley RDI〈Agents' Last Exam〉https://rdi.berkeley.edu/blog/agents-last-exam/
- Capable but Unreliable: Canonical Path Deviation as a Causal Mechanism of Agent Failure in Long-Horizon Tasks https://arxiv.org/html/2602.19008v1/
- AI Agent Reliability in 2026: Compounding Error and Overrides https://withtai.com/blog/ai-agent-reliability-in-2026-compounding-error-and-overrides.php
沒有留言:
張貼留言