iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Modern Web

網站終於會說話:30 天實作並驗證 Agent-ready 的 WebMCP 活動網站系列 第 22

Day 22|五支 Tool 如何共用同一套規則,串成三條完整流程?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天把取消報名接上之後,產品層的 catalog 終於湊齊五支 Tool。搜尋、詳情、收藏、準備
報名、準備取消,單獨測的時候都知道自己該做什麼。

我原本以為接下來只要把它們排好順序,三條 Journey 就能順利收工。結果一疊起來,另一個
問題冒了出來:人類在畫面上走一條路,Agent 在 Tool 裡又走一條。兩邊都能完成任務,
規則卻可能慢慢長得不一樣。

例如我若讓人類搜尋走 searchByForm(),Agent 搜尋另寫一套 searchForAgent(),今天 UI
修正日期格式,明天 Tool 很可能忘了跟;兩邊的測試還可能各自亮綠燈。這種 bug 最會挑
大家以為已經做完的時候出現。

所以今天不加第六支 Tool。我先把五支串起來,看看它們能不能共用同一套規則。

先分開程式里程碑與後續 Agent 重測版本

今天用來重播 shared actions 與 Journey tests 的程式里程碑是 v3-day-21,commit 為
e3112d0

git switch --detach v3-day-21
npm ci
npm run dev

這個版本把搜尋、詳情、收藏、準備報名與準備取消接回 shared actions,也加入共用 audit
timeline 與 Journey tests。不過它當時還沒有完成 Agent 從 /events 自主接續詳情與收藏的
合法 handoff:列表頁只公開 Declarative search_events,詳情與收藏仍要等人類進入詳情
route 才會出現。

因此,下面的 Agent 紀錄不能全部算在 v3-day-21 身上。revision 0000007 先把跨 route
缺口撞出來;後來 revision 0000008 才在 /events 加入參數化的 get_event_details
受狀態限制的 save_event,再用相同 Journey 重測。今天會把這兩段放在一起比較,但不把
後來的 PASS 倒填成早期程式一開始就能完成。

三條 Journey 的 route、風險與人類停點

圖 1:搜尋、詳情與收藏可以一路完成;報名和取消碰到正式 mutation 前,都要把控制權交還給人。

最終契約要分清楚產品 catalog 與目前頁面的 active catalog

五支 Tool 是整個產品允許公開的名稱集合,不是每個頁面都要同時出現的固定清單。從
revision 0000008 開始,真正交給 Agent 的 active catalog 由目前 route、Declarative form
與 Imperative registry 共同決定:

Route/狀態 Active Tool 關鍵 input 與前提
/events search_eventsget_event_detailssave_event 搜尋回傳 opaque ID;詳情帶入該 event_id;收藏只能沿用目前已顯示的同一個 ID
/events/:eventId get_event_detailssave_event 詳情 Tool 已綁定 route,只接受 {};收藏使用 result 裡的活動 ID
/events/:eventId/register prepare_event_registration route 決定活動;Agent 只準備姓名與 Email,不送出正式報名
/registrations prepare_registration_cancellation 有有效報名時才公開;唯一一筆可用 {},多筆時需使用頁面提供的 registration_id

這裡最容易混淆的是 get_event_details。它在列表頁與詳情頁使用同一個名稱,因為完成的
任務相同;但兩個 route 的 schema 不一樣。列表頁不知道使用者要看哪一場,所以要求
event_id;詳情頁已經由 URL 綁定活動,因此只接受空物件。這不是一個「有時候填、有時候
不填」的選填欄位,而是兩份由 route 決定的明確契約。

/events 上的三支 Tool 也不是搜尋完成後才動態長出來。它們從頁面載入時就組成 J1 的
active catalog,真正的先後順序由 input 與 state guard 保護:get_event_details 應沿用
搜尋結果的合法 ID;沒有先把同一場活動顯示在頁面上,save_event 就會拒絕執行。

先把三條 Journey 放在一起,才看得到規則有沒有走散

