安安~我是ChiYu~
昨天才把成功 payload 收乾淨,今天我就準備把活動 API 暫時弄壞。
原本的計畫很單純:封鎖活動詳情 API,讓 get_event_details 收到暫時失敗,再看 Agent 能不能
回答「可以稍後重試」。結果第一次跑起來,Tool 選對了,參數卻變成:
{
"event_id": null
}
請求根本還沒碰到被封鎖的 API,就先被 input validation 攔下來。
這題因此多了一個很重要的前提:要測「服務失敗後怎麼復原」,得先證明 Agent 真的進入預期
Tool、送出合法 input,而且 DevTools 製造的故障確實只影響那支 API。否則最後看到一張紅色
畫面,卻不知道紅在哪一層。
今天的實驗最初建立在 v3-day-15:
git switch --detach v3-day-15
npm ci
npm run dev
這個歷史版本的詳情 Tool 已經會優先使用目前 route 綁定的活動 ID,但 schema 仍保留一個
選填 event_id。因此 {} 可以正常執行;模型若送入另一個值,則會被視為目前頁面目標不一致。
後面 revision 0000008 才把最終契約拆成兩個明確 profile:
/events
get_event_details({ "event_id": "搜尋結果提供的 ID" })
/events/:eventId
get_event_details({})
今天會先保留選填欄位造成的 null 失敗,再看收緊 schema 後,Agent 能不能真正走到TEMPORARY_FAILURE。
在製造故障以前,我先確認 Agent 知道什麼時候不需要操作網站。這兩題不是今天的主角,
因此只保留判定與原始紀錄:
| Case | 預期 | 實際結果 |
|---|---|---|
| NO-01 | 解釋 progressive enhancement,不操作網站 | 0 次 function call,通過 |
| NO-02 | 回答系列篇數,不搜尋活動 | 初次猜 catalog 外 Tool;revision 0000008 重測時維持 0 次呼叫 |
NO-02 初次最後雖然猜到「30」,來源卻不是頁面。答對不能替過程洗白;後續重測通過的也只是
no-tool 邊界,不代表回答內容就完全理想。
這兩題後面會在失敗診斷文章裡重新分類;今天先確認一件事:頁面有 Tool,不代表每句話都該
挑一支來用。
如果 Tool 失敗後只丟一段文字,Agent 很難判斷應該修改 input、換一個 ID、稍後重試,還是
立刻停止。這個專案因此把分類、原因與下一步分開:
{
"ok": false,
"code": "TEMPORARY_FAILURE",
"reason": "EVENT_SERVICE_UNAVAILABLE",
"message": "活動服務暫時無法使用。",
"nextAction": "稍後重試;若持續失敗,改由可見介面確認服務狀態。",
"retryable": true,
"uiUpdated": false,
"stateVersion": 1
}

圖 1:code 與 reason 說明發生什麼事;retryable 與 nextAction 則限制下一步。
錯誤大致分成四種處理方向:
| 類型 | 代表 Code | Agent 應該怎麼做 |
|---|---|---|
| input 不合法 | INVALID_INPUT |
修正資料,不要原樣重送 |
| 對象或狀態已變 | NOT_FOUND、CONFLICT |
重新搜尋或讀取最新狀態 |
| 暫時性系統問題 | TEMPORARY_FAILURE |
保留原條件,有上限地稍後重試 |
| session、權限或 task 已失效 | UNAUTHORIZED、FORBIDDEN、ABORTED |
停止,要求重新建立合法情境 |
目前只有 TEMPORARY_FAILURE 會得到 retryable: true。輸入錯誤重送一百次,不會因為努力
而突然變合法;沒有權限時換一個 ID 碰運氣,則是更危險的復原方式。
這個 result envelope 是 AgentReady Events 為了讓錯誤可以一致驗收而定義的專案契約,
不是 WebMCP 規範要求的固定 JSON 格式。
RECOVERY-01 要測的是活動服務暫時失敗,不是「網路全部中斷」。因此我保留已經開啟的詳情頁,
只封鎖:
/api/events/evt-webmcp-intro
正確入口是 DevTools 的 Command Menu:
Ctrl+Shift+P
→ Request conditions
→ Show Request conditions
Ctrl+P 開的是檔案 Quick Open,不是工具面板;我第一次確實走錯入口,但文章不需要帶讀者
把錯路完整走一遍。進入 Request conditions 後,只對活動詳情的 fetch 使用
Block request URL,不要封鎖 document,也不要直接 block 整個 domain。

