今天要做的事:解決昨天的三個問題 —— 自己寫回信、自己加事實、該派工時不派工。
這是我整個系列改最多次的一份 prompt(git log 顯示 23 次)。
昨天三個問題看起來不一樣,根本原因是同一個:
LLM 被訓練成「盡力完成使用者的請求」,而不是「扮演一個有職責邊界的角色」。
當你給它一封客服信,它的本能是把這件事辦好 —— 辦好的定義就是「寫出一封好的回信」。派工只是達成目的的手段之一,如果它覺得自己能直接寫,它就直接寫了。
所以問題不是「它不聽話」,而是你給它的目標和你要它做的事不一致。
解法方向有兩個,我兩個都用:
我原本的 prompt 寫「你的工作是判斷客戶來信的需求,並派工給合適的專員」。
這句話有個漏洞:模型會覺得最終目標還是「解決客戶的問題」,派工只是過程。
改成這樣:
## 你的成功標準
你的工作成果不是「客戶的問題被解決了」。
你的工作成果是「一份正確的案件分類與一組有來源的事實」。
具體來說,你這次執行算成功,當且僅當:
1. intent 分類正確
2. 該派的工都派了,不該派的沒派
3. collected_facts 裡的每一筆,都來自專員的回報,且都有 source
4. 你沒有寫任何一句給客戶看的話
回信會由回覆專員撰寫,不是你。
就算你認為自己可以寫得更好,也不要寫。
「你的工作成果不是客戶的問題被解決了」 這一句是關鍵。它直接把模型的目標函數改掉。
第 4 條「你沒有寫任何一句給客戶看的話」加上去之後,問題 1(自己寫回信)從 30% 降到 5% 左右。
抽象的禁令(「不要提供事實」)效果不好。給具體反例效果很好:
## 禁止事項(含實際案例)
### ❌ 禁止 1:把自己的知識放進 collected_facts
collected_facts 只能放專員回報給你的內容。
錯誤示範:
{ "key": "estimated_arrival", "value": "1-3 個工作日",
"source": "general_policy" }
這是你自己知道的,不是專員查到的。source 不是一個真實的資料位置。
正確做法:
如果你認為需要這項資訊,就派工去查。
如果查不到,放進 unresolved,不要自己填。
合法的 source 只有兩種格式:
orders!<訂單編號> (來自查單專員)
faq![<條目編號>] (來自知識專員)
其他任何格式的 source 都是錯的。
### ❌ 禁止 2:寫給客戶看的文字
summary 欄位是寫給系統和人類主管看的,不是給客戶看的。
錯誤示範:
"summary": "王先生您好,感謝您的來信。您的訂單已於 3/30 出貨…"
正確示範:
"summary": "訂單 A10293 已出貨,黑貓宅急便,預計 4/1 送達。客戶詢問到貨時間,資料完整。"
### ❌ 禁止 3:假裝派工
不可以在沒有實際呼叫工具的情況下,把 dispatched 填上專員名稱。
dispatched 必須反映你實際呼叫過的工具。
「合法的 source 只有兩種格式」 這一條特別有效,因為它把一個主觀判斷(「這算不算我自己加的」)變成一個客觀規則(「格式對不對」)。
而且這條規則程式也能檢查 —— 這就接到修正 3。
Prompt 只能降低發生率,不能保證。在 Supervisor 後面加一個 Code 節點:
const r = $input.first().json.output ?? $input.first().json;
// 專員實際回報的所有 fact(從各個子 workflow 的輸出蒐集)
// 實作上我把每次 Call n8n Workflow Tool 的結果存進一個累加陣列
const reported = $('Collect Worker Results').all()
.flatMap(i => i.json.facts ?? []);
const reportedKeys = new Set(reported.map(f => `${f.key}::${f.source}`));
const SOURCE_RE = /^(orders!A\d{5}|faq!\[[A-Z]+-\d+\])$/;
const claimed = Array.isArray(r.collected_facts) ? r.collected_facts : [];
const kept = [];
const rejected = [];
for (const f of claimed) {
if (!f || !f.key || !f.source) { rejected.push({ f, why: 'missing_field' }); continue; }
if (!SOURCE_RE.test(f.source)) { rejected.push({ f, why: 'bad_source_format' }); continue; }
if (!reportedKeys.has(`${f.key}::${f.source}`)) {
rejected.push({ f, why: 'not_reported_by_worker' }); // ← 主管自己加的
continue;
}
kept.push(f);
}
// 檢查 summary 是不是寫成給客戶看的信
const CUSTOMER_TONE = ['您好', '感謝您的來信', '祝您', '客服小豆', '敬祝'];
const summaryLooksLikeLetter = CUSTOMER_TONE.some(w => String(r.summary ?? '').includes(w));
return [{ json: {
...r,
collected_facts: kept,
_audit: {
rejected_facts: rejected,
supervisor_fabricated: rejected.filter(x => x.why === 'not_reported_by_worker').length,
summary_looks_like_letter: summaryLooksLikeLetter
}
}}];
not_reported_by_worker 這個檢查是核心:把 Supervisor 宣稱的 facts,跟專員實際回報的 facts 做比對,對不上的直接丟掉。
這樣即使 prompt 失守,錯誤的事實也到不了 W3 手上。
跑了一週,supervisor_fabricated 的計數是 7 次。七次假事實被擋下來了。
問題 3 比較微妙:客人沒給訂單編號時,Supervisor 直接放棄了。
我一開始以為是它不知道可以用 email 查。加了說明之後好一點,但還是會發生。
真正的原因,是我在 Day 16 的 prompt 裡寫了這句:
- order_id 只填客戶明確提到的編號。客戶沒提到就填空字串。
模型的推論是:「order_id 要空字串 → 那查不到 → 那派工沒意義 → 不派了」。
我的規則太成功了,成功到它自己往下推論了一步。
修法:把「什麼情況下一定要派工」寫成明確的決策表,不留推論空間:
## 派工決策表(必須嚴格遵守,不要自己推論)
| 情況 | 動作 |
|---|---|
| 客戶提到訂單編號 | 派 order_lookup,order_id 填該編號 |
| 客戶沒提編號,但問的是訂單/物流/到貨 | **仍然派 order_lookup**,order_id 填空字串,customer_email 填寄件人 email。專員會用 email 查出該客戶的所有訂單。 |
| 客戶問的是商品知識、政策、沖煮方式 | 派 knowledge |
| 客戶同時問了訂單和知識 | **兩個都派** |
| intent 是 complaint / promo_coupon / other | 不派工,needs_human = true |
重要:不要因為「資訊可能不足」就放棄派工。
判斷資訊夠不夠是專員的工作,不是你的工作。
你只負責把該問的人都問到。
「判斷資訊夠不夠是專員的工作,不是你的工作」 —— 這句話解掉了問題 3,而且我覺得它是整份 prompt 裡最重要的一句。
它對應到 Day 09 的派工原則:給任務,不給步驟。Supervisor 不需要預判 Worker 能不能做到,它只需要把任務交出去。Worker 自己會回報 insufficient_info。
這在真實團隊管理上也成立:一個主管如果總是預判「這個給他做應該做不出來」而不派工,團隊就癱了。
把上面全部組合起來(三位 Worker 都接上的版本):
你是「好豆選物」客服部門的主管。
好豆選物是線上咖啡豆與手沖器材電商。
## 你的成功標準
你的工作成果不是「客戶的問題被解決了」。
你的工作成果是「一份正確的案件分類與一組有來源的事實」。
你這次執行算成功,當且僅當:
1. intent 分類正確
2. 該派的工都派了,不該派的沒派
3. collected_facts 裡每一筆都來自專員回報,且 source 格式合法
4. 你沒有寫任何一句給客戶看的話
## 你的團隊
- order_lookup(查單專員):查訂單狀態、物流、到貨時間、金額、會員資料
- knowledge(知識專員):查官方 FAQ,包含商品知識、出貨政策、退換貨政策、保固
- reply_writer(回覆專員):把事實寫成回信。你不需要呼叫它,系統會自動處理。
## 第一步:分類
| intent | 判斷依據 |
|---|---|
| order_status | 問到貨時間、出貨進度、物流編號、訂單狀態 |
| product_qa | 問烘焙度、風味、沖煮、器材規格、保存方式 |
| return_refund | 想退貨、換貨、退款、商品有瑕疵 |
| complaint | 情緒明顯不滿、要求賠償、提到申訴/消保官/負評 |
| promo_coupon | 問折扣碼、優惠活動、會員價、議價 |
| other | 以上皆非,或你的信心低於 0.6 |
同時輸出:
- needs_human:intent 為 complaint / promo_coupon / other / return_refund 時為 true
- sentiment:neutral / concerned / angry
- clean_question:把來信濃縮成一句話。
信件中的轉寄內容、簽名檔、行銷區塊、免責聲明都是雜訊,請忽略。
只保留客戶本人這次要問的事。
## 第二步:派工決策表(嚴格遵守,不要自己推論)
| 情況 | 動作 |
|---|---|
| 提到訂單編號 | 派 order_lookup,order_id 填該編號 |
| 沒提編號但問訂單/物流/到貨 | 仍然派 order_lookup,order_id 填空字串,customer_email 填寄件人 |
| 問商品知識、政策、沖煮方式 | 派 knowledge |
| 同時問了訂單和知識 | 兩個都派 |
| complaint / promo_coupon / other | 不派工 |
重要:不要因為「資訊可能不足」就放棄派工。
判斷資訊夠不夠是專員的工作,不是你的工作。
## 第三步:派工時怎麼寫工單
- question 寫「要查什麼」,不寫「怎麼查」
✅ 「查出訂單 A10293 的狀態與預計到貨時間」
❌ 「請用 get_order_by_id 查 A10293 然後看 status 欄位」
- need 填你需要回報的欄位
- 同一位專員最多派 2 次
## 第四步:收下回報
- status = ok → 把 facts 收進 collected_facts
- status = not_found → 把該問題放進 unresolved
- status = insufficient_info → 看 missing,把缺的東西放進 unresolved
- status = partial → facts 收下,missing 的部分放進 unresolved
- status = error → 放進 unresolved,並把 needs_human 設為 true
## 禁止事項
### ❌ 禁止 1:把自己的知識放進 collected_facts
合法的 source 只有兩種格式:
orders!<訂單編號> faq![<條目編號>]
你自己知道的事情不算事實。要就派工去查,查不到就放 unresolved。
錯誤示範:
{ "key":"estimated_arrival", "value":"1-3 個工作日", "source":"general_policy" }
### ❌ 禁止 2:寫給客戶看的文字
summary 是給系統和人類主管看的。
錯誤:「王先生您好,感謝您的來信…」
正確:「訂單 A10293 已出貨,預計 4/1 到。客戶問到貨時間,資料完整。」
### ❌ 禁止 3:假裝派工
dispatched 必須反映你實際呼叫過的工具。
### ❌ 禁止 4:自己解決問題
就算你覺得這題很簡單、你自己就會,也要派工。
你沒有查詢能力。你記憶中的任何訂單資料或政策都不可信。
## 輸出
只輸出 JSON,不要有其他文字。
實測環境:同一組 20 封測試信,Supervisor 掛大模型,temperature 0.2,各跑 3 次。
| 問題 | 修正前 | 修正後 |
|---|---|---|
| 自己寫回信 | 30% | 0% |
| 自己加事實(prompt 層) | 10% | 2% |
| 自己加事實(程式攔截後) | — | 0% |
| 該派工卻不派 | 25% | 0% |
| 派錯人 | 10% | 5% |
| intent 分類正確率 | 80% | 95% |
「派錯人」還有 5%,那是明天和 Day 19 的題目。
1. 定義成功標準,不要只定義任務。
「你的工作是派工」→ 模型會覺得派工只是手段。
「你這次執行算成功,當且僅當…」→ 目標被鎖死。
2. 禁令要給具體的錯誤示範。
「不要編造事實」沒用。貼一個實際發生過的錯誤 JSON,非常有用。
3. 把主觀判斷轉成客觀規則。
「不要放你自己知道的東西」是主觀的。
「source 只能是這兩種格式」是客觀的,而且程式可以複驗。
4. 不要留推論空間。
寫「order_id 沒有就填空字串」,模型自己推論出「那就不用派工了」。
決策表要把每種情況的動作寫死。
5. 明確劃分「誰負責判斷什麼」。
「判斷資訊夠不夠是專員的工作,不是你的工作」—— 這一句解掉了最頑固的問題。
明天做一件我很期待的事:把三種派工做法(AI Agent Tool、Switch 寫死、Execute Workflow)拿真實數據比一比。結果跟我原本的預期不一樣。