iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Modern Web

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

Day 19|同一場活動收藏兩次,網站為什麼只能留下同一筆?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天把唯讀失敗拆成「該停止」與「可以稍後重試」,也確認 Agent 不能一遇到錯誤就擅自換
動作。今天終於往前跨一步,讓它真的改變網站狀態。

不過第一次寫入,我沒有直接挑報名。報名會占用名額,一個不小心就不是 Demo,而是替其他
參加者製造困擾。先從可以復原的收藏開始,比較適合拿來確認寫入流程有沒有站穩。

收藏沿用詳情頁版本,今天不新增 Tag

今天仍沿用 v3-day-15。這個版本已具備活動詳情頁、get_event_detailssave_event,所以
不用為了一題收藏測試再切出沒有 source 差異的里程碑。單篇閱讀時,先切到固定版本:

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

今天新增的是寫入與重複呼叫的實測證據,不是另一份 source。版本先說清楚,後面看到
alreadySavedfalse 變成 true 時,才知道它是同一版網站的冪等行為。

接著進入活動詳情頁,我在 Inspector 輸入:

把這個活動收藏,然後再做一次同樣的事。

Agent 真的連續呼叫兩次 save_event。如果後端每收到一次 request 就 append 一筆資料,畫面
應該會長出兩筆相同收藏。實際結果是第一次回傳 alreadySaved: false,第二次變成
alreadySaved: true,畫面始終只有一筆,旁邊還留著讓人類反悔的 Undo。

這就是今天要驗收的冪等性:相同動作重複執行,最後狀態仍然相同。第一次建立收藏;第二次
發現目標狀態已成立,不再新增資料,只把這件事明確回報給呼叫端。

低風險寫入,該守的邊界還是一個都不能少

收藏不會占用活動名額,也可以取消,確實比正式報名安全。但「風險較低」不代表在前端切換
一個布林值就能交差。

save_event 至少要處理四件事:收藏屬於哪個 session、event ID 是否指向公開活動、重複呼叫
會不會多出第二筆,以及使用者能不能在畫面上看見結果並復原。

收藏後的可見狀態與操作紀錄

圖 1:收藏後的狀態與操作紀錄同步更新,旁邊也保留 Undo。

寫入結果若只存在 Tool result 裡,人類不知道網站被改了什麼;反過來,若只有畫面改字、
server 沒留下狀態,也只是演得很像成功。兩邊必須對得起來。

Agent 提供活動 ID,使用者身分由 session 決定

這支 Tool 的輸入很小,Agent 只需要傳目前活動的不透明 ID:

{ "event_id": "evt-webmcp-intro" }

我沒有讓它傳 user_id。使用者身分來自 server session,不是由 Tool body 自我申報。否則
Agent 只要送出下面這種資料,就可能把「我要收藏」變成「替別人收藏」:

{ "event_id": "...", "user_id": "admin" }

在 Tool schema 這一層,additionalProperties: false 會擋掉 user_id 之類的額外欄位。到了
HTTP route,server 只讀取 eventIdinteractionMode,實際收藏仍寫進 cookie 對應的
session;request 沒有有效 CSRF token 時,直接回傳 403

readOnlyHint: false 只是告訴 Agent「這支 Tool 會改變狀態」,不是通行證,也不會自動完成
身分驗證。真正的存取邊界仍在 HTTP 與 server-side state。

第二次收藏不該報錯,也不該新增資料

第一次收藏時,server 回傳:

{
  "ok": true,
  "code": "SUCCESS",
  "data": {
    "eventId": "evt-webmcp-intro",
    "alreadySaved": false
  },
  "uiUpdated": true
}

同一個 session 再收藏同一場活動,結果變成:

{
  "ok": true,
  "code": "SUCCESS",
  "data": {
    "eventId": "evt-webmcp-intro",
    "alreadySaved": true
  },
  "uiUpdated": true
}

第二次依然是 SUCCESS,因為使用者想要的狀態已經成立;alreadySaved: true 則告訴呼叫端
「這筆原本就在」。後端使用 Set 保存 event ID,再次加入相同值不會產生第二筆資料。

