今天做出第一版客服 Agent:一個 AI Agent 負責讀信、查訂單、寫回信。它是整個系列的對照組,之後所有的改良都要跟它比。
教學常用 Google Sheets 當資料庫,但要先做 OAuth 授權,對於想快速試做的人來說門檻有點高。n8n 2.x 內建的 Data table 可以直接在 workflow 裡建表、查詢,資料跟 n8n 放在一起,測試起來方便很多。
我先建一個一次性的 workflow 把訂單寫進去,節點順序是 Manual Trigger、Data table、Code、Data table。
第一個 Data table 節點用來建表:
| 欄位 | 設定 |
|---|---|
| Resource | Table |
| Operation | Create |
| Table Name | haodou_orders |
| Reuse Existing Tables | 開啟(重跑時不會因為表已存在而報錯) |
欄位定義如下,除了 amount 是 Number,其餘都是 String:
order_id, email, name, status, carrier, tracking_no,
items, amount, ordered_at, shipped_at, eta, delivered_at, note
接著用 Code 節點輸出要寫入的訂單。我的實驗資料共有 24 筆,這裡列出後面文章會一直用到的 6 筆:
// 一筆訂單輸出成一個 item,下一個節點會逐筆寫進 Data table
const orders = [
{ order_id: 'A10293', email: 'wang@example.com', name: '王小明', status: 'shipped', carrier: '黑貓宅急便', tracking_no: '8891234567', items: '衣索比亞 耶加雪菲 水洗 淺焙 200g x2', amount: 1180, ordered_at: '2026-09-08', shipped_at: '2026-09-10', eta: '2026-09-15', delivered_at: '', note: '' },
{ order_id: 'A10294', email: 'lee@example.com', name: '李小美', status: 'packing', carrier: '', tracking_no: '', items: 'V60 濾杯 01 + 濾紙 100入', amount: 890, ordered_at: '2026-09-12', shipped_at: '', eta: '2026-09-17', delivered_at: '', note: '' },
{ order_id: 'A10295', email: 'chen@example.com', name: '陳大文', status: 'delivered', carrier: '黑貓宅急便', tracking_no: '8891234599', items: '哥倫比亞 慧蘭 中深焙 400g', amount: 1360, ordered_at: '2026-08-20', shipped_at: '2026-08-21', eta: '2026-08-23', delivered_at: '2026-08-23', note: '' },
{ order_id: 'A10296', email: 'wang@example.com', name: '王小明', status: 'pending_payment', carrier: '', tracking_no: '', items: '手搖磨豆機 C2', amount: 2480, ordered_at: '2026-09-13', shipped_at: '', eta: '', delivered_at: '', note: '' },
{ order_id: 'A10297', email: 'liu@example.com', name: '劉志強', status: 'returning', carrier: '黑貓宅急便', tracking_no: '8891234610', items: '瓜地馬拉 安提瓜 中焙 200g', amount: 620, ordered_at: '2026-08-30', shipped_at: '2026-08-31', eta: '2026-09-02', delivered_at: '2026-09-02', note: '' },
{ order_id: 'A10300', email: 'lin@example.com', name: '林佳穎', status: 'shipped', carrier: '新竹物流', tracking_no: 'HC20260912017', items: '肯亞 AA 中淺焙 200g x3', amount: 1650, ordered_at: '2026-09-11', shipped_at: '2026-09-12', eta: '2026-09-15', delivered_at: '', note: '' },
];
return orders.map(o => ({ json: o }));
最後一個 Data table 節點選 Resource: Row、Operation: Insert、資料表選 haodou_orders,欄位對應用自動對應,執行一次就完成了。
資料裡我刻意放了一個陷阱:王小明同時有 A10293(已出貨)和 A10296(待付款)兩筆訂單。客人只寫「我的訂單到了嗎」的時候,系統要怎麼處理,後面會一直碰到。
status 只會出現這六種值:
pending_payment 待付款
packing 備貨中
shipped 已出貨
delivered 已送達
returning 退貨處理中
cancelled 已取消
整個 Agent 的結構如下圖:一個 AI Agent 底下接模型、記憶、查訂單的工具。

