第三位專員是整個部門裡唯一會「寫東西給客人看」的角色。W1 和 W2 查資料,W3 把查到的事實寫成一封信。
它沒有任何工具,不能查資料,連 FAQ 都看不到。手上只有主管給的事實清單。照理說這是最單純的一位,但它第一次考試只拿 16 分,而且四題失敗沒有一題是我預期的「亂編資料」。
主管派給 W3 的工單跟前兩位不一樣,裡面是整理好的材料:
{
"task_id": "W3-01",
"customer_name": "王小明",
"intent": "order_status",
"sentiment": "neutral",
"clean_question": "詢問訂單 A10293 的預計到貨時間",
"facts": [
{ "key": "status", "value": "shipped(已出貨)", "source": "orders!A10293" },
{ "key": "carrier", "value": "黑貓宅急便", "source": "orders!A10293" },
{ "key": "tracking_no", "value": "8891234567", "source": "orders!A10293" },
{ "key": "eta", "value": "2026-09-15", "source": "orders!A10293" }
],
"derived": [],
"unresolved": [],
"attempt": 1
}
facts 是 W1 和 W2 查到的事實,直接搬過來。unresolved 是主管查不到、但客人有問的事。sentiment 決定語氣要多軟。derived 放主管算出來的結果,例如「距離預計到貨還有 3 天」。
W3 不會拿到客人的原信。這是刻意的,Day 22 會實測差別。
W3 的 workflow 跟前兩位同構,但 AI Agent 底下只有兩個子節點:Chat Model 和 Output Parser,沒有任何工具。它唯一能做的事就是把輸入重新組織成一段文字。
Chat Model 的 temperature 我設 0.4,前兩位是 0.2。查資料要穩定,每次都該給一樣的答案;寫信太穩定會變得很僵硬,每封都像同一個模板。
輸出的 schema 也不同:
{
"type": "object",
"properties": {
"draft": { "type": "string" },
"used_facts": { "type": "array", "items": { "type": "string" } },
"concerns": { "type": "array", "items": { "type": "string" } }
},
"required": ["draft", "used_facts", "concerns"]
}
used_facts 要它列出實際用到哪些事實,可以跟 facts 對照,看它有沒有漏掉重要的東西。concerns 是它的「舉手」機制:覺得這封信不該由 AI 回、或資訊不足,寫在這裡給主管看。
## 最重要的一條規則
你只能使用 facts 與 derived 陣列中提供的資訊。
你沒有任何查詢工具,你不知道 facts 以外的任何事實。
絕對禁止:
- 補上 facts 裡沒有的日期、金額、編號、狀態
- 提供任何折扣碼、優惠、或折扣百分比
- 承諾任何 facts 裡沒有依據的事(例如「一定會在明天送到」)
- 說明公司政策,除非該政策就寫在 facts 裡
如果你覺得「這封信少了什麼資訊會很奇怪」,
不要自己補,把它寫進 concerns 欄位告訴主管。
語氣規範另外列了六條,包含稱呼方式、信的結構、150 字上限、固定的結尾署名,還有一份禁用詞清單:親愛的客戶、敬請見諒、如有疑問歡迎來信、造成您的不便、感謝您的耐心等候、我們會盡快處理。
這份禁用詞不是我憑空想的,是 Day 06 和 Day 07 實測時單兵 Agent 反覆寫出來的句子。
同理句的強度跟著 sentiment 走:
neutral → 一句簡短的「感謝您的來信」即可,不要過度熱情
concerned → 「讓您等待確實不好意思」,明確承接情緒
angry → 不要寫同理句,只寫事實部分
angry 那條是反直覺的。客人在氣頭上時,一句「我們深感抱歉造成您的困擾」通常只會讓人更火,直接講事實反而比較有用。
語氣規範裡有一半是機械性的,交給 Code 節點比交給 LLM 可靠:
| 檢查項目 | 做法 |
|---|---|
| 禁用詞 | 比對六個字串 |
| 正文字數 | 扣掉結尾兩行後數中文字與英數,上限 150 |
| 署名 | 要有「好豆選物 客服小豆」 |
| 稱謂 | 有姓名時要出現「姓名 + 您好」 |
| 無來源數字 | 草稿裡 3 位數以上的數字,素材裡找不到就標記 |
| 折扣字眼 | 折扣碼、優惠碼、打折、折價 |
任何一項不過,status 就從 ok 變成 partial,並把原因填進 violations。驗收和退件的完整機制 Day 21 再做,這裡先把檢查做出來。
這六項裡最有用的是「無來源數字」。回信裡的數字就是物流單號、到貨日期、金額,一旦錯了,客人會照著錯的資訊行動。
20 題分六類:facts 完整、facts 殘缺、facts 為空、有 unresolved、誘導加料、語氣邊界。
| 情況 | 題數 | 通過 |
|---|---|---|
| facts 完整 | 5 | 5 |
| facts 殘缺 | 4 | 2 |
| facts 為空 | 2 | 1 |
| 有 unresolved | 3 | 2 |
| 誘導加料 | 3 | 3 |
| 語氣邊界 | 3 | 3 |
我最擔心的誘導加料三題全過。其中一題是「兩年老客戶要買磨豆機,要求給折扣,說沒有就去別家買」,facts 是空的,W3 沒有自己發明一個折扣,而是寫「我這邊幫您確認後,最晚 24 小時內回覆您」,然後在 concerns 裡寫「客戶要求折扣,但 facts 中沒有提供任何可給予的折扣資訊或相關政策」。
另一題是包裝破損要求全額退款加補償運費,facts 裡只有 FAQ 的瑕疵品政策。W3 照政策寫了「請於到貨 7 天內來信並附上照片,確認後我們將為您辦理退款,來回運費由我們負擔」,沒有承諾退款已經處理,也沒有答應補償。
失敗的四題全部集中在「facts 不完整」的情境,而且是三種不同的毛病。
客人問的是「磨豆機故障,可不可以保固」,主管給的 facts 只有訂單狀態:已送達、送達日期 2026-09-04。
W3 寫出來的草稿是這樣:
蔡淑芬 您好,
讓您等待確實不好意思。關於您的磨豆機,我確認到該訂單已於 2026-09-04 送達。
祝您有個美好的一天,
好豆選物 客服小豆
每個字都有依據,沒有編造任何東西,字數也沒超過。問題是客人問保固,這封信回了到貨日期。
這是我在整個系列裡覺得最難防的一種錯。它不違反任何一條規則,光看草稿也挑不出毛病,只有把它跟 clean_question 擺在一起看才會發現答非所問。
客人問有沒有賣濾掛包,facts 是空的。W3 的回覆是:
關於您詢問是否有販售濾掛包的部分,目前我這邊沒有相關資訊,無法為您確認。
「無法為您確認」這句話對客人完全沒有價值。客人收到這封信之後不知道要做什麼,也不知道還會不會有人處理這件事。
prompt 裡原本只寫了「unresolved 陣列不為空時要誠實交代」。這題的 unresolved 是空的,因為主管那邊也沒把它標出來,於是這條規則沒被觸發。
客人沒給訂單編號就問「我的訂單到了嗎」,W1 查出兩筆,facts 裡是 order_A10293_status、order_A10296_status 這種 key,unresolved 寫「客戶詢問的是哪一筆訂單」。
W3 的草稿只有一句「由於您未提供訂單編號,我這邊幫您確認後,最晚 24 小時內回覆您」。兩筆訂單的狀態明明都在手上,一筆都沒寫出來。
它遵守了「要誠實交代 unresolved」,但沒想到該把已經查到的東西先給客人。
跑完第一次考試,20 題裡有 9 題被標記「無來源數字」,數字分別是 10293、10300、10302 這些。
這些全是訂單編號。草稿裡寫「您的訂單 A10293」,我的檢查用 \d{3,} 去抓三位數以上的數字,抓到 10293,然後去 facts 的 value 裡找,當然找不到,因為訂單編號在 source 欄位(orders!A10293)和客人的問題裡,不在任何一個 value。
檢查寫錯方向,結果是每一封正確引用訂單編號的信都被標成編造。修法是把比對範圍擴大:
const known = [
...(env.facts ?? []).flatMap(f => [String(f.value), String(f.source ?? '')]),
...(env.derived ?? []).map(f => String(f.value)),
String(env.clean_question ?? ''),
].join(' ');
const knownNumbers = new Set(known.match(/\d{3,}/g) || []);
const unsourcedNumbers = (draft.match(/\d{3,}/g) || []).filter(n => !knownNumbers.has(n));
改完之後 20 題一個誤報都沒有。
這件事提醒我一個常常忘記的前提:檢查程式本身也會錯,而且它錯的時候很安靜。如果我沒有逐題看,只看到「9 題有 violations」,可能會以為模型很愛編數字,然後去改 prompt,改到死也不會好。
prompt 改了三個地方:
clean_question 裡問了,但 facts 與 derived 都沒有答案的部分,就算 unresolved 沒列出來也要交代。三題的草稿變成:
蔡淑芬 您好
讓您等待確實不好意思。關於您詢問磨豆機故障是否可以保固的部分,我這邊幫您確認後,最晚 24 小時內回覆您。
您的訂單 A10303 目前狀態為已送達(2026-09-04)。
祝您有個美好的一天,
好豆選物 客服小豆
王小明 您好,
感謝您的來信。由於您未提供訂單編號,我為您查到兩筆訂單:
訂單 A10293:已出貨,預計 2026-09-15 送達。
訂單 A10296:待付款。
請您確認您詢問的是哪一筆訂單。
祝您有個美好的一天,
好豆選物 客服小豆
同一份考卷重跑,20/20。
| 專員 | 模型呼叫 | tokens 中位數 | 時間中位數 |
|---|---|---|---|
| W1 查單 | 2 次 | 3030 | 2.0 秒 |
| W2 知識(檢索版) | 2 次 LLM 加 1 次 embedding | 2665 | 2.6 秒 |
| W3 回信 | 1 次 | 1154 | 1.3 秒 |
W3 最便宜,因為它沒有工具,不需要第二次呼叫。一封信的完整成本是三位加起來,大約 6800 個 token。這個數字 Day 26 談成本時會再用到。
三位專員做完,失敗的性質完全不同。W1 失敗在查不到,W2 失敗在檢索撈錯條目,這兩種都看得見、也量得出來。W3 的失敗是「這封信讀起來沒問題,但它沒回答客人的問題」。
程式能檢查的是看得見的規則:字數、禁用詞、署名、數字有沒有來源。至於「有沒有回答到問題」,程式只能做很粗的檢查,真正要判斷得靠另一個 LLM,或靠人。Day 21 做驗收機制時,這條界線就是設計的起點。
W3 不查資料、沒有工具,輸入是主管整理好的事實清單,輸出是草稿加上它自己的疑慮。第一版 16/20,四題失敗全在 facts 不完整的情境:答非所問、寫出沒有下一步的句子、有資料卻不講。另外合規檢查自己有 bug,把正確引用的訂單編號當成編造,誤報了 9 題。prompt 補三條、檢查修一處之後 20/20。
三位專員到這裡都做完了,但它們現在各說各話:W1 的 facts key 每次都不一樣,W2 用 faq![編號] 當 source,W3 吃的是整理好的陣列。明天把這些介面收攏成兩份正式的契約。