iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

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

Day 16|Agent 怎麼知道「我現在看的活動」是哪一場?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天的 search_events 已經找到「WebMCP 入門工作坊」,還順便帶回一個不透明的活動 ID:
evt-webmcp-intro。今天我沿著搜尋結果進入詳情頁,準備問下一個很自然的問題:

請整理我現在看的活動名稱、地點、時間與剩餘名額。

這句話對人類毫無難度。頁面上那麼大一個活動名稱,總不會是在介紹隔壁棚的活動。但對
Tool 來說,「現在看的」不是參數,也不是合法 ID;如果網站沒有提供頁面脈絡,Agent 就
只能開始猜 currentactive1,甚至 current_event

猜中算運氣,猜錯才是日常。今天要處理的,就是讓 Tool 知道使用者正站在哪一頁,同時
也要確保他離開之後,舊 Tool 不會繼續躲在背景裡加班。

這種把 Tool 綁定到目前頁面情境的做法,可以稱為 route-aware context。

活動詳情頁本來就知道自己正在顯示哪一場活動,因此網站可以在註冊 Tool 時,把目前活動的
opaque ID 一起綁進 handler。Agent 不必閱讀網址、解析畫面,更不需要發明 current 這類
不存在的 ID。

今天使用的歷史版本,仍把 event_id 留成選填欄位;詳情 route 會優先使用自己綁定的 ID,
所以 {} 可以正常執行,傳入另一個 ID 則會被拒絕。這份設計先解決「目前活動怎麼取得」,
但後面還會遇到 null 與跨 route handoff 的問題。系列最後才把同名 Tool 拆成兩個更明確
的 route profile:列表頁必填 ID,詳情頁只接受 {}

它省掉的是重複猜測,不是後端驗證。Tool 仍然只能讀取公開活動,離開詳情頁後,綁定該
route 的舊 instance 也必須解除註冊。

先切到加入活動詳情 route 的版本

搜尋基線驗收完後,今天需要的詳情頁程式差異封存在 v3-day-15。這個 tag 記錄的是程式
里程碑,不是文章篇號;先切到這個版本:

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

這個版本新增正式活動詳情頁、route-aware 的 get_event_details、對應樣式與 browser test。
今天可以驗證兩件事:站在詳情頁時,空 input 能取得目前活動;離開詳情頁後,舊 Tool 會從
catalog 退場。先有這個版本邊界,後面看到 Agent 猜 current 或敲到舊 Tool 時,才知道是
模型行為、瀏覽器生命週期,還是網站契約出了問題。

詳情頁已經知道活動 ID,不必再考 Agent 閱讀網址

我先開啟這個頁面:

/events/evt-webmcp-intro

如果 get_event_details 每次都強迫模型傳入 event_id,就等於要求 Agent 把網址中已經存在
的資料再抄一次。網站明明知道答案,卻把考卷推回給模型,這個分工有點說不過去。

v3-day-15 當時的 input schema 仍保留選填 event_id,實際執行規則是:

目前 route 已綁定活動,input 為 {}
→ 使用 route 裡的 evt-webmcp-intro

目前 route 已綁定活動,input 帶入同一個 event_id
→ 仍讀取目前活動

目前 route 已綁定活動,input 帶入另一個 event_id
→ 回傳 CURRENT_EVENT_MISMATCH

Tool 執行時會從目前 route 的 closure 取得 evt-webmcp-intro,再向 server 查詢公開活動。
這裡省掉的是重複輸入,不是 server 驗證。route 提供可信的查詢目標;活動是否存在、是否
公開,仍由後端判斷。

換句話說,{} 在這張詳情頁代表「目前活動」,並不是一張可以到處兌換資料的空白支票。
要查看別場活動,應先前往那場活動的 route,不能把另一個 ID 塞進目前這支 Tool。

目前頁面查詢的真實 Agent trace

圖 1:Agent 呼叫 get_event_details 時傳入 {};Tool 從詳情 route 取得正確 ID,回傳活動時間、地點與剩餘名額。

