iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
AI 自動化

從單一 Agent 到 Supervisor 架構:用 n8n 實作會自己分工的 Multi-Agent AI 部門系列 第 20 篇

Day 20|接力任務:一封信需要三位 Worker 輪流處理時怎麼編排

  • 分享至 

  • xImage
  •  

到目前為止,主管派工都是一次決定好:這封信要查訂單、要查 FAQ,或兩個都查。兩位專員拿到同一個問題,各查各的,結果丟給回覆專員。

有一類信這樣處理不了。

A10295 的豆子喝起來太苦了,不太喜歡,想退。

要回答這封信,得知道三件事:這張單什麼時候送達的、退貨期限是幾天、以及今天距離送達過了多久。前兩件分別在訂單表和 FAQ 裡,第三件誰都沒有——它必須把前兩件算在一起才會出現。

今天做接力:W1 先查,中間插一個計算步驟,再把算好的東西交給 W2 和 W3。

平行派工漏掉的那封信

先看現況。Day 18 的 Switch 版跑這封信,草稿是這樣:

陳大文 您好

讓您等待確實不好意思。關於訂單 A10295 的哥倫比亞 慧蘭 中深焙 400g,若咖啡豆未開封且外包裝完整,且在到貨 2026-08-23 起 7 天內,您可以申請退貨。但若咖啡豆已開封,則不接受退貨或換貨。

祝您有個美好的一天,
好豆選物 客服小豆

沒有錯誤資訊,到貨日是對的、政策也是對的。但它把判斷丟回給客人:你自己算算看有沒有超過 7 天,自己想想有沒有開封。

實際上系統知道答案。送達日是 8/23,信是 9/14 寄來的,已經過了 22 天,這封信的答案就是不能退。但沒有任何一個環節負責做這個減法:W1 只回報它查到的欄位,W2 只回報 FAQ 原文,W3 被明文禁止使用 facts 以外的資訊——它連「22」這個數字都不該自己算出來。

加一個推論步驟

Day 09 定契約的時候,derived 這個欄位就在裡面了,意思是「主管算出來的結果」。但從 Day 16 到 Day 19,我傳給 W3 的 derived 一直是空陣列,因為沒有人去填它。

今天把流程改成這樣:分類完先派 W1,拿到訂單資料之後進一個 Code 節點算推論,再派 W2,最後合併交給 W3。兩種做法的差別如下圖。

https://ithelp.ithome.com.tw/upload/images/20261006/20183868svEtVk0iEQ.png

推論節點做三件事:

const today = String(email.received_at ?? new Date().toISOString()).slice(0, 10);
const days = (from, to) => Math.round((Date.parse(to) - Date.parse(from)) / 86400000);

const derived = [];
const delivered = get('delivered_at');
if (delivered && /^\d{4}-\d{2}-\d{2}$/.test(String(delivered.value))) {
  const n = days(String(delivered.value), today);
  derived.push({ key: 'days_since_delivery', value: String(n), basis: `今天 ${today} 減去到貨日 ${delivered.value}` });
  derived.push({
    key: 'within_7day_window',
    value: n <= 7 ? 'true' : 'false',
    basis: `到貨 ${n} 天,退貨期限為到貨 7 天內`,
  });
}

const items = get('items');
const EQUIP = ['磨豆機', '濾杯', '手沖壺', '壺', '濾紙', '器材', '聰明'];
if (items) {
  const isEquip = EQUIP.some((k) => String(items.value).includes(k));
  derived.push({ key: 'item_category', value: isEquip ? 'equipment' : 'beans', basis: `依品項「${items.value}」判斷` });
}

每一筆推論都帶一個 basis,寫清楚是怎麼算出來的。這跟 facts 要有 source 是同一個原則:事實要能追到來源,推論要能追到算式。之後驗收或人工審核時,看到「已超過 7 天」才有辦法確認這個結論對不對。

「今天」刻意取客人來信的時間,不是取執行當下的時間。這樣同一封信重跑一百次,算出來的天數都一樣,考卷才有意義。

順便改寫給 W2 的問題

既然 W1 已經回報了,那 W2 的問題就不該跟 W1 的一樣。推論節點最後會把問題補上條件:

const parts = [c.clean_question];
if (cat) parts.push(cat.value === 'equipment' ? '(此案的商品是器材類)' : '(此案的商品是咖啡豆類)');
if (win) parts.push(win.value === 'true' ? '(仍在到貨 7 天內)' : '(已超過到貨 7 天)');

原本問「客戶想退貨,詢問是否可以退」,現在問「客戶想退貨,詢問是否可以退(此案的商品是咖啡豆類)(已超過到貨 7 天)」。檢索要找的條目從一堆退貨政策縮到咖啡豆那幾條。

這就是接力的實際意義:不是「依序執行」,是後面的人知道前面的人查到什麼。

Day 14 鎖的 key,到今天才用上

推論節點裡那行 get('delivered_at'),要能取到值,前提是 W1 回報的 key 真的叫 delivered_at。

Day 11 那版 W1 的 key 是自由填的,20 張工單生出 16 種寫法,送達日可能叫「送達日」「到貨日期」「訂單 A10295 送達日」。這種情況下沒辦法寫 get('delivered_at'),只能用模糊比對,而模糊比對遲早會比對錯。

