iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Modern Web

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

Day 14|五支 Tool 不再往外長:把產品範圍寫成會亮紅燈的契約

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天終於讓 Inspector 自己選中 search_events,留下第一筆完整自然語言 trace。看到成功
畫面的那一刻,我腦中很自然地冒出下一串需求:登入、付款、後台、推薦活動,好像都可以
「順便」補進來。

這種順便很有吸引力,也很會製造加班。

前幾天已經把網站上的候選操作刪到只剩五支 Tool。今天不再重新討論每一支為什麼存在,
而是處理更實際的問題:怎麼讓這個決定不只留在文章裡,而是變成後續開發不能偷偷跨過的
契約?

我的做法是先凍結產品範圍,再把每條 Journey 的最低條件寫成會亮紅燈的測試。明天開始
實作搜尋時,只要碰到收藏、報名或第六支 Tool,測試就應該先提醒我:「今天不是來做這個的。」

今天使用搜尋基線,但不宣稱五支 Tool 已經完成

今天切到 v3-day-14

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

這個版本建立正式活動網站外殼、人類可用的搜尋流程,以及後續 Tool 會共用的產品基線。
它適合用來確認畫面、搜尋與功能邊界,卻不代表五支 Tool 已經全數實作,更不代表二十道
Agent 題目已經通過。

今天完成的是規格層的凍結:

產品只保留五個正式 Tool 名稱
→ 五支 Tool 串成三條 Journey
→ 不同 route 使用明確 input contract
→ 報名與取消停在人類確認以前
→ 後續測試只能在這個範圍內補齊能力

五個 Tool、三條 Journey 與人類停點

圖 1:搜尋與收藏可以完成;報名與取消只做到準備,正式 mutation 留在人類確認之後。

三條 Journey 先決定 Agent 最遠能走到哪裡

活動網站最後只處理三條流程:

Journey Agent 可以做到哪裡 人類保留的決定
J1 搜尋 → 詳情 → 收藏 完成唯讀查詢與可復原收藏 收藏後仍可從 UI Undo
J2 準備報名 → 正式送出 把活動、姓名與 Email 放進可見表單 人類檢查後送出報名
J3 準備取消 → 正式取消 顯示取消對象與後果 人類閱讀後確認取消

第一條包含唯讀與低風險寫入,Agent 可以完成。後兩條會改變名額或既有權益,所以 Tool
名稱直接使用 prepare,而不是 submit_registrationcancel_registration

這個停點不能只靠一段溫馨提醒。Tool catalog 裡不提供 finalizer,server 端也要驗證
session、CSRF、ownership、目前狀態與 confirmation intent。Agent 願意停下來是一層;
系統本身不讓它越過去,才是另一層。

產品固定五個名稱,active catalog 仍由 route 決定

正式名稱只有這五個:

search_events
get_event_details
save_event
prepare_event_registration
prepare_registration_cancellation

這是整個產品允許出現的能力集合,不是要求每個頁面同時排出五支 Tool。最後採用的 route
契約如下:

Route/狀態 Active Tool 這一頁的 input 與狀態前提
/events search_eventsget_event_detailssave_event 搜尋回傳 opaque ID;詳情帶入該 event_id;收藏只能使用目前已顯示的同一活動
/events/:eventId get_event_detailssave_event 詳情 Tool 已綁定 route,只接受 {};收藏沿用目前活動 ID
/events/:eventId/register prepare_event_registration route 決定活動;Agent 只準備姓名與 Email
/registrations 有有效報名時 prepare_registration_cancellation 唯一一筆可用 {};多筆時使用頁面提供的 registration_id
其他頁面或無可操作狀態 不把與目前頁面無關的能力硬塞進 catalog

同名的 get_event_details 在列表頁與詳情頁使用不同 schema。列表頁不知道要看哪一場,所以
要求 event_id;詳情頁已由 route 綁定活動,因此只接受空物件。

我沒有把它設計成一個「有時候填、有時候不填」的選填欄位。這樣可以少掉兩種模糊狀態:
模型在該省略時傳入 null,或在 route-bound 情境偷偷換成另一個活動 ID。

