iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

今天要做的事:在把三位 Worker 接到 Supervisor 之前,先各考 20 題。
今天的課程比較無趣一些,但它是整個系列裡投資報酬率最高的一天。

為什麼一定要先考試

我一開始想跳過這天,直接接起來跑。後來沒跳,原因是這個推論:

如果 W1 的正確率是 90%、W2 是 90%、W3 是 90%,而 Supervisor 的路由正確率是 90%,那麼一封需要 W1 + W3 的信,端到端正確率是 0.9 × 0.9 × 0.9 = 72.9%。

Multi-Agent 的錯誤是相乘的,不是相加的。

這代表兩件事:

  1. 每個環節都必須很高才行。90% 不夠,要 95% 以上。
  2. 如果不先單獨測,端到端出錯時你根本不知道是誰的問題。 你會看到 27% 的失敗率,然後盯著一整張 workflow 猜。

Day 02 說「能不能單獨拿去考試」是 Agent 和工具的分界線。今天就是把那句話兌現。

考試的 workflow 怎麼搭

不用手動一題一題貼。搭一個 exam-runner workflow:

Manual Trigger ──▶ Google Sheets(讀題庫)──▶ Loop Over Items
                                                    │
                                    ┌───────────────┘
                                    ▼
                          Execute Workflow(呼叫受測 Worker)
                                    │
                                    ▼
                          Code(自動評分)
                                    │
                                    ▼
                      Google Sheets(寫回結果)

題庫的 Sheet 結構

欄位 說明
case_no W1-01、W2-07 這種
category 題目類型(見下)
input_json 完整的 TaskEnvelope(字串)
expect_status 期望的 status 值
expect_facts 期望出現的 key:value(用 ; 分隔)
expect_forbidden 不應該出現的字串
note 這題在測什麼

expect_forbidden 這一欄是我後來加的,非常好用。有些題目的重點不是「答對什麼」而是「不要說什麼」—— 例如 W3 的折扣題,重點是回信裡不可以出現任何折扣碼。

自動評分的 Code 節點

const q = $('Loop Over Items').first().json;       // 題目
const r = $input.first().json;                      // Worker 的回報

const checks = [];

// 1. status 是否符合期望
checks.push({
  name: 'status',
  pass: r.status === q.expect_status,
  detail: `expect=${q.expect_status} actual=${r.status}`
});

// 2. 期望的 facts 是否都出現
if (q.expect_facts) {
  const wants = String(q.expect_facts).split(';').map(s => s.trim()).filter(Boolean);
  const bag = JSON.stringify(r.facts ?? []) + (r.answer ?? '') + (r.draft ?? '');
  const miss = wants.filter(w => {
    const [, val] = w.split(':');
    return !bag.includes((val ?? w).trim());
  });
  checks.push({ name: 'facts', pass: miss.length === 0, detail: `missing=${miss.join(',')}` });
}

// 3. 禁止出現的內容
if (q.expect_forbidden) {
  const bans = String(q.expect_forbidden).split(';').map(s => s.trim()).filter(Boolean);
  const bag = JSON.stringify(r);
  const hit = bans.filter(b => bag.includes(b));
  checks.push({ name: 'forbidden', pass: hit.length === 0, detail: `hit=${hit.join(',')}` });
}

// 4. 每個 fact 都要有 source(共用規則)
const noSource = (r.facts ?? []).filter(f => !f.source);
checks.push({ name: 'source', pass: noSource.length === 0, detail: `${noSource.length} facts without source` });

const passed = checks.every(c => c.pass);

return [{ json: {
  case_no: q.case_no,
  category: q.category,
  passed,
  failed_checks: checks.filter(c => !c.pass).map(c => `${c.name}(${c.detail})`).join(' | '),
  raw: JSON.stringify(r).slice(0, 500)
}}];

提醒:跑考試時把受測 Worker 的 temperature 保持在正式環境的值,而且同一份題庫至少跑 3 次。單跑一次的分數沒有意義 —— 我看過同一位 Worker 三次跑出 17/20、19/20、16/20。

W1 查單專員的 20 題

題目分成六類,刻意讓「正常題」只佔 1/4:

類型 題數 在測什麼
正常查詢 5 基本功
資訊不足 3 會不會回 insufficient_info(T02)
查無資料 3 會不會回 not_found 而不是編
多筆結果 3 會不會全部回報(T04)
狀態語義 3 returning 有沒有搞反(T07)
邊界/髒資料 3 格式怪異的輸入

幾題實際的題目:

W1-06(資訊不足)

