安安~我是ChiYu~
昨天先把網站範圍鎖定在五支 Tool 與三條活動流程,也留下可以重播的正式搜尋基線。今天
不再增加功能,而是正式驗收第一段能力:搜尋活動。
這裡最重要的不是讓 Agent 多一條專用捷徑,而是讓人類表單與 search_events 共用同一套
搜尋規則。人類選擇台北、免費、入門,Agent 用自然語言提出相同條件,兩邊最後都應該得到
同一個結果。
如果 UI 與 Tool 各寫一套篩選邏輯,網站很快就會出現兩個版本的答案:畫面找得到,Agent
找不到;或者 Agent 為了湊出結果,偷偷把使用者指定的條件放寬。
所以今天先拆開這條共用搜尋路徑,再用三種不同說法檢查模型是否正確保留條件。第一次結果
並不完美,但失敗會留在後面的實測段落說明,不先拿它當成讀者理解全文的門檻。
如果你昨天已經停在 v3-day-14,今天不用再切一次;單篇閱讀的讀者,請先準備這個版本:
git switch --detach v3-day-14
npm ci
npm run dev
這個里程碑已經包含正式搜尋頁、人類表單與 search_events 的共用路徑。今天沒有新的 source
delta,工作重點是拿同一版程式依序做 UI 搜尋、deterministic tests 與 Inspector 題目。先把
版本固定,後面一題通過、一題失敗、另一題需要重測時,才不會連自己究竟在考哪份程式都說不清楚。

圖 1:UI 使用台北、免費、入門三個條件,回傳 2027 年 1 月 23 日的 WebMCP 入門工作坊。
搜尋頁保留原本可見表單,再加入 WebMCP metadata:
<form
id="event-search"
toolname="search_events"
tooldescription="依關鍵字、地點、費用與程度搜尋目前公開活動。"
toolautosubmit>
<!-- query、location、price、level -->
</form>
toolname 提供穩定能力名稱,tooldescription 說明適用任務,欄位的 name、label、enum 與 constraint 共同形成 input schema。它們不應描述「點藍色按鈕」,因為 UI 外觀不是能力契約。
人類 submit 與 Agent submission 都會進入同一個 eventActions.search():
async search(query, context) {
const result = await dependencies.search(query);
recordActivity("search_events", context.mode, "SUCCESS");
return {
count: result.events.length,
events: result.events
};
}
共用 action 再呼叫 GET /api/events。因此 enum validation、公開活動篩選與結果格式只有一份來源;UI 與 Tool 不會因改版各自漂移。
成功回傳不是整頁 HTML,也不是資料表全部欄位:
{
"count": 1,
"events": [
{
"id": "evt-webmcp-intro",
"url": "/events/evt-webmcp-intro",
"title": "WebMCP 入門工作坊",
"startsAt": "2027-01-23T10:00:00+08:00",
"location": "taipei",
"price": "free",
"level": "beginner"
}
]
}
id 是後續詳情查詢的 opaque key,url 讓 Agent 能把使用者帶到可見頁面。其餘只保留這次搜尋需要的公開摘要。
今天對應三個案例:
| Case | 問法/變化 | 要檢查什麼 | 目前結果 |
|---|---|---|---|
| SEL-01 | 找台北、免費、入門活動 | 是否選 search_events |
Agent trace 通過 |
| ARG-01 | 找高雄、付費、進階,關鍵字 Agent | 是否完整保留四個搜尋條件 | 初次失敗;revision 0000008 的 Attempt 2 通過 |
| ARG-02 | 只搜尋 WebMCP,不限制地點與費用 | 是否只傳關鍵字,不發明其他條件 | 歷史證據不足;revision 0000008 的 Attempt 2 通過 |
三題的 Inspector trace 檢查的是 Agent 是否選對 Tool、是否保留參數,屬於 E4 行為
證據;共用 search action、server 篩選與 UI parity 仍由 E2 測試負責。不同 Attempt
來自不同固定版本,不能把後來兩次通過倒填成第一次就 3/3。

圖 2:SEL-01 保留 prompt、Tool input、Tool result 與 AI result。
SEL-01 證明 Agent 在固定公開版本中選對 Tool,並送出 taipei、free、beginner。另外兩題先留下失敗與證據不足,之後才在新的固定 revision 各重測一次。前後結果都保留。判讀時要同時確認三件事:有沒有呼叫 Tool、條件是否完整,以及結果是否來自同一個版本。

圖 3:黑色 trace 區才是本題證據;上方輸入框是尚未送出的另一段文字,不屬於這次執行。
第一次呼叫完整保留 Agent、kaohsiung、paid、advanced,但收到空陣列後,Agent 又連續呼叫三次,依序移除程度、地點與其他限制:
四個條件 → 移除程度 → 只留關鍵字 → 只留地點
這次歷史執行判定為 failed。search_events 對每次輸入都正確回傳空結果,問題出在
Agent 未經使用者同意便改變搜尋條件。較安全的行為應是先回報沒有完全符合的活動,
再詢問是否允許放寬條件。

圖 4:input 只帶 query: "WebMCP",空字串代表不限制地點與費用;截圖沒有網址與版本座標,因此只能作為輔助證據。
這次 Agent 沒有發明非空條件,但 Inspector 回傳 count: 0;重新查詢目前公開版 API,則能找到 WebMCP 入門工作坊。在還沒確認頁面、Tool registry 與 API 是否屬於同一個 release 前,不能把它算成通過,也不能直接歸咎於模型。
因此這份歷史證據只能標記為 rerun_required。重跑時必須把網址、/health 版本座標與完整 Copy trace 一起保存。
前兩題的初次結果都留在原位。後來我把「零筆結果不得自行放寬」寫進 result guidance,
也把網站、/health 與 Inspector trace 綁到 revision 0000008,再各重測一次。
Attempt 2 使用完全相同的四個條件,只呼叫一次 search_events。Tool 回傳零筆後,
Agent 沒有自行移除條件,而是把決定權交還給使用者:

圖 5:query、location、price、level 四個參數完整保留,且 trace 沒有第二次放寬搜尋。
這次改變不是再補一句 prompt,而是讓零筆結果把決策邊界說清楚。Tool result 除了回傳appliedFilters,也明確標示 constraintsRelaxed: false、requiresUserDecision: true,並在nextAction 要求先詢問使用者。Agent 因此知道四個條件仍然有效,下一步是停下來問,
不是自行改寫搜尋條件。
revision 0000008 的 Attempt 2 補齊了這些條件。Agent 只傳query: "WebMCP",location 與 price 維持空字串,也沒有發明 level;Tool 找到一筆evt-webmcp-intro,全程沒有呼叫詳情或寫入 Tool。

圖 6:單次呼叫的 input、Tool result 與最終回答一致,ARG-02 在這個固定 revision 判定通過。
兩份文字紀錄是依截圖逐行轉錄的 reconstructed trace,不冒充 Inspector 原始匯出檔:
若 human submit 自己過濾資料,Tool execute 又維護另一份條件轉換,按鈕與 Agent 很快會得到不同結果。更糟的是,兩邊都可能各自「測試通過」。
共用 use case 的原則是:
UI adapter ┐
├→ search action → API → server validation
Tool adapter┘
差別只在 context.mode 用來記錄操作來源,不應改變搜尋規則。
第一份成績不好看,卻把兩件事講得很清楚:UI 與 Tool 必須共用規則,模型是否守住
條件則要另外驗。搜尋 result 現在已帶回 opaque event ID,明天就進詳情頁問一句更像
人話的:「我現在看的活動是哪一場?」接著再切走頁面,看看舊 Tool 會不會識相地下班。