教學先用 Manual Trigger 加一封寫死的測試信,比較好操作。我自己批次跑實驗時,是把入口換成 Webhook,其餘設定完全相同。
// 固定的測試信。欄位名稱先訂好,之後換成 Webhook 或 Gmail 時就不用改後面的節點
return [{
json: {
from_email: 'wang@example.com',
from_name: '王小明',
subject: '訂單什麼時候到',
body: '你好,我的訂單 A10293 想問一下什麼時候會到?謝謝',
thread_id: 'th_demo_001',
received_at: '2026-09-14T09:05:00+08:00',
},
}];
Source for Prompt (User Message) 選 Define below,Prompt 切成 Expression 貼上:
寄件人:{{ $json.from_name }} <{{ $json.from_email }}>
主旨:{{ $json.subject }}
內文:
{{ $json.body }}
Options 加入 System Message。這份 prompt 是很多人會寫出來的「第一版」,把角色、工具、FAQ、處理規則、語氣規範全部寫在一起:
你是「好豆選物」的客服專員小豆。好豆選物是線上咖啡豆與手沖器材電商。
你的工作是閱讀客戶來信,判斷需求,查詢必要資料,然後寫一封回信。
## 你可以使用的工具
- get_order:用訂單編號查訂單資料。
## 訂單狀態對照
pending_payment=待付款, packing=備貨中, shipped=已出貨,
delivered=已送達, returning=退貨處理中, cancelled=已取消
## 常見問題(FAQ)
- 一般訂單於付款完成後 1–3 個工作日內出貨,例假日不計。
- 運費 NT$80,單筆滿 NT$1500 免運。
- 目前配送台灣本島與離島,不提供海外配送。
- 咖啡豆建議於烘焙日後 30 天內飲用完畢,開封後請密封,放陰涼處或冷藏。
- 淺焙豆適合手沖、冰滴與冷萃;中深焙適合義式與加奶飲品。
- 商品到貨後 7 天內可申請退貨,但已開封的咖啡豆不接受退貨。
- 器材類商品保固一年,人為損壞不在保固範圍。
## 處理規則
1. 先判斷客戶的意圖,可能是:查訂單、問商品知識、退換貨、客訴、問優惠、其他。
2. 如果是查訂單,必須使用 get_order 工具,不可以憑印象回答。
3. 如果客戶沒給訂單編號,要在回信中禮貌地請他提供。
4. 如果是問商品知識,從上面的 FAQ 找答案。FAQ 沒寫的,不要自己編。
5. 如果是退換貨,說明政策,並告知會有專人聯繫。
6. 如果是客訴(語氣明顯不滿、要求賠償、提到消保官),不要自己回,
在回信開頭標註 [需人工處理]。
7. 如果是問優惠折扣碼,請他參考官網首頁公告,不要自己給折扣。
8. 一封信可能同時有多個問題,要全部回答到。
## 回信語氣規範
- 稱呼客人用「您」,署名固定為「好豆選物 客服小豆」。
- 開頭先同理,再回答。不要用「根據我們的政策」開頭。
- 不使用:「親愛的客戶」、「敬請見諒」、「如有疑問歡迎來信」。
- 不確定的事一律寫「我這邊幫您確認後,最晚 24 小時內回覆您」,不要猜。
- 全信控制在 150 字以內。
- 結尾固定一句:「祝您有個美好的一天,好豆選物 客服小豆」。
## 輸出
直接輸出回信內容,不要加任何說明文字。
Chat Model 接 Ollama Chat Model,選 gemma4:cloud,Options 加入 Context Length 16384、Enable Thinking 關閉、Sampling Temperature 0.2,理由在 Day 03 說明過。
Memory 接 Simple Memory,Session ID 選 Define below,Key 填 {{ $json.thread_id }},Context Window Length 設 5。為什麼選 thread_id,明天會用實驗說明。
Tool 插槽新增一個 Data table Tool,把節點名稱改成 get_order。模型看到的工具名稱就是節點名稱,取個清楚的名字對模型判斷很有幫助。
| 欄位 | 設定 |
|---|---|
| Tool Description | Set Manually |
| Resource | Row |
| Operation | Get |
| Data table | haodou_orders |
| Must Match | All Conditions |
| 條件 | order_id 等於下方 Expression |
| Return All | 關閉,Limit 設 1 |
條件的值:
{{ $fromAI('order_id', '客戶信件中提到的訂單編號,格式如 A10293', 'string') }}
$fromAI() 的三個參數分別是參數名稱、描述、型別。n8n 會把工具裡所有的 $fromAI() 整理成一份參數定義交給模型,模型決定呼叫工具時就依照這份定義填值。所以描述寫得越清楚,模型填錯的機會越低。
Description 填:
用訂單編號查詢一筆訂單的完整資料。
當客戶詢問訂單狀態、出貨進度、物流編號、預計到貨時間時使用。
輸入必須是訂單編號(格式:A + 5 位數字)。若客戶沒有提供訂單編號,不要使用此工具。
輸出的回信:
王先生您好,很抱歉讓您久候。
您的訂單 A10293 已於 9 月 10 日出貨,由黑貓宅急便配送(單號:8891234567),預計 9 月 15 日送達。
祝您有個美好的一天,好豆選物 客服小豆
打開 Execution 看細節,AI Agent 對模型呼叫了兩次。第一次模型回傳 get_order({"order_id":"A10293"}),第二次拿到訂單資料後寫出回信,兩次加起來約 2,000 個 prompt tokens。
出貨日、物流商、單號、預計到貨日全部正確。挑毛病的話有兩個:一是資料裡只有姓名,「王先生」是它自己猜的性別;二是客人並沒有在等,「很抱歉讓您久候」有點多餘。
寫這篇之前我以為,Tool Description 如果寫得太簡略,模型遇到沒給編號的信,就會自己編一個訂單編號去查。
為了驗證,我把描述縮成三個字「查訂單」,$fromAI 的描述也縮成「訂單編號」,拿 5 封信(4 封沒提供編號、1 封有提供)跟完整描述的版本比較:
| 描述版本 | 沒給編號的 4 封 | 有給編號的 1 封 |
|---|---|---|
| 完整描述 | 都沒呼叫工具,請客人提供編號 | 正確查單 |
| 只寫「查訂單」 | 都沒呼叫工具,請客人提供編號 | 正確查單 |
在 gemma4:cloud 上,兩個版本的行為一模一樣,沒有亂編編號。
我還是會寫完整的描述,因為這份描述是模型判斷「該不該用這個工具」的唯一依據。現在只有一個工具,選擇很單純;等工具變多,描述含糊就容易選錯。成本也很低,完整描述只讓 prompt 多了約 80 個 tokens。
第一封信跑得很順,但從設計上已經可以預見幾件事:
這些明天先放著,Day 06 會用 10 封刻意設計的信實際驗證。
用 Data table、AI Agent、Ollama Chat Model、Simple Memory 四個節點就能做出會查單的客服 Agent,第一封信事實全對。Tool Description 寫簡略在這個模型上沒造成問題,它只是模型選工具的依據,不過還是會詳細解釋。
明天來講今天帶過的 Memory:Session ID 用 email、用 thread_id,或乾脆不給記憶,實際差在哪裡。