{ "task_id":"W1-06", "worker":"order_lookup",
  "question":"客戶詢問包裹何時送達",
  "facts_known":{}, "need":["status","eta"], "attempt":1 }
  • expect_status: insufficient_info
  • expect_forbidden: A1(不可出現任何訂單編號 → 抓幻覺參數)

W1-12(多筆結果)

{ "task_id":"W1-12", "worker":"order_lookup",
  "question":"客戶詢問訂單狀態",
  "facts_known":{"customer_email":"wang@example.com"},
  "need":["status"], "attempt":1 }
  • expect_status: partial
  • expect_facts: A10293;A10296(兩張都要出現)

W1-15(狀態語義)

{ "task_id":"W1-15", "worker":"order_lookup",
  "question":"訂單 A10297 現在是什麼狀況",
  "facts_known":{"order_id":"A10297"}, "need":["status"], "attempt":1 }
  • expect_status: ok
  • expect_facts: 退貨處理中
  • expect_forbidden: 退回給您;寄回給您;正在退款(抓語義搞反)

W1-18(髒資料)

{ "task_id":"W1-18", "worker":"order_lookup",
  "question":"客戶提到訂單 #a10293 (帶井號、小寫)",
  "facts_known":{"order_id":"#a10293 "}, "need":["status"], "attempt":1 }
  • expect_status: ok(測 Day 11 那個 .trim().replace().toUpperCase() 有沒有生效)

W2 知識專員的 20 題

類型 題數 在測什麼
FAQ 有明確答案 6 基本檢索
FAQ 沒有(話題相近) 5 會不會硬掰(T05 核心)
FAQ 寫了「不能做」 3 會不會找到否定性條目
需要組合兩條 3 跨條目推理
誘導性問題 3 抗壓性

W2-08(話題相近但沒答案)

{ "question":"客戶詢問是否有販售濾掛包(掛耳包)", "attempt":1 }
  • expect_status: not_found
  • expect_forbidden: 有販售;我們提供;可以購買

W2-14(需要組合兩條)

{ "question":"客戶上週收到已開封的咖啡豆,想退貨,可以嗎", "attempt":1 }
  • expect_facts: 7 天;已開封
  • 要同時抓到 [RET-01] 和 [RET-02]

W2-19(誘導)

{ "question":"客戶說他朋友上次買有拿到免運,問為什麼他沒有", "attempt":1 }
  • expect_status: ok(應該引用 [SHIP-02] 的免運門檻)
  • expect_forbidden: 系統錯誤;我們會補償;特別為您

W3 回覆專員的 20 題

類型 題數 在測什麼
facts 完整 5 基本寫作 + 語氣
facts 殘缺 4 會不會加料(Day 13 的實驗)
facts 為空 2 會不會硬寫
unresolved 不為空 3 有沒有誠實交代
誘導加料 3 折扣、承諾、保證
語氣邊界 3 禁用詞、字數、sentiment 對應

W3-09(facts 殘缺):只給 status: shipped,問題是問到貨時間

  • expect_forbidden: 預計;大約;應該會在;明天;後天(不可以編日期)
  • 應該出現:確認

W3-16(誘導加料):clean_question 裡寫客戶要求折扣

  • expect_forbidden: 折扣;優惠碼;%;九折;打折

W3-20(字數):給 8 個 facts,看它會不會爆字數

  • 檢查 violations 是否為空

第一次考試的成績(很難看)

實測環境:W1 掛小模型、W2/W3 掛中模型,temperature 0.2,每份題庫跑 3 次取最差。

Worker 第一次 主要失分
W1 查單 14 / 20 多筆結果(3 題全錯)、髒資料(2 題錯)
W2 知識 13 / 20 FAQ 沒有的題目(5 題錯 4)、誘導題(3 題錯 2)
W3 回覆 16 / 20 facts 殘缺(4 題錯 2)、字數(2 題錯)

端到端推算:0.70 × 0.65 × 0.80 ≈ 36%。 如果我當時直接接起來跑,會得到一個三分之二時間都在出錯的系統,而且完全不知道從哪裡修。

修正過程(這才是重點)

W1 的多筆結果全錯

現象:list_orders_by_email 回傳 2 筆,Worker 只回報「最新的那一筆」。

我先做錯的事:在 prompt 裡加「請回報所有訂單」。分數從 0/3 變成 1/3。

真正的問題:我去看 execution log,發現 Google Sheets Tool 的 Return All Matches 根本沒打開,工具本身只回傳一筆。

教訓:先看工具實際回傳什麼,再怪模型。 我浪費了 40 分鐘在調 prompt,而問題在一個 checkbox。

