iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Modern Web

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

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

  • 分享至 

  • xImage
  •  

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

安安~我是ChiYu~

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

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

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

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

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

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

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

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

它也真的照做,連續呼叫兩次 save_event

這時候問題來了:如果後端每收到一次 request 就 append 一筆資料,畫面上應該會長出
兩筆相同收藏。幸好實際結果沒有。第一次回傳 alreadySaved: false,第二次變成
alreadySaved: true,畫面始終只有一筆,旁邊還留著讓人類反悔的 Undo。

這種「同一個動作重複執行,最後狀態仍然相同」的特性,就是冪等性。

收藏第一次成功時,網站建立一筆收藏;第二次收到相同活動 ID 時,不再新增資料,只回報
它原本就已收藏。對 Agent 來說,重送 request 不會把收藏清單越堆越長;對使用者來說,
畫面也始終只有一筆。

alreadySaved 正是用來區分這兩種情況:第一次是 false,第二次是 true,但兩次完成後
的網站狀態相同。這就是今天要驗收的重點:Agent 可以寫入,但不能把副作用也複製一份。

收藏風險不高,該守的邊界還是一個都不能少

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

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

先看收藏成功後的畫面。按鈕狀態、提示文字與 audit trail 都一起更新,使用者不必只相信
Agent 在對話框裡說「已完成」。

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

圖 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 維持同一筆收藏

接下來這張圖要同時看左右兩邊。右側 trace 留下兩次 save_event,左側網站則顯示
「已收藏(沒有重複新增)」。

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

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

這次不是我在下方手動按兩次 Execute Tool。原始 Copy trace 包含中文 prompt、Agent
自行取得活動 ID、兩次 save_event、兩份 Tool result,以及最後的自然語言回答。因此,
REPEAT-01 能證明這次對話裡真的發生過重複呼叫。

畫面上方雖然還留著 Inspector 預設的英文示例句,Copy trace 裡的
userPrompt.message 只有本題中文,判讀時要以原始 JSON 為準。

自動測試回答的是:「即使程式被重複呼叫,系統仍會安全處理。」Agent trace 補上的則是:
「模型在這次對話中,真的選了這支 Tool,而且真的呼叫兩次。」

依前面約定的證據分級,前者屬於 deterministic 的 E2,這份 Inspector trace 則能證明本次
Agent invocation,也就是 E4。不過它沒有公開網址、commit 與 revision,無法單獨綁定某個
不可變部署版本,更不能拿來宣稱已完成乾淨環境重播的 E5。成功畫面有了,證據的邊界還是
要照實寫。

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

你可能已經注意到:畫面可以 Undo,五支正式 Tool 裡卻找不到 unsave_event

這是我刻意留下的產品邊界。收藏後,人類可以立刻從可見介面復原;但 catalog 不需要把
每一個反向操作都公開給 Agent。每多一支 Tool,就會多一組選擇、授權與測試情境。前幾天
確定五支 Tool 的 feature boundary 後,我不打算為了讓清單看起來更豐富,再偷偷塞進第六支。

目前的設計已經足以驗證低風險寫入的三件事:狀態屬於 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 應該重試還是停下來?
系列文
別再讓 Agent 猜按鈕:30 天打造並實測 Agent-ready 的 WebMCP 活動網站19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言