昨天的單兵 Agent 接了 Simple Memory,Session ID 直接填了 thread_id,沒有多解釋。今天用實驗把這個選擇講清楚,順便分享一個我在做實驗時被記憶坑到的經驗。
n8n 的 Memory 插槽做的事其實很單純:每次呼叫模型之前,把同一個 Session 過去幾輪的對話,照順序插在這次的訊息前面。
以 Context Window Length 設 5 為例,送給模型的內容大概是這樣排的:
[system] System Message
[human] 第 1 輪客人的信
[ai] 第 1 輪 Agent 的回信
... 最多保留最近 5 輪
[human] 這一次客人的信
它不會做摘要,也不會判斷哪些內容重要,只是把歷史對話原樣貼上去。所以有幾件事是必然的:記得越多輪,每次呼叫的 prompt 就越長;歷史對話裡如果有錯,模型會把它當成前提繼續用。
Simple Memory 的資料存在 n8n 的記憶體裡,n8n 重啟就清空。正式上線通常會換成 Postgres Chat Memory 或 Redis Chat Memory 這類外部儲存,Session ID 的設計邏輯是一樣的。
在 Simple Memory 裡,Session ID 選 Define below,Key 可以填不同的值。我比較了三種:
| 設定 | Key | 效果 |
|---|---|---|
| thread | {{ $json.thread_id }} |
同一串信共用記憶 |
{{ $json.from_email }} |
同一位客人的所有信共用記憶 | |
| none | 不接 Memory | 每封信都從零開始 |
測試用 6 封信,分成三個情境,每個情境都是兩封信連續寄進來:
| 情境 | 第一封 | 第二封 |
|---|---|---|
| M1 換話題 | 王小明:A10293 什麼時候會到? | 王小明另開一封新信:你們的器材保固多久? |
| M2 補資料 | 王小明:我上禮拜買的東西什麼時候會到? | 同一串信回覆:A10293 |
| M3 追問 | 李小美:A10294 出貨了嗎? | 同一串信回覆:那貨號是多少? |
M1 想看舊話題會不會混進新信,M2 想看客人補上編號之後接不接得上,M3 想看它懂不懂「那」指的是哪張訂單。
| 設定 | M1 換話題 | M2 補資料 | M3 追問 |
|---|---|---|---|
| thread | 新信沒有提到舊訂單 | 正確查到 A10293 | A10294 備貨中,尚未產生物流編號 |
| 新信沒有提到舊訂單 | 正確查到 A10293 | A10294 備貨中,尚未產生物流編號 | |
| none | 新信沒有提到舊訂單 | 正確查到 A10293 | 請客人提供訂單編號 |
M3 才是真正需要記憶的情境。第二封信只有一句「那貨號是多少?」,沒有記憶的版本根本不知道客人在問哪一張單。有記憶的兩個版本不但接上了,還正確回答「尚未產生物流編號」,沒有自己編一個貨號出來。
M2 反而用不到記憶,因為客人第二封信直接寫了 A10293,光看這封信就能查單。
我原本以為用 email 當 Key,客人問保固的新信會冒出「關於您先前詢問的訂單 A10293」這種話。實際跑下來,gemma4:cloud 並沒有把舊話題混進來。
問題出在 prompt 的長度。下表是王小明那 4 封信,每封信所有模型呼叫合計送出的 prompt tokens:
| 信 | thread | none | |
|---|---|---|---|
| M1 第一封 | 1,974 | 1,974 | 1,973 |
| M1 第二封(新信) | 852 | 1,250 | 852 |
| M2 第一封(新信) | 854 | 1,349 | 854 |
| M2 第二封(同一串) | 2,172 | 3,155 | 1,966 |
以 email 為 Key 時,這位客人所有的信都累積在同一段記憶裡。明明是一封毫不相關的新信,也得把前面幾輪對話一起送出去。同樣是 M2 的第二封,email 版本比 thread 版本多了將近 1,000 個 tokens。客人寫信越勤快,這個數字就越大。
還有一個小地方:M1 第二封問保固,thread 版本回「您好」,email 版本回「王先生您好」,把前一封信的稱呼帶過來了。
做 Day 04 的比較實驗時,我先用 50 封評估信跑了一輪單兵 Agent,接著又挑其中 5 封做另一組對照。結果第二組的 prompt 莫名多出 100 到 500 個 tokens。
查了 Execution 才發現,兩次實驗用的是同一個 workflow、同樣的 thread_id。Simple Memory 會跨執行保留對話,所以第二次跑的時候,模型看得到第一次實驗裡自己寫過的回信。
這次運氣好,兩次問的是同樣的問題,回信內容看不出差異。但如果第一次的回答是錯的,第二次就會帶著錯誤前提繼續回答,而且從輸出上完全看不出來。後來我把測試腳本改成每一輪自動換新的 thread_id,那組實驗也重跑了。
這次也順便確認了一件事:Simple Memory 是以 workflow 為單位隔開的。換一個 workflow,就算 Key 一樣,也讀不到另一個 workflow 的記憶。
「王小明是 VIP、偏好淺焙、之前抱怨過包裝」這種資訊,不適合塞進 Memory 插槽。比較好的做法是做成一張可以查詢的表,再包成工具。
做法跟 Day 04 的訂單一樣:建一張 haodou_customers Data table,欄位是 email、name、tier、preference、notes、updated_at,再接一個 Data table Tool 用 email 查詢。Day 11 的查單專員會用到它。
用表格而不是讓 AI 自己寫客人摘要,原因很實際:表格內容看得到、查得到,寫錯了可以手動改。
客服信件我會用 thread_id。它剛好對應人對「一件事」的直覺:同一串往來是同一件事,客人另開新信,記憶就自然重新開始。
email 當 Key 在客服情境下我不建議,成本會跟著客人寄信的次數一路累積。至於完全不接 Memory,適合每次輸入就包含完整資訊的任務;只要客人會用「那個」「另外那筆」追問,就需要記憶。
到了 Multi-Agent 架構,情況又不太一樣。專員收到的是主管整理好的工單,大多數專員根本不需要記憶,這在 Day 23 會實測。
Memory 就是把歷史對話貼在 prompt 前面。三種 Session ID 設定裡,只有追問的情境真正需要記憶;用 email 當 Key 雖然沒有混淆話題,但 prompt 會隨著來信次數一路變長。另外,重跑測試記得換 Session ID,不然模型會看到上一次的回答。
第一週的最主要的部分在明天:10 封刻意設計的壓力測試信,每封跑 3 輪,看單兵 Agent 到底在哪裡出問題。