AI 代理的例外處理與恢復:從脆弱到韌性
31 Jul 2026
當我們要求 AI 幫忙整理會議資料或控制智慧家電時,總有種近乎理所當然的信任——指令下達,他就該完美執行。但現實是:網路會斷線、API 會超時、資料庫會當機、驗證碼會擋路。沒有例外處理機制的 AI,就像一個絕頂聰明但從來沒有出過社會的實習生——做資料分析是一流的,但只要辦公室的印表機一卡紙,他就呆呆站在那裡,完全不知所措。在實驗室裡表現完美的 AI,一放到真實環境就頻繁出錯。這就是例外處理與恢復模式要解決的核心問題-AI 代理要從一戳就破的脆弱程式,進化成能在混亂中自我修復的可靠夥伴。
數位痛覺神經:錯誤檢測
AI 活在虛擬的程式碼世界裡,不像人類能看到印表機卡紙或聽到硬碟怪聲。工程師透過錯誤檢測機制為 AI 裝上數位化的痛覺神經。這些檢測手段可以分為兩個層級。
基礎設施層
處理的是通訊與格式層面的異常,檢測手段相對確定:
- HTTP 狀態碼。404(找不到網頁)、500(伺服器錯誤)是最基礎的痛覺訊號,AI 看到這些程式碼就知道出事了。
- 格式驗證。AI 呼叫一個計算工具,預期收到 JSON 格式的資料,結果回傳一串無法解析的 HTML 亂碼。系統透過 schema 驗證立刻察覺不對勁。
- 超時偵測。AI 本身沒有時間流逝的直觀感受,對它來說等一毫秒跟等一分鐘沒差別。開發者必須在外部設定計時器,一旦工具呼叫超過預設時間(例如 10 秒),系統強制中斷並傳遞超時訊號。
語意層
最難處理的是 AI 自己的幻覺——它在胡言亂語時,在自己的神經網路機率分布裡覺得自己是對的。解法不是讓同一個 AI 自我檢查,而是引入獨立的監控系統。監控者通常不是另一個龐大的語言模型,而是確定性的程式邏輯(如正規表示式)或只做單一判斷的輕量模型,只檢查輸出是否包含特定關鍵字或格式。
日誌記錄:跨所有階段的必要步驟
所有錯誤處理策略的共同前提是日誌記錄。就像飛機的黑盒子,系統必須記錄觸發錯誤那一瞬間的完整狀態——觸發 prompt、工具回傳值、API 回應標頭、錯誤訊息——確保開發者事後有足夠線索來除錯。沒有日誌,後續的診斷與反思都缺乏基礎。
錯誤處理的應對策略
偵測到錯誤後,AI 不能只把錯誤日誌丟在螢幕上就停擺。它必須啟動一系列逐漸升級的應對策略。
第一線:重試
針對暫時性的網路波動或伺服器偶發延遲,系統等待幾秒、稍微微調連線參數後再次傳送請求。但不是所有錯誤都能無腦重試——自動交易機器人遇到資金不足的錯誤,狂按一萬次傳送只會讓帳號被封鎖。系統必須判斷什麼錯誤可以重試、什麼不行。
第二線:後備
遇到絕對不可能成功的錯誤時,放棄原本的路徑,改用替代方法。例如主要資料庫連不上,就改連備用資料庫。
第三線:優雅降級
當連後備方案都行不通時,系統試圖維持部分功能,而不是全面崩潰。就像米其林餐廳廚房大停電,主廚無法煎牛排,但利用冷藏食材端出一份極品冷盤——我們雖然沒吃到牛排,但還是享用到一頓高品質的餐點。
在資料處理場景中尤其關鍵:AI 正在處理一萬份檔案,第五千份損壞無法讀取。沒有優雅降級的話,系統會直接崩潰,前面四千九百九十九份處理好的資料也跟著遺失。有優雅降級的話,AI 跳過損壞的檔案、標記在錯誤日誌中,繼續處理剩下的檔案。
重試 → 後備 → 優雅降級 → 通知人類
需要注意的是,優雅降級並非萬靈丹。在某些場景中,降級帶來的「部分結果」反而比直接停擺更危險——例如金融交易的帳務不平、醫療診斷的資料缺漏。實務上應由開發者根據業務領域配置每種錯誤的降級策略,而非硬性套用同一條規則。
如果連部分價值都生不出來,最後一步就是通知人類操作員接手。承認自己處理不來,也是一種負責的表現。
UI/UX 考量:錯誤訊息怎麼說
錯誤處理不只在程式碼層面,使用者感受到的才是真實的錯誤處理品質。同樣的底層邏輯,UI 呈現方式不同,使用者體驗天差地遠。
錯誤訊息的三層次
不是所有錯誤都需要讓使用者看到。系統應根據錯誤類型分層呈現:
| 層次 | 使用者看到什麼 | 適用場景 |
|---|---|---|
| 透明 | 什麼都不顯示 | 後備 API 自動接手、短暫重試成功,使用者不需要知道中間發生過問題 |
| 資訊 | 簡短提示,不阻塞操作 | 已降級為粗略資料時,在介面角落標記「部分資料為估計值」 |
| 行動 | 錯誤說明 + 下一步可做的事 | 系統無法處理時,明確告知使用者發生什麼、可以怎麼補救 |
以地理編碼的情境為例,三種層次對應的實際畫面:
透明層——後備代理成功接手,使用者看到的畫面跟正常情況完全一樣:
┌─────────────────────────────────────┐
│ 地點查詢 │
│ │
│ [台北101在哪裡 ] [搜尋] │
│ │
│ 結果:台北市信義區信義路五段7號 │
│ │
│ 地圖顯示中... │
└─────────────────────────────────────┘
使用者不知道主要 API 超時過、不知道後備代理接手了。這是最好的錯誤處理——使用者完全沒感覺到出過問題。
資訊層——主要 API 失敗且後備只能給粗略資料,使用者在角落看到標記:
┌─────────────────────────────────────┐
│ 地點查詢 ⚠️ 部分 │
│ │
│ [台北101在哪裡 ] [搜尋] │
│ │
│ 結果:台北市信義區 │
│ ───────── │
│ ⓘ 此為估計位置,精確度可能 │
│ 有所誤差 │
│ │
│ 地圖顯示中... │
└─────────────────────────────────────┘
右上角的「部分」標籤和資料下方的提示圖示讓使用者知道資訊有限,但不阻擋操作流程。
行動層——所有策略都失敗,使用者需要知道下一步:
┌─────────────────────────────────────┐
│ ❌ 目前無法查詢地點 │
│ │
│ 無法連線到地圖服務,已記錄此問題。 │
│ │
│ [重試] [回報問題] [回到首頁] │
│ │
│ 問題編號:ERR-20260731-001 │
└─────────────────────────────────────┘
不是「發生錯誤」,而是明確的三件事:發生什麼、系統做了什麼、使用者可以點什麼。
降級狀態的 UI 模式
當系統進入優雅降級模式時,UI 應該讓使用者感受到「雖然少了什麼,但還在運作」。以一份 AI 生成的財務報表為例:
┌─────────────────────────────────────┐
│ 季度財務報表 [⚙️ 離線模式] │
├─────────────┬───────────────────────┤
│ │ 營收 $12,400,000 │
│ [下載 PDF] │ 成本 $ 8,200,000 │
│ (離線不支 │ 毛利 $ 4,200,000 │
│ 援下載) │ 毛利率 33.9% ⓘ │
│ │ (使用估計值) │
│ │ │
│ 狀態: │ AI 產出時間:10:32 │
│ ● 連線正常 │ 資料來源:快取 │
│ ○ 資料庫 ? │ │
│ ○ PDF ✗ │ │
└─────────────┴───────────────────────┘
左側的狀態面板用顏色標記每項功能的健康狀態(正常 / 不確定 / 失敗),「下載 PDF」按鈕灰化並提示原因。右側的毛利率旁標記「估計值」,使用者知道這筆資料需要人工確認。
批次處理場景的即時儀表板:
┌─────────────────────────────────────┐
│ 發票整理進度 │
│ │
│ ████████████████░░░░░░ 82% │
│ 8,217 / 10,000 已完成 │
│ │
│ ✅ 成功 8,217 │
│ ⚠️ 跳過 3(詳見錯誤日誌) │
│ ❌ 失敗 0 │
│ │
│ ┌─ 錯誤日誌 ───────────────────┐ │
│ │ #1 編碼錯誤 - 發票_0051.pdf │ │
│ │ #2 格式不符 - 發票_0382.pdf │ │
│ │ #3 檔案損毀 - 發票_0927.pdf │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
使用者不是面對一個轉圈圈的 spinner,而是清楚知道進度、成功筆數、跳過的原因。
人類接手時的 UI 設計
當所有策略都失敗、需要通知人類操作員時,操作員看到的儀表板應該讓他「一秒進入狀況」:
┌─────────────────────────────────────┐
│ 🔴 [高優先] 地理編碼服務中斷 │
│ 2026-07-31 10:47:23 │
├─────────────────────────────────────┤
│ 錯誤摘要 │
│ ┌─────────────────────────────────┐│
│ │ 節點: primaryHandler ││
│ │ 錯誤: Geo API timeout ││
│ │ 嘗試: 重試 3 次 → 失敗 ││
│ │ 後備 API → 也失敗 ││
│ │ 輸入: "台北101在哪裡" ││
│ │ 影響: 使用者無法查詢地點 ││
│ └─────────────────────────────────┘│
│ │
│ 建議行動 │
│ ┌─────────────────────────────────┐│
│ │ 可能原因:地理 API 憑證過期 ││
│ │ 建議修復:重新產生 API key ││
│ │ 參考文件:docs/geo-api.md ││
│ └─────────────────────────────────┘│
│ │
│ [標記為已處理] [升級給主管] │
└─────────────────────────────────────┘
AI 不只是叫人來救,而是附上錯誤摘要、嘗試過的策略、推測原因與建議修復方向。操作員不需要重頭 trace 錯誤脈絡。但要注意,這是給內部操作員看的。終端使用者只需要一句話:
┌─────────────────────────────────────┐
│ 已通知技術人員,預計 30 分鐘內修復 │
│ │
│ 🔄 修復完成後將自動重試 │
└─────────────────────────────────────┘
給終端使用者太多的技術細節只會造成困惑,一句「已通知 + 預計時間 + 自動重試」就夠了。
恢復機制與反思
當 AI 決定改變策略時,面臨的第一個挑戰是狀態回滾。AI 很多時候跟外部世界互動——發送出去的 Email 不能用 Ctrl+Z 復原。所謂的狀態回滾,實際上是執行補償性事務。例如發現剛建立的日曆行程內容全錯,系統必須主動產生一個刪除該行程的新指令,把狀態清理乾淨。但補償性事務本身也可能失敗——刪除 API 超時、權限不足、行程已被其他系統鎖定。因此開發者需要為補償性事務也配上重試邏輯與通知機制,否則乾淨的恢復就不成立。
清理完畢後,任務還沒完成。更具革命性的能力是反思。傳統程式遇到錯誤就傻傻重跑一樣的迴圈。具備反思能力的 AI 會停下來分析:剛才用了工具 A、輸入了參數 B、結果得到錯誤 C——為什麼?
反思的實作流程大致是:
- 擷取錯誤發生時的完整上下文(trigger prompt、工具回傳值、錯誤訊息)
- 讓 LLM 分析錯誤原因,產生修正後的策略
- 將修正寫入當前任務的 prompt,重新執行
這些修正可以分為兩個 scope:任務級的調整只在當前任務生效,下次開始乾淨重來;永久性學習則將修正存入一個已知錯誤與解決方案的查找表,未來遇到類似錯誤時可直接套用。
但反思也有風險。自我修改 prompt 可能偏離原始目標,或陷入「失敗 → 反思 → 修改 → 再失敗 → 再反思」的無限迴圈。實務上應為反思設定次數上限(例如最多嘗試 3 次),並在超過上限時直接升級給人類處理。反思模式的詳細實作可參考這裡-利用 Agentic Design Pattern 重構 CareerWise — 從單一流程到路由、提示鏈與反思模式。
多代理後備架構:接力賽模式
在這裡用一個地理編碼(Geocoding)的情境來說明分層後備架構——主要 API 查不到精確位置時,降級到粗略區域查詢,最後統一整理輸出。這個模式的概念類似 ADK Graph Workflow 的條件路由(Conditional Routing)——節點回傳 Event(route=...) 來決定下一棒該誰接手。以下用 LangChain.js + LangGraph 實作,概念相同但工具鏈不同。核心是三個代理節點透過共享狀態串聯:主要代理(primary) 負責高精度地理編碼,後備代理(fallback) 在主要代理失敗時提供降級方案,回應代理(responder) 統一處理輸出格式。三層之間不直接通訊,而是透過一個中央狀態物件交換資訊——每個節點只從狀態讀取所需資料,只回寫自己負責的欄位。
狀態向量 (State)
┌──────────────────────────────────────┐
│ query, preciseLocation, generalArea │
│ primaryFailed, finalResponse │
└──────────────────────────────────────┘
primary fallback response
handler handler agent
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 成功 │ │ 讀取: │ │ 讀取: │
│ 跳過後備 │ │ primary │ │ location│
│ │ │ Failed │ │ result │
│ 失敗 │ │ 寫入: │ │ 寫入: │
│ 啟動後備 │ │ general │ │ final │
│ │ │ Area │ │ Response│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────────────────┴───────────────────┘
狀態
這段程式碼的核心設計是共享狀態加上條件路由,使用 LangGraph 的 StateGraph 搭配條件邊緣,在主要代理成功時直接跳過後備環節,減少不必要的 LLM 呼叫。另一種做法是使用 ADK 的 SequentialAgent 固定順序執行、後備代理讀取 state 決定是否動作,兩者精神一致但實作取捨不同——這裡追求效率(條件跳過),後者追求確定性(順序執行)。
import { StateGraph } from '@langchain/langgraph'
type AgentState = {
query: string
preciseLocation: string | null
generalArea: string | null
primaryFailed: boolean
finalResponse: string
}
// 模擬外部地理工具
const getPreciseLocation = async (address: string): Promise<string> => {
// 可能拋出錯誤(API 超時、網路斷線等)
const res = await fetch(`https://geo.example.com/precise?q=${address}`)
if (!res.ok) throw new Error(`Geo API error: ${res.status}`)
return res.json()
}
const getGeneralAreaInfo = async (city: string): Promise<string> => {
const res = await fetch(`https://geo.example.com/area?city=${city}`)
if (!res.ok) throw new Error(`Area API error: ${res.status}`)
return res.json()
}
const extractCity = (query: string): string => {
// 從查詢語句提取城市名稱的邏輯
const cities = ['台北', '台中', '高雄', '紐約', '東京']
return cities.find(c => query.includes(c)) ?? '未知區域'
}
// 主要處理者:嘗試高精度地理編碼
async function primaryHandler(state: AgentState) {
try {
const result = await getPreciseLocation(state.query)
return { preciseLocation: result, primaryFailed: false }
} catch {
return { preciseLocation: null, primaryFailed: true }
}
}
// 後備處理者:主要失敗時降級處理
async function fallbackHandler(state: AgentState) {
if (!state.primaryFailed) return {}
const city = extractCity(state.query)
const area = await getGeneralAreaInfo(city)
return { generalArea: area }
}
// 回應代理:決定最終輸出
async function responseAgent(state: AgentState) {
const result = state.preciseLocation ?? state.generalArea
return {
finalResponse: result
? `位置資訊:${result}`
: '很抱歉,目前無法取得位置資訊。',
}
}
// 組合為狀態圖
const locationAgent = new StateGraph(AgentState)
.addNode('primary', primaryHandler)
.addNode('fallback', fallbackHandler)
.addNode('responder', responseAgent)
.addEdge('__start__', 'primary')
.addConditionalEdges('primary', (s) =>
s.primaryFailed ? 'fallback' : 'responder')
.addEdge('fallback', 'responder')
.addEdge('responder', '__end__')
.compile()
AgentState:代理之間的共同記憶體
AgentState 型別已在前面定義過,這裡直接用表格說明每個欄位的設計邏輯:
| 欄位 | 角色 | 誰寫入 | 誰讀取 |
|---|---|---|---|
query |
輸入 | 外部呼叫 | primaryHandler、fallbackHandler |
preciseLocation |
主要結果 | primaryHandler(成功時) |
responseAgent |
generalArea |
後備結果 | fallbackHandler |
responseAgent |
primaryFailed |
路由標記 | primaryHandler(失敗時設 true) |
條件邊緣判斷、fallbackHandler |
finalResponse |
輸出 | responseAgent |
外部呼叫者 |
跟傳統程式的區域變數不同,AgentState 的生命週期跨越整個圖表——primaryHandler 設 primaryFailed = true,下一個節點 fallbackHandler 讀得到。這種跨節點的共享狀態是 LangGraph 模式的核心。
三層代理的責任邊界
primaryHandler:前線部隊
async function primaryHandler(state: AgentState) {
try {
const result = await getPreciseLocation(state.query)
return { preciseLocation: result, primaryFailed: false }
} catch {
return { preciseLocation: null, primaryFailed: true }
}
}
這是第一線處理者。它的工作是呼叫高精度 API——如果成功,整個流程直接快轉到 responder;如果失敗,標記 primaryFailed = true 並繼續執行。注意它不做重試,重試應該由上一層(呼叫端或計時器)處理,這層的責任只在「盡力一次,失敗就交棒」。
這種「前一棒決定下一棒」的流程控制,概念類似 ADK Graph Workflow 的條件路由(Conditional Routing)——節點回傳 Event(route=...),graph 根據 route 值分派到不同的後繼節點。這裡升級的不是人類,而是後備代理。
fallbackHandler:後備支援
async function fallbackHandler(state: AgentState) {
if (!state.primaryFailed) return {}
const city = extractCity(state.query)
const area = await getGeneralAreaInfo(city)
return { generalArea: area }
}
這個節點的門檻條件是 state.primaryFailed。如果主要代理成功,它直接回傳空物件,不浪費任何運算資源。只有在主要失敗時才啟動。它的處理策略是降維打擊——高精度 API 失敗了,改用低精度但更穩定的方法:從查詢字串中提取城市名稱,再查該城市的一般區域資訊。就像餐廳停電時無法煎牛排(高精度),但還能做冷盤(低精度但穩定)。
primaryFailed 的存在讓 fallbackHandler 可以是純函式——輸入相同、輸出永遠相同,沒有副作用,易於測試。
responseAgent:統一出口
async function responseAgent(state: AgentState) {
const result = state.preciseLocation ?? state.generalArea
return {
finalResponse: result
? `位置資訊:${result}`
: '很抱歉,目前無法取得位置資訊。',
}
}
這是最後一關。它不看誰成功誰失敗,只從 preciseLocation 和 generalArea 中挑一個有值的——優先級由運算子決定 (??)。這是典型的優雅降級:有精確位置就用精確的,沒有就用大概的,再沒有就誠實告知無法提供。
條件路由:錯誤導向的流程控制
.addConditionalEdges('primary', (s) =>
s.primaryFailed ? 'fallback' : 'responder')
這一段決定整個架構的錯誤處理邏輯。primary 節點執行完後,LangGraph 會自動執行這個條件函式,傳入當前的 AgentState。根據 primaryFailed 的值:
true→ 走fallback(降級處理)false→ 跳過fallback,直接到responder(正常結束)
注意這裡的設計選擇:條件路由不是由節點本身決定,而是由圖表的邊緣決定。這讓節點保持純粹——它只回傳狀態變更,不決定流程方向。路由邏輯集中在一處,修改流程時不需要改節點程式碼。
完整流程圖:
┌──────────────────────┐
│ 輸入查詢 │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ primaryHandler │
│ (高精度地理編碼) │
└──────────┬───────────┘
│
┌─────────┴─────────┐
↓ ↓
┌──────────────┐ ┌──────────────┐
│ primaryFailed │ │ 成功 │
│ = true │ │ │
└──────┬───────┘ └──────┬───────┘
↓ ↓
┌──────────────┐ ┌──────────────┐
│ fallback │ │ │
│ Handler │ │ (跳過) │
│ (低精度降級) │ │ │
└──────┬───────┘ └──────┬───────┘
└────────┬─────────┘
↓
┌──────────────────┐
│ responseAgent │
│ (統一整理輸出) │
└────────┬─────────┘
↓
┌──────────────────┐
│ 結束 │
└──────────────────┘
與傳統 try-catch 的差異
傳統寫法長這樣:
try {
const result = await getPreciseLocation(query)
return formatSuccess(result)
} catch {
try {
const city = extractCity(query)
const area = await getGeneralAreaInfo(city)
return formatFallback(area)
} catch {
return formatError()
}
}
兩者行為一模一樣,但多代理版本多了兩個關鍵設計:
-
狀態可視化。
primaryFailed、preciseLocation、generalArea都是圖表狀態的一部分,不需要看 error stack 就知道流程走過哪些路徑。除錯時直接console.log(state)就有一份完整決策履歷。 -
可組合性。傳統的巢狀 try-catch 如果要插入新環節(例如在
primary與fallback之間加一個cache層),需要改寫整段邏輯。多代理版本只需新增節點與邊緣:
.addNode('cache', cacheHandler)
.addEdge('primary', 'cache')
.addConditionalEdges('cache', (s) =>
s.cacheHit ? 'responder' : 'fallback')
這種設計可稱為分層後備模式(Layered Fallback Pattern),每一層處理一種精確度等級的錯誤,層層過濾直到找到可用的結果。ADK 的 Graph Workflow 中也有類似的條件路由機制可參考。
與 Human-in-the-Loop 版 Geolocation 的差異
在另一篇文章-AI 為什麼需要人類救場:Human-in-the-Loop 架構探討中我們用同樣的地理編碼情境實作了信心閥值版本(HITL)。兩者核心差異:
| 對比維度 | 錯誤處理版本(本文) | HITL 版本 |
|---|---|---|
| 觸發條件 | 例外拋出(try-catch) | 信心分數低於閥值 |
| 流程方向 | 主要失敗 → 後備 | 輕量分析 → 深度分析 → 人類 |
| 共享狀態 | primaryFailed boolean |
escalatedTo string |
| 終點 | 後備結果或錯誤訊息 | 人類決策 |
同一個地理編碼情境,可以針對不同需求衍伸出不同實作——錯誤處理版注重系統穩定,HITL 版注重決策品質。
狀態向量 + 錯誤資訊 → 決定是否重試或切換策略 → 執行補償性事務 → 反思與修正
測試建議:如何驗證錯誤處理
錯誤處理最怕的是「表面看起來沒事,但實際沒被觸發」。測試的關鍵在於模擬外部失敗,確認代理在異常情況下仍能走完正確的分支:
import { describe, it, expect, vi } from 'vitest'
// 模擬主要 API 永遠失敗
vi.mock('./tools', () => ({
getPreciseLocation: vi.fn()
.mockRejectedValue(new Error('Geo API timeout')),
getGeneralAreaInfo: vi.fn()
.mockResolvedValue('台北市大安區'),
}))
it('主要 API 超時時應自動啟動後備代理', async () => {
const result = await locationAgent.invoke({
query: '台北101在哪裡',
})
expect(result.primaryFailed).toBe(true)
expect(result.finalResponse).toContain('台北市')
})
it('所有後備都失敗時應優雅降級', async () => {
vi.mocked(getGeneralAreaInfo)
.mockRejectedValue(new Error('Area API timeout'))
const result = await locationAgent.invoke({
query: '台北101在哪裡',
})
expect(result.finalResponse).toContain('無法取得位置資訊')
})
這兩個測試案例分別驗證兩條關鍵的錯誤處理分支:
測試一:主要 API 失敗時自動啟動後備
vi.mock('./tools', () => ({
getPreciseLocation: vi.fn()
.mockRejectedValue(new Error('Geo API timeout')),
getGeneralAreaInfo: vi.fn()
.mockResolvedValue('台北市大安區'),
}))
vi.mock 在檔案層級將 ./tools 模組整個替換掉。getPreciseLocation 永遠回傳 rejected Promise(模擬 API 超時),getGeneralAreaInfo 永遠回傳 resolved 值(後備 API 正常)。這確保測試進入主要代理失敗的路徑,且後備代理有資料可用。
it('主要 API 超時時應自動啟動後備代理', async () => {
const result = await locationAgent.invoke({
query: '台北101在哪裡',
})
expect(result.primaryFailed).toBe(true)
expect(result.finalResponse).toContain('台北市')
})
斷言兩件事:primaryFailed 為 true(確認主要代理真的失敗了),且 finalResponse 包含「台北市」(確認後備代理成功接手、提供降級結果)。兩個斷言缺一不可——只檢查失敗不檢查降級結果,不能確認後備代理真正被觸發。
測試二:所有後備都失敗時優雅降級
it('所有後備都失敗時應優雅降級', async () => {
vi.mocked(getGeneralAreaInfo)
.mockRejectedValue(new Error('Area API timeout'))
const result = await locationAgent.invoke({
query: '台北101在哪裡',
})
expect(result.finalResponse).toContain('無法取得位置資訊')
})
這裡用 vi.mocked(getGeneralAreaInfo) 只覆寫 getGeneralAreaInfo 的行為(其他 mock 保持不變),讓後備也失敗。此時 preciseLocation 和 generalArea 都是 null,responseAgent 走 ?? 運算子的最後一個分支,回傳「很抱歉,目前無法取得位置資訊」。斷言確認系統在全面崩潰邊緣仍能給出有意義的錯誤訊息,而不是直接 throw exception 或回傳空白。
寫測試時把握一個原則:每一條錯誤處理分支都要有一組測試覆蓋——主要路徑測成功、重試路徑測暫時失敗後恢復、後備路徑測主要永久失敗、降級路徑測全面崩潰。
總結
例外處理與恢復模式讓 AI 代理從脆弱、不可靠的實驗室產物,進化成能在真實世界中穩健運作的系統。無論是客服機器人、金融交易系統、智慧家庭還是資料處理管線,結構化的錯誤檢測、分層處理策略與恢復機制,都是代理能否在不可預測環境中生存的關鍵。這不是可選的附加功能——任何要部署到真實世界的 AI 代理,都必須內建這套模式。
幾個關鍵原則值得思考:
- 錯誤檢測是多層次的,從基礎設施層的 HTTP 狀態碼到語意層的獨立監控系統,每一層負責不同類型的異常。
- 處理策略循序漸進,日誌記錄為前提,依序嘗試重試、後備、優雅降級,最後通知人類。
- 恢復不只是復原狀態,補償性事務清理副作用,反思機制分析根因。
- 分層後備架構讓錯誤處理可組合,每層只負責自己的範圍,超出就交棒。每一條錯誤分支都要有測試覆蓋,模擬外部失敗,確認代理在異常情況下仍能走完正確路徑。
下次評估 AI 工具時,不要只看展示影片裡成功完成任務的畫面——真正的強大在於遇到無效資料、網頁驗證碼、資料庫連線超時時,它能優雅處理例外,不讓進度付諸流水。兩個 AI 助手都號稱「可以整理一萬份發票」,第一個在第一份檔案無法讀取時直接 500 錯誤全部停擺,第二個跳過損毀檔案、在報表上標記「第 1 份跳過,其餘 9,999 份已處理完畢」。看一個 AI 要看它失敗時有多穩健。
最後留一個延伸問題:具備反思能力的 AI 可以從失敗中學習,那會不會開始故意觸發小錯誤來測試自己的應變能力?就像人類免疫系統需要接觸微量病毒來產生抗體,這種刻意製造的混亂——類似 Chaos Monkey——是否會成為 AI 自主演化的開端?