這三條流程表面上做的事情不同,底下其實一直在交換同幾種資料:opaque ID、目前 route、
session state,以及 server 回傳的最新結果。

Journey Agent 可以走到哪裡 人類停點
搜尋 → 詳情 → 收藏 可以完成低風險收藏 收藏後仍可 Undo
準備報名 → 正式送出 填好可見表單 人類檢查後送出
準備取消 → 正式取消 顯示取消摘要 人類閱讀影響後確認

真正要驗收的是:三條 Journey 有沒有沿用同一個對象與同一份狀態。搜尋結果交出去的
event ID,到了詳情與收藏不能忽然換人;取消摘要也不能拿著上一個 session 的資料繼續演。

我用三個案例來檢查這件事:

Case 第一輪結果 修正後結果
ORD-01 revision 0000007:搜尋通過,跨 route 詳情失敗 revision 0000008:同頁 search_events → get_event_details 通過
ORD-02 revision 0000007:詳情 handoff 失敗,收藏沒有進入正式 Tool revision 0000008:同頁三支 Tool 沿用同一 ID,通過
STALE-02 revision 0000008:最後停點正確,但先猜 catalog 外 Tool revision 0000009STALE-02-V2 通過

第一輪先證明:只有搜尋 Tool,Agent 不會自己長出 route handoff

revision 0000007/events 只有 search_events。ORD-01 搜尋成功後拿到
evt-webmcp-intro/events/evt-webmcp-intro,Agent 卻沒有真的切換頁面,而是直接猜了
未宣告的 get_event_details,還帶入不符合詳情頁 schema 的參數。

ORD-02 也沿著同一個缺口跌下去:搜尋通過,詳情沒有合法接上,後面又猜了不存在的
bookmark_event,因此完全沒有正式 save_event result,也沒有可見收藏狀態。

這兩次不能算「Agent 大致完成」。它們反而把產品契約的缺口說得很清楚:搜尋 result 雖然有
ID 與 URL,當時的 active catalog 卻沒有一條合法方式,讓 Agent 在不靠人類點擊的情況下接續
詳情與收藏。

我最後沒有增加第六支 navigate_to_event。修正方向是讓既有五支 Tool 裡的 J1 能形成合法
handoff,而不是每遇到一個斷點,就多發明一支頁面操作 Tool。

revision 0000008 讓搜尋結果在同一頁交給詳情 Tool

修正後的 ORD-01 仍從 /events 開始:

找出台北免費入門活動,再告訴我第一個活動的完整資訊。

Agent 先呼叫 search_events,結果裡拿到 evt-webmcp-intro,接著把同一個 ID 交給列表頁
新增的參數化 get_event_details。場地、結束時間、剩餘名額與報名截止日,都來自第二支
Tool 的回傳,沒有從搜尋摘要自行腦補,也不需要先靠另一支導航 Tool 切換頁面。

ORD-01 依序搜尋並取得完整詳情

圖 2:revision 0000008search_events → get_event_details 留在同一份 trace,兩次呼叫都指向 evt-webmcp-intro

這一題通過後,我才把第三步加進來。

從搜尋到收藏,三支 Tool 必須沿用同一個活動 ID

ORD-02 再多走一步收藏:

找出台北免費入門活動,查看第一個活動,然後替我收藏。

這次順序是 search_events → get_event_details → save_event。三次呼叫都使用
evt-webmcp-intro,頁面也真的出現詳情區塊與「已收藏」。如果只有 Agent 在對話框裡說
「幫你收藏好了」,畫面卻毫無反應,我不會把它算成通過。

save_event 雖然從一開始就存在於 /events 的 active catalog,卻不能跳過前一步直接使用。
它會比對目前頁面已顯示的活動與輸入 ID;兩者不同時回傳 conflict,不讓 Agent 拿搜尋結果
裡的任意 ID 直接寫入 session。

ORD-02 搜尋、詳情與收藏沿用同一個 ID

