前端開發 別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站 · 第 16 / 30 篇

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

2026 iThome 鐵人賽 WebMCP Day 16:安安~我是ChiYu~ 昨天的 search_events 已經找到「WebMCP 入門工作坊」,還順便帶回一個不透明的活動 ID: evt-webmcp-intro。今天我沿著搜尋結果進入詳情頁,準備問下一個很自然的問題: 請整理我現在看的活動名…

快速導覽 查看本文目錄

本文同步自 2026 iThome 鐵人賽原文 。完整系列可見 iThome 系列頁 。

安安~我是ChiYu~

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

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

這句話對人類毫無難度。頁面上那麼大一個活動名稱,總不會是在介紹隔壁棚的活動。但對 Tool 來說,「現在看的」不是參數,也不是合法 ID;如果網站沒有提供頁面脈絡,Agent 就 只能開始猜 current、active、1,甚至 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_details 與 save_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 並排,看看怎麼回得剛剛好。

讀完了!你覺得這篇如何?

你的回饋會決定下一篇要寫什麼。

還有疑問

Learning path

別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站

這篇是系列中的第 16 / 30 篇。可以順著列表補完整脈絡,也可以直接跳到需要的段落。

  1. 01 Day 01|網站真的會被 Agent 使用嗎?我先丟了一句自然語言
  2. 02 Day 02|換到全新資料夾後,網站還能不能跑?
  3. 03 Day 03|先別急著做 Tool,我把活動網站自己走了一遍
  4. 04 Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住?
  5. 05 Day 05|只改按鈕四個字,Playwright 為什麼停在 click 以前?
  6. 06 Day 06|Playwright、REST API、MCP 與 WebMCP,各自負責哪一段?
  7. 07 Day 07|Agent 偶爾答對還不夠,我先出二十道測試題
  8. 08 Day 08|WebMCP Tool 該怎麼設計?把「點按鈕」改成「搜尋活動」
  9. 09 Day 09|網站該公開哪些 WebMCP Tool?從操作盤點到五支正式能力
  10. 10 Day 10|Inspector 裝好了,為什麼 Tool 面板還是一片空白?
  11. 11 Day 11|什麼是 Declarative API?讓 HTML 表單成為 WebMCP Tool
  12. 12 Day 12|Tool 註冊完還不算完成:離開 route 後誰把它收回來?
  13. 13 Day 13|第一次按下 Inspector 的 Send,先被 Gemini 429 擋在門外
  14. 14 Day 14|五支 Tool 不再往外長:把產品範圍寫成會亮紅燈的契約
  15. 15 Day 15|讓人類與 Agent 共用同一套活動搜尋規則
  16. 16 Day 16|Agent 怎麼知道「我現在看的活動」是哪一場? 目前閱讀
  17. 17 Day 17|Tool 回太少做不完,回太多又把內部資料倒給 Agent
  18. 18 Day 18|活動服務暫時失敗時,Agent 應該重試還是停下來?
  19. 19 Day 19|同一場活動收藏兩次,網站為什麼只能留下同一筆?
  20. 20 Day 20|表單填好了,為什麼這次報名仍然不能算通過?
  21. 21 Day 21|一句「這個我不要了」,Agent 到底敢不敢直接取消?
  22. 22 Day 22|五支 Tool 如何共用同一套規則,串成三條完整流程?
  23. 23 Day 23|在問 Agent 聰不聰明前,先證明網站沒有壞
  24. 24 Day 24|活動摘要叫 Agent 洩漏祕密,它會把這句話當資料還是命令?
  25. 25 Day 25|localhost 成功不算交付:怎麼證明公開網址跑的是同一份程式?
  26. 26 Day 26|把自然語言送進公開站,Agent 真的會自己選對 Tool 嗎?
  27. 27 Day 27|寫入型 Tool 怎麼驗收?把 Trace、Network、UI 與 Server 狀態放在一起看
  28. 28 Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?
  29. 29 Day 29|把網站交給 Agent 前,我最後會檢查哪十件事?
  30. 30 Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼?

Further reading

Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼? 封面圖 2026-09-30 Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼? Day 29|把網站交給 Agent 前,我最後會檢查哪十件事? 封面圖 2026-09-29 Day 29|把網站交給 Agent 前,我最後會檢查哪十件事? Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層? 封面圖 2026-09-28 Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?

留言討論

用 GitHub 帳號留言,留言會同步到這個 Blog 的 GitHub Discussions。

閱讀導覽

約 8 分鐘

本文目錄

本篇所在系列 · 第 16 / 30 篇

別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站

  1. Day 01|網站真的會被 Agent 使用嗎?我先丟了一句自然語言
  2. Day 02|換到全新資料夾後,網站還能不能跑?
  3. Day 03|先別急著做 Tool,我把活動網站自己走了一遍
  4. Day 04|按鈕只多包一層,哪種 Playwright Locator 撐得住?
  5. Day 05|只改按鈕四個字,Playwright 為什麼停在 click 以前?
  6. Day 06|Playwright、REST API、MCP 與 WebMCP,各自負責哪一段?
  7. Day 07|Agent 偶爾答對還不夠,我先出二十道測試題
  8. Day 08|WebMCP Tool 該怎麼設計?把「點按鈕」改成「搜尋活動」
  9. Day 09|網站該公開哪些 WebMCP Tool?從操作盤點到五支正式能力
  10. Day 10|Inspector 裝好了,為什麼 Tool 面板還是一片空白?
  11. Day 11|什麼是 Declarative API?讓 HTML 表單成為 WebMCP Tool
  12. Day 12|Tool 註冊完還不算完成:離開 route 後誰把它收回來?
  13. Day 13|第一次按下 Inspector 的 Send,先被 Gemini 429 擋在門外
  14. Day 14|五支 Tool 不再往外長:把產品範圍寫成會亮紅燈的契約
  15. Day 15|讓人類與 Agent 共用同一套活動搜尋規則
  16. Day 16|Agent 怎麼知道「我現在看的活動」是哪一場?
  17. Day 17|Tool 回太少做不完,回太多又把內部資料倒給 Agent
  18. Day 18|活動服務暫時失敗時,Agent 應該重試還是停下來?
  19. Day 19|同一場活動收藏兩次,網站為什麼只能留下同一筆?
  20. Day 20|表單填好了,為什麼這次報名仍然不能算通過?
  21. Day 21|一句「這個我不要了」,Agent 到底敢不敢直接取消?
  22. Day 22|五支 Tool 如何共用同一套規則,串成三條完整流程?
  23. Day 23|在問 Agent 聰不聰明前,先證明網站沒有壞
  24. Day 24|活動摘要叫 Agent 洩漏祕密,它會把這句話當資料還是命令?
  25. Day 25|localhost 成功不算交付:怎麼證明公開網址跑的是同一份程式?
  26. Day 26|把自然語言送進公開站,Agent 真的會自己選對 Tool 嗎?
  27. Day 27|寫入型 Tool 怎麼驗收?把 Trace、Network、UI 與 Server 狀態放在一起看
  28. Day 28|Agent 選錯、傳錯或答錯時,怎麼知道問題卡在哪一層?
  29. Day 29|把網站交給 Agent 前,我最後會檢查哪十件事?
  30. Day 30|網站終於會說話:做完五支 Tool 後,我學到什麼?
看完整系列 30 篇