所以今天順便把主線的 W1 換成 Day 14 做的 enum 版本:key 限定在 12 個值、多一個 subject 欄位區分多筆訂單、守門員多一條「查不到就不該有事實」。Day 14 那篇最後我說「這筆錢划算」,當時只是推測,今天才看到它買到了什麼——一個可以安全地用 f.key === 'delivered_at' 取值的下游。

契約的價值通常不會在訂契約的那天顯現。

實測:8 封退貨信

評估集裡退貨類剛好 8 封,涵蓋器材保固、已開封想退、包裝破損、未開封想退、買錯尺寸想換、超過期限想退、問退款何時到帳、退貨想改成換貨。

平行派工 接力派工
完整通過 7/8 7/8
部分通過 1 1
危險錯誤 0 0
tokens 中位數 8912 9216
延遲 p50 7.4 秒 6.7 秒
延遲 p95 10.3 秒 7.6 秒

總分一樣。接力修好了它要修的那一封,但弄壞了另一封。

修好的那封

一開始那封「豆子太苦想退」,接力版的草稿變成:

陳大文 您好

讓您等待確實不好意思。關於訂單 A10295 的哥倫比亞 慧蘭 中深焙 400g,咖啡豆退貨條件為未開封且外包裝完整,並在到貨 7 天內申請;已開封的咖啡豆不接受退貨或換貨。由於您的訂單於 2026-08-23 送達,目前已超過 7 天的申請期限,因此無法為您辦理退貨。

祝您有個美好的一天,
好豆選物 客服小豆

從「你自己算算看」變成「已超過期限,無法辦理」。客人收到這封信就知道結果了,不用再寫一封信來問。

弄壞的那封

A10297 我之前申請退貨了,但我想改成換一包新的就好,可以嗎?

這封的標準答案是「我幫您確認後回覆」,因為 FAQ 裡根本沒寫「退貨能不能改成換貨」。平行版答對了。

接力版的草稿:

關於訂單 A10297 的瓜地馬拉 安提瓜 中焙 200g,目前狀態為退貨處理中。由於咖啡豆的退貨期限為到貨 7 天內,而您的訂單於 2026-09-02 送達,目前已超過此期限。

客人問的是「能不能把退貨改成換貨」,回信在講退貨期限。

原因是推論節點不分青紅皂白,看到有 delivered_at 就算了 within_7day_window = false,然後 W3 看到這個醒目的推論,就拿它來回答一個它根本不適用的問題。這封信的退貨申請早就送出去了,7 天期限跟客人的問題沒有關係。

平行版沒有這個問題,純粹因為它沒算這個推論。不是它比較聰明,是它比較無知。

推論是有代價的

這件事讓我重新想了一下 derived 的定位。

facts 是查到的,有來源,不會錯。推論是算出來的,而算式本身帶著一個前提——「退貨期限」這個推論的前提是「客人正在問能不能退貨」。前提不成立的時候,推論仍然是正確的計算,但它變成了誤導。

W3 分不出這個差別。它看到的 derived 跟 facts 長得一樣,都是 key、value,而且還多一個看起來很權威的 basis。

兩個方向可以修:推論節點依 intent 決定要算什麼,或是給每一筆推論加一個適用條件讓下游判斷。我傾向前者,因為把判斷留給 W3 等於又把問題丟回給模型。不過這兩個我今天都還沒做,明天做驗收機制的時候,這種「用了不該用的推論」正好是驗收該抓的東西之一。

另外兩個發現

延遲沒有變差,而且更穩。 我原本以為接力一定比較慢,畢竟 W1 跑完才輪到 W2。實際上 p50 從 7.4 秒變 6.7 秒、p95 從 10.3 秒變 7.6 秒。原因是 n8n 的兩條分支本來就不是真的平行執行,合併還要等兩邊都好;接力的流程反而單純。這個差距在 8 封信的樣本上我不敢說有多可靠,但至少「接力一定比較慢」這個直覺是錯的。

我又犯了一次 Day 18 的錯。 新 workflow 的最後一個節點,我寫成「needs_human 為真就 escalate」,結果 8 封退貨信全部變成不寫草稿,第一次跑出來 0/8。退貨的 needs_human 意思是「草稿要人審」,不是「不要寫草稿」——這件事我兩天前才寫過一篇,換個 workflow 又踩一次。

小結

接力的重點不是執行順序,是中間那個把兩邊資料算在一起的步驟。推論放進 derived,每一筆要帶算式,「今天」要取來信時間而不是執行時間。接力讓那封「超過退貨期限」的信從模稜兩可變成明確答案,但也讓另一封被不相關的推論帶偏,8 封總分打平。推論節點目前不管意圖是什麼都照算,這是它會誤導下游的原因。

明天做驗收與退件:草稿寫好之後,誰來檢查它有沒有用錯資料、漏答問題,以及檢查不過的時候要怎麼退回去重做。


上一篇
Day 19|派工失敗長什麼樣:先把 trace log 做出來,再看 175 次派工的分布
下一篇
Day 21|驗收與退件:Supervisor 驗收成果並觸發重做的 Feedback Loop
系列文
從單一 Agent 到 Supervisor 架構:用 n8n 實作會自己分工的 Multi-Agent AI 部門 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言