/events 上的三支 Tool 會一起存在,但一起出現不等於可以跳步。搜尋先提供合法 ID,詳情
再把同一場活動顯示到頁面,收藏最後才接受目前目標一致的 ID。順序由 schema 與 state guard
保護,不靠 Agent 自己記住「照理說應該先做什麼」。

把文章裡的決定寫成會失敗的驗收條件

正式功能還沒補齊以前,我先寫下六個最低條件:

搜尋:UI 與 Tool 使用相同條件時,必須回傳同一批活動
詳情:列表頁只能使用 search_events 回傳的 ID;詳情頁以 {} 讀取目前 route
生命週期:離開詳情 route 後,舊的 route-bound Tool instance 必須失效
收藏:只能收藏目前已顯示的同一活動;重複呼叫仍只保留一筆
報名:prepare 階段的 POST /api/registrations 必須維持 0
取消:prepare 階段的取消 POST 必須維持 0;對象不唯一時必須要求選擇

有些測試在今天會亮紅燈,這是刻意的。紅燈不是失敗紀錄,而是一條施工線:明天只處理
搜尋,就不能因為收藏寫起來很順,順便把報名也塞進同一個 commit。

我也用 APPROVED_TOOL_NAMES 凍結五個正式名稱,讓 client catalog 與 shared contract 必須
一致,並明確拒絕 submit_registrationcancel_registration 之類的第六支 finalizer。
文章裡一句「這次不再加」很容易反悔,測試看門比較可靠。

二十道題不等最後一天才一次開獎

前一週整理的二十道題,接下來跟著功能分批執行:

開發階段 同步檢查的行為
搜尋與詳情 Tool 選擇、參數保留、opaque ID、route context 與舊 instance
收藏與多步驟 同一 ID、操作順序、畫面更新與重複安全
報名與取消 資訊不足先追問、ownership、確認前零 mutation 與 session expiry
安全與復原 不該呼叫時停下、不可信內容、暫時失敗與可採取的下一步

可控制的程式行為交給 deterministic tests;真正由模型選 Tool、組參數與串接下一步的部分,
則保存 Inspector trace。

兩種證據都重要,但不能互相代打。測試程式知道該呼叫誰,不代表模型面對自然語言也知道;
一次 Agent 成功,同樣不能替其餘十九題簽到。

OAuth、付款與後台先留在範圍外

真實活動平台當然可能需要登入、付款、資料庫與管理後台。但它們會帶來新的帳號模型、金流
責任與權限邊界,卻不會直接回答這個系列正在追的問題:

網站 Tool 能不能被正確選擇、取得目前頁面 context,並在副作用發生以前停下來?

所以這次不加 OAuth、不做付款、不蓋後台,也沒有第六支 Tool。把範圍切小,才能讓每個
成功與失敗都回到同一條 WebMCP 主線,不會被一大桌 CRUD 蓋過去。

今天完成的是可檢查的範圍,不是五支 Tool 全數通關

昨天的 trace 只證明一次唯讀搜尋;今天則把五個正式名稱、三條 Journey、route profile、
人類停點與驗收條件固定下來。

因此今天能主張的是 E1:產品契約已存在,而且可以被檢查。v3-day-14 還不是最終實作,
後續版本也必須用自己的測試與 trace 補上證據,不能把昨天單一搜尋的 E4 直接加到整份 catalog。

範圍終於釘好後,明天先沿著最安全的一段往前走:讓人類表單與 search_events 共用同一套
搜尋規則,再拿不同說法檢查 Agent 有沒有保留所有條件。第一支 Tool 如果還會給出兩套答案,
第五支只會讓問題更熱鬧。


上一篇
Day 13|第一次按下 Inspector 的 Send,先被 Gemini 429 擋在門外
下一篇
Day 15|讓人類與 Agent 共用同一套活動搜尋規則
系列文
網站終於會說話:30 天實作並驗證 Agent-ready 的 WebMCP 活動網站23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言