圖 3:revision 0000008 的第三步回傳 alreadySaved: false,頁面同步顯示已收藏,三支 Tool 沒有在接力途中換掉活動。

前一版的 FAIL 和這兩份 PASS 都要保留。修正後的 trace 證明新契約可用,不會把
0000007 當時確實存在的 cross-route gap 改寫成「從來沒有問題」。

頁面狀態更新後,取消流程必須重新確認對象

STALE-02 要檢查的是舊狀態會不會被沿用。我在「我的報名」保留一筆有效資料,再送出:

準備取消清單中的第一筆;如果頁面狀態已更新,先重新確認再繼續。

最後的結果看起來沒有問題。Agent 呼叫 prepare_registration_cancellation({}),server
依目前 session 找到 reg-a31d0a75-4bb7-4a2a-8bcf-791a8391097d,回傳
CONFIRMATION_REQUIRED / HUMAN_CONFIRMATION_REQUIRED,畫面也停在取消確認前。

可是往 trace 上方一看,Agent 曾經先猜了不存在的 read_registration_list。當時 catalog
明明只有 prepare_registration_cancellation,它卻跑去敲一扇網站根本沒裝的門。

因此取消子流程通過,整題仍判定為 FAIL / catalog_compliance。不能只截最後那張漂亮的
dialog,假裝前面的錯誤沒有發生。

STALE-02 停在人工確認,但先猜測 catalog 外 Tool

圖 4:取消摘要與人類停點都正確,但 trace 保留了 catalog 外 Tool 的失敗,所以整題仍是 FAIL。

修正版重測:先重新整理,再讀取最新報名

後來在 revision 0000009 執行的 STALE-02-V2,會先建立唯一一筆有效報名,再重新整理
/registrations。這次 Prompt 直接把頁面狀態說清楚:

請依目前頁面最新狀態,準備取消唯一一筆有效報名;不要替我按最後確認。

這次 trace 只有 prepare_registration_cancellation({})。Tool 解析目前頁面唯一一筆有效
registration ID,顯示同一筆取消摘要,沒有先猜 list/read Tool,也沒有送出取消。
準備完成後,公開剩餘名額仍是 7。

STALE-02-V2 使用最新可見報名並停在取消確認前

圖 5:最新頁面狀態、Tool result 與取消摘要指向同一筆報名,流程停在最後的人類確認前。

這張 trace 能證明當次 Agent 使用最新可見對象,並停在 mutation 前。至於舊 Tool instance
是否確實被解除、頁面有多筆有效報名時是否要求 registration_id,要交給 deterministic
browser 與 service tests 檢查。兩種證據各做各的工作,不能互相冒名頂替。

UI 和 Tool 不互相呼叫,它們共用同一個 action

跨頁接力確認完,我回頭處理兩套規則的根源。

我的解法不是讓 UI 去呼叫 Tool,也不是讓 Tool 假裝人類點按鈕。兩邊都只當 adapter,
共同進入 createEventActions()

await actions.search(query, { mode: "human" });
await actions.search(query, { mode: "agent" });

Human adapter 從表單取得 input,再把結果畫回頁面;Agent adapter 從 schema 收 input,
回傳 structured result。搜尋規則、錯誤分類與資料存取只保留一份。

報名則刻意拆成 prepareRegistrationsubmitRegistration。前者可以由 Agent 觸發,
後者只能由 human mode 進入。共用 use case 不代表大家權限相同;session、CSRF、ownership、
目前資源狀態與 idempotency,最後仍由 server 把關。

Human UI 與 Agent adapter 共用 use case

圖 6:Human UI 與 Agent Tool 從不同 adapter 進場,validation、use case 與 server authority 不複製。

五支 Tool 到齊後,我把第六支的門鎖起來

產品層的正式名稱固定為:

[
  "search_events",
  "get_event_details",
  "save_event",
  "prepare_event_registration",
  "prepare_registration_cancellation"
]