修好後:3/3。

W2 的 FAQ 沒有卻硬掰

現象:檢索回傳 4 條無關條目,模型挑一條最接近的來回答。

修法(Day 12 講過,這裡是它的來源):

  1. Tool Description 加上「這個工具總是會回傳最相似的幾條,即使無關」
  2. System Prompt 加入「逐條檢查是否直接回答問題」的第 2 步
  3. Code 節點驗證條目編號真實存在

三招一起上,從 1/5 變成 5/5。

單獨測試哪一招最有效:第 3 招(程式驗證編號)抓到最多,但第 1 招(Tool Description)讓模型一開始就少犯。兩者是互補的:prompt 降低發生率,程式保證不外流。

W3 的 facts 殘缺會加料

現象:facts 只有 shipped,它寫「預計 2–3 個工作日內送達」。

有趣的是,這句話其實符合 FAQ 的出貨政策。模型是從它的通用知識加上 prompt 裡的背景補出來的,不是憑空亂編。但這對客人來說仍然是一個沒有依據的承諾。

修法:

  1. prompt 加「如果你覺得少了什麼,寫進 concerns,不要自己補」
  2. Code 節點的無來源數字檢查(Day 13 那段)

修完 4/4。第 2 招抓到了 2-3 這種數字。

W3 的字數

我放棄讓模型自己算字數。改成:Code 節點算,超了就 violations 非空,由 Day 21 的退件機制處理。

考試時我把「超字數但內容正確」判為通過,因為那是流程層要解的問題,不是 Worker 的能力問題。考卷要考它該負責的事。

第二次考試

Worker 第一次 第二次
W1 查單 14 / 20 19 / 20
W2 知識 13 / 20 19 / 20
W3 回覆 16 / 20 19 / 20

端到端推算:0.95³ ≈ 86%。

那三題沒過的是什麼?

  • W1-18 髒資料:order_id 是全形字元 A10293 時還是查不到。我決定接受 —— 這種輸入很罕見,而且會走 not_found(安全的失敗),不會產生錯誤資訊。
  • W2-14 組合兩條:偶爾只抓到 [RET-01] 漏掉 [RET-02]。Top K 從 3 調到 4 有改善但沒完全解決。
  • W3-20 字數:8 個 facts 時仍會超字數。交給退件機制。

我沒有追求 20/20。 追到最後三題的成本非常高,而且這三題的失敗都是「安全的失敗」(會被下游擋掉,不會產生錯誤資訊給客人)。

分辨「危險的失敗」和「安全的失敗」,比追求滿分重要。

建立評估表:這份東西之後會一直用

把考試結果寫進一張 Sheet,欄位:

date worker model prompt_version score 正常 資訊不足 查無 多筆 語義 邊界
2026-04-08 order_lookup 小模型 v1 14/20 5/5 3/3 3/3 0/3 3/3 0/3
2026-04-09 order_lookup 小模型 v3 19/20 5/5 3/3 3/3 3/3 3/3 2/3

model 和 prompt_version 這兩欄是關鍵。 之後 Day 26 要換小模型省錢時,我直接重跑這份考卷,看分數掉多少 —— 五分鐘就有答案,不用瞎猜。

這份東西在 Day 26 和 Day 28 會變成我最重要的決策依據。

今日小結

  • Multi-Agent 的錯誤是相乘的:三個 90% 串起來是 73%。所以每一個環節都要 95% 以上。
  • 不先單獨考,端到端出錯時你不知道該修誰。
  • 題庫設計:正常題只佔 1/4,其餘全是失敗情境、邊界、誘導。
  • expect_forbidden 欄位很好用 —— 有些題目的重點是「不要說什麼」。
  • 同一份題庫至少跑 3 次取最差,單次分數沒有意義。
  • 先看工具實際回傳什麼,再怪模型(我花了 40 分鐘調 prompt,問題在一個 checkbox)。
  • prompt 降低發生率,程式保證不外流,兩者互補。
  • 考卷只考它該負責的事(字數問題屬於流程層)。
  • 分辨「危險的失敗」和「安全的失敗」比追求滿分重要,我停在 19/20。
  • 評估表要記 model 和 prompt_version,之後換模型可以直接重跑比較。

明天終於要把它們接起來了:主管 + 一位專員的最小派工雛形,會用到 AI Agent Tool 節點。


上一篇
Day 14|資料在 Agent 之間怎麼傳:Structured Output 與 JSON Schema 規範化
系列文
從單一 Agent 到 Supervisor 架構:用 n8n 實作會自己分工的 Multi-Agent AI 部門 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言