AI 代理的例外處理與恢復:從脆弱到韌性

錯誤處理與恢復的架構圖:錯誤檢測、重試到後備的例外處理鏈與補償性事務的反思機制

當我們要求 AI 幫忙整理會議資料或控制智慧家電時,總有種近乎理所當然的信任——指令下達,他就該完美執行。但現實是:網路會斷線、API 會超時、資料庫會當機、驗證碼會擋路。沒有例外處理機制的 AI,就像一個絕頂聰明但從來沒有出過社會的實習生——做資料分析是一流的,但只要辦公室的印表機一卡紙,他就呆呆站在那裡,完全不知所措。在實驗室裡表現完美的 AI,一放到真實環境就頻繁出錯。這就是例外處理與恢復模式要解決的核心問題-AI 代理要從一戳就破的脆弱程式,進化成能在混亂中自我修復的可靠夥伴。

數位痛覺神經:錯誤檢測

AI 活在虛擬的程式碼世界裡,不像人類能看到印表機卡紙或聽到硬碟怪聲。工程師透過錯誤檢測機制為 AI 裝上數位化的痛覺神經。這些檢測手段可以分為兩個層級。

基礎設施層

處理的是通訊與格式層面的異常,檢測手段相對確定:

語意層

最難處理的是 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——為什麼?

反思的實作流程大致是:

  1. 擷取錯誤發生時的完整上下文(trigger prompt、工具回傳值、錯誤訊息)
  2. 讓 LLM 分析錯誤原因,產生修正後的策略
  3. 將修正寫入當前任務的 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 輸入 外部呼叫 primaryHandlerfallbackHandler
preciseLocation 主要結果 primaryHandler(成功時) responseAgent
generalArea 後備結果 fallbackHandler responseAgent
primaryFailed 路由標記 primaryHandler(失敗時設 true 條件邊緣判斷、fallbackHandler
finalResponse 輸出 responseAgent 外部呼叫者

跟傳統程式的區域變數不同,AgentState 的生命週期跨越整個圖表——primaryHandlerprimaryFailed = 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}`
      : '很抱歉,目前無法取得位置資訊。',
  }
}

這是最後一關。它不看誰成功誰失敗,只從 preciseLocationgeneralArea 中挑一個有值的——優先級由運算子決定 (??)。這是典型的優雅降級:有精確位置就用精確的,沒有就用大概的,再沒有就誠實告知無法提供。

條件路由:錯誤導向的流程控制

.addConditionalEdges('primary', (s) =>
  s.primaryFailed ? 'fallback' : 'responder')

這一段決定整個架構的錯誤處理邏輯。primary 節點執行完後,LangGraph 會自動執行這個條件函式,傳入當前的 AgentState。根據 primaryFailed 的值:

注意這裡的設計選擇:條件路由不是由節點本身決定,而是由圖表的邊緣決定。這讓節點保持純粹——它只回傳狀態變更,不決定流程方向。路由邏輯集中在一處,修改流程時不需要改節點程式碼。

完整流程圖:

              ┌──────────────────────┐
              │       輸入查詢        │
              └──────────┬───────────┘
                         ↓
              ┌──────────────────────┐
              │    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()
  }
}

兩者行為一模一樣,但多代理版本多了兩個關鍵設計:

  1. 狀態可視化primaryFailedpreciseLocationgeneralArea 都是圖表狀態的一部分,不需要看 error stack 就知道流程走過哪些路徑。除錯時直接 console.log(state) 就有一份完整決策履歷。

  2. 可組合性。傳統的巢狀 try-catch 如果要插入新環節(例如在 primaryfallback 之間加一個 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('台北市')
})

斷言兩件事:primaryFailedtrue(確認主要代理真的失敗了),且 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 保持不變),讓後備也失敗。此時 preciseLocationgeneralArea 都是 null,responseAgent?? 運算子的最後一個分支,回傳「很抱歉,目前無法取得位置資訊」。斷言確認系統在全面崩潰邊緣仍能給出有意義的錯誤訊息,而不是直接 throw exception 或回傳空白。

寫測試時把握一個原則:每一條錯誤處理分支都要有一組測試覆蓋——主要路徑測成功、重試路徑測暫時失敗後恢復、後備路徑測主要永久失敗、降級路徑測全面崩潰。

總結

例外處理與恢復模式讓 AI 代理從脆弱、不可靠的實驗室產物,進化成能在真實世界中穩健運作的系統。無論是客服機器人、金融交易系統、智慧家庭還是資料處理管線,結構化的錯誤檢測、分層處理策略與恢復機制,都是代理能否在不可預測環境中生存的關鍵。這不是可選的附加功能——任何要部署到真實世界的 AI 代理,都必須內建這套模式。

幾個關鍵原則值得思考:

下次評估 AI 工具時,不要只看展示影片裡成功完成任務的畫面——真正的強大在於遇到無效資料、網頁驗證碼、資料庫連線超時時,它能優雅處理例外,不讓進度付諸流水。兩個 AI 助手都號稱「可以整理一萬份發票」,第一個在第一份檔案無法讀取時直接 500 錯誤全部停擺,第二個跳過損毀檔案、在報表上標記「第 1 份跳過,其餘 9,999 份已處理完畢」。看一個 AI 要看它失敗時有多穩健。

最後留一個延伸問題:具備反思能力的 AI 可以從失敗中學習,那會不會開始故意觸發小錯誤來測試自己的應變能力?就像人類免疫系統需要接觸微量病毒來產生抗體,這種刻意製造的混亂——類似 Chaos Monkey——是否會成為 AI 自主演化的開端?

參考資料


Agentic Design Pattern Agentic AI Error Handling Reflection AI Architecture Design Pattern