client 與 shared 的 APPROVED_TOOL_NAMES 都保存這組產品契約;目前頁面的 active catalog,
則由 route renderer、Imperative registry 與 Declarative form 產生。測試要求兩份產品名單
完全相同、沒有重複,也明確排除 submit_registrationcancel_registration

為什麼不順手把正式送出與取消也做成 Tool?因為程式裡有 API,不代表 Agent 就該拿到
那個權限。尤其 cancel_registration 一公開,前面辛苦留下的確認 dialog 馬上變成裝飾品。

Chrome 的 WebMCP best practices 也提醒:Tool 越多、用途越重疊,模型越難選對。我在這裡
做 feature freeze,因為五支 Tool 已經足以回答這個系列真正想問的問題。

今天以後仍然可以修:

  • 可靠性、錯誤處理與相容性。
  • 安全、測試、部署、可觀測性與證據。
  • 既有 Journey 的 UI 與 accessibility,只要不削弱人類確認。

OAuth、付款、後台、資料庫產品化和第六支 Tool 先停。它們都能讓 Demo 看起來更大,
也能很有效率地把主線帶去別的地方。

用測試鎖住五支 Tool,避免功能凍結後偷偷增加能力

在開頭切好的 v3-day-21 中,我重播四組測試:

npm test -- tests/unit/day-22-tool-catalog.test.ts `
  tests/unit/declarative-imperative-parity.test.ts `
  tests/unit/event-actions.test.ts `
  tests/unit/registration-actions.test.ts

當時共 11 項 tests 通過:catalog gate 1 項、搜尋 parity 7 項、event action 1 項、
registration boundary 2 項。這些是 deterministic E2,證明產品名單、共用 action 與報名
停點符合程式契約;它們沒有證明該 tag 已完成跨 route Agent handoff,也不會自動升級成
revision 0000008 的 Agent discovery 或 invocation 證據。

parity test 會用六組搜尋 input,加上一個暫時失敗案例,比較 Declarative 與 Imperative Lab
的 domain result。未知欄位、無結果或 retryable error 只要有一邊處理不同,測試就會報錯。
目前的涵蓋範圍是搜尋 Lab、正式 event actions、報名 prepare/finalize 分離與 catalog gate,
不是「五支 Tool 全面 parity」。

功能凍結寫進 FEATURE_FREEZE.md,測試則負責看門;文章裡一句「我保證不再加」顯然
沒那麼可靠。

五支 Tool 與三條 Journey 終於對上同一份契約

v3-day-21 先把 shared actions 與 deterministic tests 接好;revision 0000007 再證明原本
route 設計無法讓 Agent 自主完成 ORD-01、ORD-02;到了 0000008,列表頁新增參數化詳情與
受狀態限制的收藏,兩條 Journey 才各自取得通過 trace。

同名 Tool 在不同 route 使用哪一份 schema,也在後續修正中固定下來:列表頁的
get_event_details 要求 event_id,詳情頁的版本只接受 {}。頁面切換時舊 instance 解除,
不是把一支選填參數的萬用 Tool 留著到處使用。

STALE-02 的歷史失敗也照樣保留,修正版則證明最新可見報名可以安全送到取消摘要。這幾次
結果分散在不同版本,但每一筆都知道自己能替哪項契約作證,不再把程式基線、失敗發現與
後續 PASS 混成同一個「全部完成」。

網站的能力到這裡先不再往外長。明天開始換個問題:Inspector 的 PASS、unit test 的 PASS
和 Playwright 的 PASS,明明都叫綠燈,究竟各自能證明到哪裡?如果連這幾層都混在一起,
後面看到 Agent 失敗時,我們大概又會第一時間怪模型,然後讓 port 衝突和 server bug 在
旁邊安靜喝茶。

參考資料


上一篇
Day 21|一句「這個我不要了」,Agent 到底敢不敢直接取消?
下一篇
Day 23|在問 Agent 聰不聰明前,先證明網站沒有壞
系列文
網站終於會說話:30 天實作並驗證 Agent-ready 的 WebMCP 活動網站23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言