iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Modern Web

WebMCP:30 天打造 AI Agent 看得懂、也操作得動的網站系列 第 15 篇

Day 15|AI 可以直接幫你下單嗎?我把敏感操作拆成 Preview → Confirm → Execute

  • 分享至 

  • xImage
  •  

本篇重點

Agent 有能力執行,不代表網站應該允許它直接執行。搜尋商品和刷卡付款只差幾個 Tool Calls,但風險完全不同。

今天建立一個很實用的模式:

Preview
→ Confirm
→ Execute

並搭配 WebMCP 的 consequentialHint,讓高風險操作有明確語意。

📸 圖片 1|Preview → Confirm → Execute 三階段流程
https://ithelp.ithome.com.tw/upload/images/20260924/20121296kwkaI5AfLQ.png

先分兩個任務

任務 A

幫我找 2000 元以下的耳機。

失敗代價:很低。

任務 B

幫我買第二個。

可能造成:

  • 扣款。
  • 建立訂單。
  • 寄送商品。
  • 暴露地址。
  • 無法立即撤銷。

所以「Agent 已經知道使用者想買」還不夠。

最危險的 Tool

await document.modelContext.registerTool({
  name: 'buy_product',
  description: 'Buy a product.',
  // ...
  execute: async ({ productId }) => {
    return checkout(productId);
  }
});

一個 Tool 同時:

選商品
確認數量
確認地址
確認付款方式
真的扣款

如果 Agent 理解錯 product ID,就直接產生真實後果。

拆成 prepare_checkout

本日 Demo 用本地記憶體模擬結帳,不會實際扣款或寄送商品,重新整理就會清空資料。商品 ID 為 123,單價 NT$1,680,運費 NT$60。

以下擷取 app.js 的 Tool 註冊與主要流程;prepareCheckout()、confirmCheckout() 與畫面更新由 Demo 實作,並不是 WebMCP 內建的方法。prepareCheckout() 檢查商品與數量後,產生有效 10 分鐘的 previewId,保存明細並更新預覽;建立新預覽會取代尚未完成的舊預覽。

await document.modelContext.registerTool({
  name: 'prepare_checkout',
  description: 'Prepare a checkout preview. This does not place an order or charge the user.',
  inputSchema: {
    type: 'object',
    properties: {
      productId: { type: 'integer', minimum: 1 },
      quantity: { type: 'integer', minimum: 1, maximum: 10 }
    },
    required: ['productId', 'quantity']
  },
  annotations: {
    readOnlyHint: false,
    consequentialHint: false
  },
  execute: async ({ productId, quantity }) => {
    const preview = prepareCheckout({ productId, quantity });
    return JSON.stringify(preview);
  }
});

以數量 1 為例,回傳的預覽包含以下欄位(previewId 與有效期限依實際執行產生):

{
  "status": "preview_ready",
  "simulated": true,
  "previewId": "preview_abc123",
  "product": "Wireless Headphones 無線耳機",
  "quantity": 1,
  "subtotal": 1680,
  "shipping": 60,
  "total": 1740,
  "shippingAddress": "台北市中正區示範路 123 號(虛構地址)",
  "payment": "Demo Visa **** 1234(不扣款)",
  "expiresAt": "實際產生的 ISO 時間字串"
}

在 Inspector 呼叫 prepare_checkout,傳入 {"productId":123,"quantity":1},就會看到商品、數量、小計、運費、總額與收件資訊。此時總額為 NT$1,740,訂單數仍為 0。

這一步只建立「你即將做什麼」的可見結果,還沒有下單。

📸 圖片 2|Checkout Preview 顯示真正要確認的內容
https://ithelp.ithome.com.tw/upload/images/20260924/20121296CkG7UTOgT9.png

確認與執行交給另一個 Tool

接著把預覽回傳的 previewId 交給 confirm_checkout。Demo 的 confirmCheckout() 會先檢查預覽,再開啟網站自己的確認視窗,等待使用者選擇;呼叫 Tool 本身還不會建立訂單。

await document.modelContext.registerTool({
  name: 'confirm_checkout',
  description: 'Open the website confirmation dialog for a checkout preview. Wait for the user to confirm or cancel. Creates only a simulated order and never charges money.',
  inputSchema: {
    type: 'object',
    properties: {
      previewId: {
        type: 'string',
        description: 'Exact previewId returned by prepare_checkout.'
      }
    },
    required: ['previewId']
  },
  annotations: {
    readOnlyHint: false,
    consequentialHint: true
  },
  execute: async ({ previewId }) => {
    const result = await confirmCheckout({ previewId });
    return JSON.stringify(result);
  }
});