這一題是 SEL-02。Agent 沒有猜 current,也沒有請使用者再念一次網址,而是正確整理出
2027 年 1 月 23 日、台北前端共學空間與剩餘 8 名。到這裡可以證明:自然語言裡的「我
現在看的活動」,已經能落到目前詳情頁的 route-bound get_event_details

但這張 trace 只替當時的選填 schema 作證。它不能提前證明後來才完成的「詳情頁只接受空
物件」或「列表頁要求搜尋結果 ID」;那兩項要等修正版部署後另外驗收。

Tool 能跟著頁面上場,也要在離開時準時下班

查對活動只是前半場。接著我切回活動列表,想確認詳情頁的 closure 會不會還抓著
evt-webmcp-intro 不放。

每次 route 改變,頁面都會重新整理當下需要的 Tool definitions:

await registry.sync(currentDefinitions);

Registry 會比較新舊 catalog。不再需要的 Tool 透過 AbortSignal 解除;同一個 route 重複
render,也不會重複註冊。理想狀態下,catalog 應該回答「這一頁現在能做什麼」,而不是
把使用者曾經走過的每一頁都收藏成 Tool 博物館。

我把今天的驗收拆成兩題:

Case 操作 驗收重點 結果
SEL-02 在活動詳情頁詢問目前活動 {} 能否取得 route 綁定的活動 通過
STALE-01 從詳情頁切回列表後繼續對話 舊 handler 是否失效,Agent 是否改用新 route 的能力 第一輪失敗;修正版 STALE-01-V2 通過

第一次切換頁面後,Agent 仍嘗試呼叫已解除的 Tool

STALE-01 的第一輪 Prompt 是:

請先記住我現在看的活動,並告訴我它的名稱。

我原本想測跨頁後能不能延續同一個活動,結果「記住」兩個字先挖了一個坑。Agent 正確
呼叫 get_event_details({}) 之後,又接著呼叫 save_event。在它的理解裡,「記住」近似
「收藏」;但使用者並沒有明確要求寫入,所以這一回合不能算通過。

詳情頁公開兩個 Tool,Agent 先讀取活動再擅自收藏

圖 2:詳情頁公開 get_event_detailssave_event。黑色區域才是本次 trace;上方英文仍留在輸入框,沒有送出。

這張圖提醒我,測試 Prompt 也屬於契約的一部分。若真正想驗證的是 lifecycle,就不該再
塞一個可能被理解成寫入意圖的詞。下一版要直接說「只讀取,不要收藏或修改資料」。

接著我切回活動列表。這個歷史版本的列表頁 catalog 只剩 search_events,詳情頁上的
get_event_details 已經解除註冊:

切回列表後,Tool catalog 只剩 search_events

圖 3:離開詳情頁後,catalog 只剩 search_events。下方保留前一輪 trace;輸入框中的攝影活動文字沒有送出。

Agent 接下來依序嘗試了:

navigate_to_event  → catalog 中不存在,瀏覽器拒絕
get_event_details  → 詳情頁 Tool 已失效,瀏覽器拒絕
get_saved_events   → catalog 中不存在,瀏覽器拒絕
search_events      → 目前頁面有效,成功找回活動

這串失敗乍看很熱鬧,實際上要分開判讀。網站的 lifecycle 防線有守住:舊
get_event_details 沒有留在 catalog,無效呼叫也沒有真的執行。問題出在 Agent 仍猜了
不存在的 Tool,還試著呼叫已經下班的 handler;最後雖然改用有效的 search_events
找回活動,也只能算復原成功,不能把前面的亂敲門一起當成通過。

Failed to execute ... RegisteredTool 在這裡不是「網站殘留舊 Tool」的證據,正好相反,
它表示瀏覽器拒絕了目前 catalog 以外的呼叫。完整紀錄保留在:

後續修正把同名 Tool 拆成兩個 route profile

第一輪除了暴露 Prompt 歧義,也讓跨頁 handoff 的缺口變得很明顯:回到 /events 後,
Agent 手上雖然還有合法活動 ID,頁面卻沒有可以接住它的正式詳情 Tool。

後續 revision 0000008 因此完成兩項調整:

/events
get_event_details({ "event_id": "evt-webmcp-intro" })
→ 列表頁要求搜尋結果提供的 opaque ID