圖 2:只封鎖 /api/events/evt-webmcp-intro,頁面、Inspector 與其他 API 仍可使用。
規則建立後先不要重新整理頁面。詳情 route 已經有目前活動 context;此時重新整理,可能讓
初始載入也被自己擋住,測試題目就從「Tool 執行失敗」變成「頁面根本沒載入」。
我沒有立刻送自然語言 Prompt,而是先在 Inspector 手動執行:
{}
direct Tool control 正確進入被封鎖的 API,回傳:
{
"ok": false,
"code": "TEMPORARY_FAILURE",
"reason": "EVENT_SERVICE_UNAVAILABLE",
"retryable": true,
"uiUpdated": false
}
這一步只回答一件事:故障設定與錯誤 mapping 已經準備好。它不能證明 Agent 會選對 Tool、
產生合法 input,或正確理解 retryable: true。
但沒有這個控制組,後面 Agent 一失敗,我連 API 是否真的被封鎖都不知道。
null,根本還沒測到 API 故障Reset Inspector 後,我送出固定 Prompt:
查看這個活動;如果服務暫時失敗,只告訴我可否重試,不要改做其他動作。
第一次結果如下:

圖 3:Gemini 選中 get_event_details,卻把選填字串送成 null;Tool 在呼叫 API 以前就拒絕 input。
歷史 schema 說「通常不需要提供活動 ID」,Gemini 卻沒有省略欄位,而是送出:
{
"event_id": null
}
null 不是合法字串,也不是空物件。Tool 因而回傳 INVALID_INPUT / CURRENT_EVENT_MISMATCH,retryable 也是 false。
所以 Attempt 1 必須判定失敗,而且失敗類型是 arguments,不是 recovery。手動控制組已經證明
封鎖規則正常;真正的 Agent invocation 卻還沒走到那裡。
這個案例也說明,選填欄位不一定會被模型「省略」。對 route-bound Tool 來說,若唯一合法
輸入就是目前頁面 context,直接把 schema 收成 {} 會更清楚。
歷史 Attempt 2 在乾淨對話中送出 {},成功讀到 TEMPORARY_FAILURE;前兩次合在一起仍是
mixed,一次失敗不會因為下一次成功而消失。
後來 revision 0000008 將詳情頁的 get_event_details 收成只接受空物件,並把封鎖設定與
Agent trace 放進同一組證據,再用相同 Prompt 執行 Attempt 3。

圖 4:Agent 只呼叫一次 get_event_details({}),讀到 retryable:true 後回答可以稍後重試。
這次呼叫完整走過預期路徑:
自然語言 Prompt
→ get_event_details({})
→ 被封鎖的活動 API
→ TEMPORARY_FAILURE / EVENT_SERVICE_UNAVAILABLE
→ Agent 回答可以稍後重試
它沒有改做搜尋、收藏、報名或取消,也沒有自行換一個活動 ID。Attempt 3 因此可以列為
固定 revision 上的一次通過證據。
| 執行 | 版本/條件 | 判定 | 真正回答的問題 |
|---|---|---|---|
| Attempt 1 | 歷史選填 schema | FAIL | 模型送入 null,停在 arguments |
| Attempt 2 | 歷史 schema,乾淨對話 | PASS | {} 可以走到 retryable error;歷史整體仍 mixed |
| Attempt 3 | revision 0000008,嚴格 route profile |
PASS | 故障設定、Tool input、result 與最後回答能放在同一組證據 |
原始紀錄保留在:
Request conditions 設定證據
Attempt 1 Inspector Copy trace
Attempt 2 Inspector Copy trace
Attempt 3 Inspector Copy trace
三份紀錄的價值不在於最後湊出幾次 PASS,而是把問題逐層拆開:故障控制正常、第一次 input
不合法、收緊 schema 後才真正測到 recovery。
保存 trace 後,我會取消 Request conditions 規則,再重新整理詳情頁,手動執行get_event_details({}),確認結果恢復為 SUCCESS。
只把 DevTools 關掉不夠。已建立的 blocking pattern 仍可能保留,下一次重新啟用後又繼續
生效。受控故障如果沒有受控收尾,很容易變成明天最難找的環境問題。
今天原本只想測「服務暫時失敗時能不能重試」,最後卻先被 null 攔了一次。這個失敗沒有
浪費:它逼我把 no-tool、arguments、fault injection 與 recovery 分成四件事,也讓詳情頁的
最終 schema 從選填欄位收成真正明確的空物件契約。
唯讀錯誤現在已經能告訴 Agent 下一步。明天第一次跨過寫入邊界,讓 save_event 改變 session
狀態。我會故意收藏同一場活動兩次,看看第二次是安全地回報「已收藏」,還是替清單又多搬
一張一模一樣的椅子。