iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Modern Web

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

Day 08|搜尋不到算錯誤嗎?WebMCP Result/Error 的 4 種回傳設計

  • 分享至 

  • xImage
  •  

本篇重點

如果 Tool 找不到資料就 throw new Error('Not found'),Agent 很容易把「正常沒有結果」和「系統壞掉」混在一起。

今天把 Tool 結果拆成四種:successno_resultsinvalid_inputinternal_error。重點不是規定大家一定用這四個字,而是讓 Agent 可以分辨:任務狀態、使用者輸入問題、系統故障不是同一件事。

一個常見的壞寫法

execute: async ({ keyword }) => {
  const items = await searchProducts(keyword);

  if (!items.length) {
    throw new Error('No products found');
  }

  return JSON.stringify(items);
}

「沒有符合商品」其實可能是完全正常的搜尋結果。

例如:

找 100 元以下的 MacBook

結果是 0 筆,不代表資料庫掛掉。

我喜歡讓結果自己帶狀態

function ok(data) {
  return JSON.stringify({
    status: 'success',
    data
  });
}

function noResults(message) {
  return JSON.stringify({
    status: 'no_results',
    message
  });
}

搜尋 Tool:

execute: async ({ keyword, maxPrice }) => {
  const items = await searchProducts({ keyword, maxPrice });

  if (items.length === 0) {
    return noResults(
      'No products matched the current keyword and price filters.'
    );
  }

  return ok({
    count: items.length,
    items: items.slice(0, 10)
  });
}

這樣 Agent 可以自然回:

沒找到 100 元以下的 MacBook,要不要提高預算?

而不是:

工具執行失敗。

四種狀態怎麼分?

1. success

真的完成任務:

{
  "status": "success",
  "data": {
    "count": 2,
    "items": []
  }
}

2. no_results

查詢有效,只是沒有資料:

{
  "status": "no_results",
  "message": "No products matched the filters."
}

3. invalid_input

輸入在業務規則上不合理。

雖然 JSON Schema 可以擋一部分,但還是有 Schema 表達不了的跨欄位規則:

if (minPrice > maxPrice) {
  return JSON.stringify({
    status: 'invalid_input',
    message: 'minPrice must not be greater than maxPrice.'
  });
}

4. internal_error

真的系統錯誤,例如 API timeout、資料庫異常。

這類可以 throw,或轉成安全錯誤輸出;重點是不要把 stack trace、SQL、Token 等內部資訊直接送回 Agent

try {
  const items = await searchProducts(input);
  // ...
} catch (error) {
  console.error(error);

  return JSON.stringify({
    status: 'internal_error',
    message: 'Product search is temporarily unavailable.'
  });
}

📸 圖片 1|四種 Result 狀態的回傳格式
https://ithelp.ithome.com.tw/upload/images/20260917/20121296B7KXtLFQxe.png

Result 不要回整包後端 Response

假設 REST API 回:

{
  "id": 123,
  "name": "Keyboard",
  "price": 1200,
  "html_description": "<p>...</p>",
  "internal_flags": {},
  "seo": {},
  "debug": {},
  "metadata": {},
  "...": "很多資料"
}

Agent 其實可能只需要:

{
  "id": 123,
  "name": "Keyboard",
  "price": 1200,
  "url": "/products/123"
}

Chrome 的 WebMCP security guidance 目前也建議保持 Tool description 與 output 簡潔,因為 Tool 數量與輸出都會消耗 Agent context。

如果內容來自使用者,要標 untrustedContentHint

例如 Tool 回傳:

  • 商品評論
  • 論壇貼文
  • 使用者留言
  • 外部網站內容

這些內容可能藏著 Prompt Injection。

可以標示:

annotations: {
  readOnlyHint: true,
  untrustedContentHint: true
}

它不是萬靈丹,但至少把「這份資料不可信」這個語意交給 Agent/Browser。

完整範例

await document.modelContext.registerTool({
  name: 'search_products',
  description: 'Search public products by keyword and optional price range.',
  inputSchema: {
    type: 'object',
    properties: {
      keyword: { type: 'string', minLength: 1 },
      minPrice: { type: 'number', minimum: 0 },
      maxPrice: { type: 'number', minimum: 0 }
    },
    required: ['keyword']
  },
  annotations: {
    readOnlyHint: true
  },
  execute: async ({ keyword, minPrice, maxPrice }) => {
    if (
      minPrice !== undefined &&
      maxPrice !== undefined &&
      minPrice > maxPrice
    ) {
      return JSON.stringify({
        status: 'invalid_input',
        message: 'minPrice must not be greater than maxPrice.'
      });
    }

    try {
      const items = await searchProducts({ keyword, minPrice, maxPrice });

      if (items.length === 0) {
        return JSON.stringify({
          status: 'no_results',
          message: 'No products matched the current filters.'
        });
      }

      return JSON.stringify({
        status: 'success',
        data: {
          count: items.length,
          items: items.slice(0, 10).map(({ id, name, price, url }) => ({
            id, name, price, url
          }))
        }
      });
    } catch (error) {
      console.error(error);

      return JSON.stringify({
        status: 'internal_error',
        message: 'Product search is temporarily unavailable.'
      });
    }
  }
});