/events/evt-webmcp-intro
get_event_details({})
→ 詳情頁 schema 只接受空物件,目標由 route 綁定

同一版也在 /events 加入受目前選取狀態限制的 save_event,讓
search_events → get_event_details → save_event 可以在同一頁形成合法 Journey。這三支
Tool 從頁面載入時就存在,順序不是靠動態新增維持,而是由 ID continuity 與 state guard
檢查。

這項修正也解決了另一個稍後才遇到的問題:選填 event_id 可能被模型送成 null。把兩種
頁面情境拆成兩份 schema,比期待模型每次都正確判斷「省略」更直接。

修正版回到列表頁後,使用的是另一個 get_event_details

我沒有刪掉第一輪失敗,而是另外建立 STALE-01-V2。這次先把有歧義的「記住」拿掉:

請讀取目前活動的名稱與 event ID;不要收藏,也不要進行其他動作。

在詳情頁,Agent 只呼叫 get_event_details({}),取得活動名稱與
evt-webmcp-intro。切回列表後,我再要求它使用剛才的 event ID 查看同一場活動。

這裡最容易誤會,所以我特別講清楚:第二回合不是把詳情頁的舊 handler 留著繼續用。
詳情頁的 route-bound instance 已經被 abort;修正版 /events 則有自己的參數化
get_event_details,必須傳入合法的 opaque event ID。Agent 保留的是對話裡的
evt-webmcp-intro,實際呼叫的則是新 route 當下公開的另一個 Tool instance。

STALE-01-V2 切換 route 後沿用 opaque event ID,沒有額外寫入

圖 4:同一段 Inspector 對話跨越詳情與列表 route;第二回合把合法 event ID 交給新 route 的 get_event_details,完整順序以 Copy trace 為準。

這次 Agent 沒有呼叫 save_event,也沒有再猜 catalog 外的 Tool。STALE-01-V2
revision 0000009 通過,完整紀錄放在:

舊失敗與新通過必須一起留著,因為它們回答的是兩件不同的事。第一輪暴露 Prompt 歧義與
模型猜 Tool;V2 才證明修正版能完成 route rebind、沿用 opaque ID,而且沒有偷偷寫入。

這些 Inspector 圖片沒有同時拍到網址與 /health 版本座標,因此只能直接證明畫面裡的
行為順序。若要確認 commit、revision 與 image digest,仍要搭配版本證據帳本,不能只靠
截圖右上角那一小塊畫面自行腦補。

對話可以記得活動,但每次呼叫都要服從目前 route 的 schema

current 做成一個萬用特殊 ID,看起來很方便,後面卻會冒出更多問題:兩個分頁同時
開著時,它指哪一場?使用者切頁後何時失效?重新整理又該不該沿用?

最後我沒有替 event_id 留下一個全站通用的「可填可不填」模式,而是讓同名 Tool 在不同
route 使用不同且明確的契約:列表頁要求搜尋結果 ID,詳情頁只接受 {}。頁面切換時舊
instance 解除,新頁面再註冊自己的版本。

Agent 可以在對話中保留活動名稱與 ID;每次真的要呼叫時,仍得重新查看目前 catalog,
使用這一頁允許的 Tool 與 input schema。對話記得哪場活動,不能取代網站對目前 route 與
目標狀態的驗證。

今天的歷史版本先讓 get_event_details 從目前頁面找到正確活動,也確認舊 handler 會在離開
詳情頁後失效。後續修正版再補上參數化列表 Tool 與嚴格空物件 schema,才形成系列最後採用
的契約。

不過新的問題馬上排隊進場:找到之後到底要回多少資料?只回一句「找到了」很像敷衍,
把 server 物件整包倒出去又太豪邁。明天我會把三種 payload 並排,看看怎麼回得剛剛好。


上一篇
Day 15|讓人類與 Agent 共用同一套活動搜尋規則
下一篇
Day 17|Tool 回太少做不完,回太多又把內部資料倒給 Agent
系列文
網站終於會說話:30 天實作並驗證 Agent-ready 的 WebMCP 活動網站23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言