consequentialHint: true 用來標示可能造成重大或難以逆轉後果的操作,讓 Agent/Browser 可以據此要求確認。但它不保證瀏覽器一定顯示確認介面,也不會自動替網站產生商品明細。

本例雖然只模擬下單,仍用這個提示示範正式結帳 Tool 的語意。真正讓 Demo 暫停並等待選擇的,是網站實作的確認視窗。使用者可以核對商品、數量、NT$1,740 的總額、地址與付款摘要,再決定是否繼續。

但要注意:

Hint 是提示,不是你的唯一安全機制。
📸 圖片 3|網站顯示訂單明細,等待使用者確認
https://ithelp.ithome.com.tw/upload/images/20260924/20121296RI5VLLhRY0.png

確認後,才回傳執行結果

使用者按下「確認並模擬下單」後,Demo 會再次檢查預覽是否有效,再建立模擬訂單。頁面保留完成狀態,Tool 則回傳 status: success、simulated: true、orderId: DEMO-001 與 total: 1740;訂單數從 0 變成 1。

如果使用者按「取消」或 Escape,Tool 回傳 cancelled,不會新增訂單。等待選擇期間,Tool 尚未回傳最終結果是正常的。

📸 圖片 4|確認後完成模擬下單,回傳訂單結果
https://ithelp.ithome.com.tw/upload/images/20260924/20121296ELy8r0tb0P.png

同一個已完成的 previewId 再次呼叫時,Demo 只回傳原訂單,並標示 alreadyExecuted: true,避免重試產生第二筆訂單。尚未完成的預覽若已過期,則回傳 expired,需要重新建立預覽。

這些紀錄目前只保存在本頁記憶體中。正式網站需要把預覽、確認與防止重複下單的紀錄交由後端保存;前端視窗與 Tool description 都不能作為付款授權的安全邊界。

後端仍要確認:

  • preview 是否屬於目前使用者。
  • preview 有沒有過期。
  • 金額是否在 Server 重新計算後一致。
  • 庫存是否仍存在。
  • 使用者是否真的有付款權限。

用 Preview ID 綁定已確認的內容

如果 Confirm Tool 還接受:

{
  "productId": 999,
  "quantity": 20,
  "price": 1
}

那 Preview 就沒有意義。

我更喜歡:

prepare_checkout
→ Server 產生 previewId
→ User 看預覽
→ confirm_checkout(previewId)
→ Server 依 previewId 重新驗證

Confirm 階段只接受 previewId,就能避免重新傳入一組不同的商品或價格。後端仍須把這個 ID 綁定目前使用者、預覽明細與確認紀錄,拒絕未授權或已失效的請求;ID 本身不等於使用者已經同意付款。

哪些 Action 我會強制確認?

至少:

  • 付款/轉帳。
  • 建立正式訂單。
  • 預訂旅行/醫療/活動。
  • 刪除資料或帳號。
  • 公開發布內容。
  • 傳送不可撤回的重要訊息。
  • 修改安全設定。

而像:

  • Search。
  • Sort。
  • 加收藏。
  • 變更可撤銷 UI 狀態。

通常不需要同等級確認。

Human-in-the-loop 不是多一個「Are you sure?」

好的確認應該包含使用者真正需要看的內容:

商品:Wireless Headphones
數量:1
總額:NT$1,740
地址:台北市…
付款:Visa **** 1234

確認下單?

不是只問:

Agent wants to use confirm_checkout. Allow?

因為使用者真正想確認的是結果,不是 Tool Name。

可帶走的重點

  1. Agent 能做不代表應直接自動做。
  2. 高風險 Action 可以拆成 Preview → Confirm → Execute。
  3. consequentialHint 用來標示重大/不可逆操作。
  4. Hint 不能取代 Server-side 驗證。
  5. Confirm 階段最好使用 Server 產生的 preview / transaction token,而不是重新接受所有可變參數。
  6. 好的確認 UI 應讓使用者看到實際後果。

參考資料


上一篇
Day 14|AI 加入收藏兩次怎麼辦?Stateful Tool 最容易踩的 4 個坑
下一篇
Day 16|50 個按鈕做成 50 個 Tools?這是最容易讓 Agent 變笨的設計
系列文
WebMCP:30 天打造 AI Agent 看得懂、也操作得動的網站 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言