實際操作:用 Inspector 驗證四種回傳

以下使用本地 Day 08 Demo 的商品資料:Keyboard(1200 元)、Wireless Keyboard(2400 元)、Mouse(800 元)與 MacBook Air(34900 元)。價格單位為新台幣。執行前需備妥包含 searchProducts() 與示範資料的 app.js,以及顯示註冊狀態、Arguments、Result 和「模擬商品服務故障」開關的 index.html;上方程式碼主要展示 Tool 的註冊與回傳設計。

1. 開啟 Demo 與 Inspector

  1. 開啟本地 Demo 網頁並重新整理。
  2. 確認頁面顯示「✅ search_products 已成功註冊」。
  3. 開啟 WebMCP - Model Context Tool Inspector。
  4. 在 Tool 選單選擇 search_products
  5. 確認頁面上的「模擬商品服務故障」沒有勾選。

這次要觀察 Tool 的實際回傳格式,直接使用 Input Arguments → Execute Tool 即可,不需要透過自然語言呼叫。

2. 搜尋成功:success

在 Input Arguments 貼上:

{
  "keyword": "keyboard"
}

按下 Execute Tool,預期回傳:

{
  "status": "success",
  "data": {
    "count": 2,
    "items": [
      {
        "id": 123,
        "name": "Keyboard",
        "price": 1200,
        "url": "/products/123"
      },
      {
        "id": 124,
        "name": "Wireless Keyboard",
        "price": 2400,
        "url": "/products/124"
      }
    ]
  }
}

這表示搜尋完成,找到兩件符合名稱的商品。商品 URL 是示範路徑。

📸 圖片 2-1|搜尋成功,回傳符合的商品
https://ithelp.ithome.com.tw/upload/images/20260917/20121296KmQWNZTdPI.png

3. 沒有符合資料:no_results

將 Input Arguments 換成:

{
  "keyword": "MacBook",
  "maxPrice": 100
}

按下 Execute Tool,預期回傳:

{
  "status": "no_results",
  "message": "No products matched the current filters."
}

搜尋已正常完成,只是沒有 100 元以下的 MacBook。Agent 可以根據這個狀態建議調整條件。

📸 圖片 2-2|搜尋正常完成,但沒有符合資料
https://ithelp.ithome.com.tw/upload/images/20260917/2012129693TLwrYrzM.png

4. 輸入不符合業務規則:invalid_input

將 Input Arguments 換成:

{
  "keyword": "keyboard",
  "minPrice": 2000,
  "maxPrice": 1000
}

按下 Execute Tool,預期回傳:

{
  "status": "invalid_input",
  "message": "minPrice must not be greater than maxPrice."
}

兩個價格都是合法的非負數,但最低價格大於最高價格。這裡由 app.js 的業務規則檢查回傳 invalid_input。

📸 圖片 2-3|價格範圍不合理,回傳輸入錯誤
https://ithelp.ithome.com.tw/upload/images/20260917/201212968zuHvmwCvV.png

5. 額外測試:模擬 internal_error

  1. 勾選 Demo 頁面的「模擬商品服務故障」。
  2. 在 Inspector 輸入正常的搜尋參數:
{
  "keyword": "keyboard"
}
  1. 按下 Execute Tool,預期回傳:
{
  "status": "internal_error",
  "message": "Product search is temporarily unavailable."
}

這是由 Demo 刻意觸發的服務故障,用來確認錯誤處理分支會回傳安全訊息。詳細錯誤留在開發者 Console,回傳給 Agent 的結果不包含 stack trace、SQL 或 Token。

📸 圖片 2-4|模擬商品服務故障,回傳 internal_error
https://ithelp.ithome.com.tw/upload/images/20260917/20121296bP1ccEF9gs.png

  1. 測試完成後取消勾選,再用相同參數執行一次,應恢復 success。

可帶走的重點

  1. 沒資料不一定是 Error。
  2. Result 要讓 Agent 分辨正常結果、輸入問題與系統故障。
  3. 不要把完整後端 Response 原封不動送進模型。
  4. 不要把 stack trace、SQL、Token 等敏感除錯資訊回傳。
  5. 回傳 UGC/外部資料時,考慮 untrustedContentHint

參考資料


上一篇
Day 07|AI 為什麼老是填錯參數?用 inputSchema 把 Tool Calling 管起來
下一篇
Day 09|登入前後 Tools 不一樣怎麼辦?動態註冊 WebMCP Tool 實戰
系列文
WebMCP:30 天打造 AI Agent 看得懂、也操作得動的網站14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言