網路 timeout、模型重試,甚至使用者自己多說一次,都可能讓相同 request 再來一遍。前面把
冪等性守住,第二次敲門時才不會順便多搬一張沙發進來。

實際讓 Agent 收藏兩次

在題庫中,這個案例的編號是 REPEAT-01。程式層的驗收條件是:

同一個 session
同一個 event_id
連續呼叫 save_event 兩次
→ 收藏集合仍只有一筆
→ 第二次回傳 SUCCESS,且 data.alreadySaved = true

service test 與 browser test 先確認 server、session 和 UI 符合契約。接著才在 Inspector 執行
REPEAT-01,確認模型真的會從自然語言走進同一條路徑。

Agent 先用 get_event_details({}) 取得目前活動 ID,再連續執行:

save_event({ "event_id": "evt-webmcp-intro" })
save_event({ "event_id": "evt-webmcp-intro" })
呼叫 code alreadySaved 畫面狀態
第一次 SUCCESS false 顯示已收藏
第二次 SUCCESS true 維持同一筆收藏

REPEAT-01 由 Agent 連續收藏兩次,畫面仍只有一筆

圖 2:Agent 的確呼叫兩次收藏,UI 仍維持同一筆,並讓使用者可以 Undo。

這次不是我在下方手動按兩次 Execute Tool。原始 Copy trace 包含中文 Prompt、Agent 自行
取得活動 ID、兩次 save_event、兩份 Tool result,以及最後的自然語言回答。畫面上方雖然
還留著 Inspector 預設英文示例句,Copy trace 裡的 userPrompt.message 只有本題中文,判讀
時以原始 JSON 為準。

自動測試回答「程式被重複呼叫時,系統是否安全處理」;Agent trace 補上「模型在這次對話中
是否真的選擇並呼叫兩次」。前者屬於 deterministic E2,後者是這次 invocation 的 E4。

這份 trace 沒有公開網址、commit 與 revision,無法單獨綁定不可變部署版本,也不能宣稱
乾淨環境重播的 E5。證據限制在這裡說清楚一次即可。

為什麼畫面有 Undo,catalog 卻沒有 unsave_event

畫面可以 Undo,五支正式 Tool 裡卻找不到 unsave_event。這是刻意留下的產品邊界:收藏後,
人類可以立刻從可見介面復原;catalog 不需要把每個反向操作都公開給 Agent。

每多一支 Tool,就會多一組選擇、授權與測試情境。前幾天已經固定五支 Tool,我不打算為了
讓清單看起來更豐富,再偷偷塞進第六支。

目前設計已足以驗證低風險寫入的三件事:狀態屬於 session、重複呼叫不會複製資料,以及
人類看得到結果也能反悔。這比 Tool 數量更重要。

今天把低風險寫入驗收到哪裡?

重播時可以照三步確認:

  1. 開一個新 session,收藏活動一次,確認畫面與 audit trail 同步更新。
  2. 對同一活動再收藏一次,確認 data.alreadySaved 變成 true,收藏清單仍只有一筆。
  3. 比對 Tool schema、session 與 API 測試:額外的 user_id 不會成為授權依據,缺少 CSRF
    的 request 會被 server 拒絕。

做完後,save_event 已經不只是「Agent 可以按收藏」:它有 session 歸屬、可辨識的重複
結果、可見 UI,也留了一條人類 Undo 的退路。

明天要碰的報名沒這麼客氣。它會真的占用一個名額,不能把今天的低風險寫入原封不動搬過去。
我會讓 Agent 把姓名與 Email 帶進可見表單,接著停在 CONFIRMATION_REQUIRED;除了畫面要
停下來,Network 裡也不能偷跑 registration POST。那顆煞車若只存在文案裡,可不算煞車。


上一篇
Day 18|活動服務暫時失敗時,Agent 應該重試還是停下來?
下一篇
Day 20|表單填好了,為什麼這次報名仍然不能算通過?
系列文
網站終於會說話:30 天實作並驗證 Agent-ready 的 WebMCP 活動網站23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言