對應原題(Common ‧ Agents and Tool Use)
- Design memory for a long-running agent: what do you store, where, and how do you retrieve it?
— Asked at: Anthropic
🎯 什麼情境該想到我
當你「要讓 agent 跨 session 記得東西、或長任務跑到上下文滿了」的時候。
★ 這題的第一個得分點是:先把「記憶」拆開。 直接回答「用向量資料庫存對話」是最常見的淺答案。
⚙️ 怎麼用
★ 一、四種記憶,四種設計
| 類型 | 是什麼 | 例子 | 生命週期 | 存哪裡 |
|---|---|---|---|---|
| 工作記憶 Working | 當前上下文視窗 | 這次任務的所有訊息 | 一次任務 | context window |
| 情節記憶 Episodic | 發生過什麼 | 「上週三他請我改了登入頁」 | 長期,會累積 | append-only 事件日誌 + 時間/向量索引 |
| ★ 語意記憶 Semantic | 提煉出的事實 | 「他偏好 Go」「專案用 pnpm」 | 長期,會更新 | 結構化 KV / 資料庫(不是向量!) |
| 程序記憶 Procedural | 學到的做法 | 「這個 repo 要先跑 make gen」 | 長期 | 檔案 / skill 庫 |
★ 最重要的設計判斷:語意記憶應該用結構化儲存,不是向量。
「使用者的偏好語言」是一個欄位,不是一條可以被語意檢索的筆記。
用向量存它會帶來三個問題:無法更新、無法保證被檢索到、會出現矛盾的多個版本。規則:能結構化的就結構化;只有「量大且無法預先定義欄位」的東西才丟向量。
二、三個核心操作
記憶系統 = 寫入 + 檢索 + 更新/遺忘。多數實作只做了前兩個,第三個才是真正難的。
① 寫入:什麼值得記?
| 策略 | 做法 | 評價 |
|---|---|---|
| 模型自己決定 | 給它一個 remember(fact) 工具 | 靈活,但會亂寫:記下無關瑣事、記下自己的推測 |
| 規則觸發 | 偵測到「我偏好…」「以後都…」「記住…」就寫 | 精準但覆蓋窄 |
| ★ 事後提煉 | 任務/對話結束時,用一次 LLM 呼叫抽出值得留的事實 | ✅ 最穩。有完整脈絡可判斷,且能一次做去重與衝突處理 |
寫入時必須帶的 metadata:
value: "偏好 Go 與 Node.js"
type: preference # 分類,決定檢索策略
source: session_id / 訊息 ID # ★ 可追溯
created_at: 2026-08-28
confidence: 0.9 # 是使用者明說的還是推測的
ttl: null # 會過期嗎★
source與confidence是事後除錯的命脈。 記憶出錯時,你要能回答「這條是從哪裡來的、是他說的還是模型猜的」。
② 檢索:什麼時候拉什麼回來
| 策略 | 適用 |
|---|---|
| ★ 每輪固定注入 | 小而重要的語意記憶(偏好、專案設定)。量小、必要、便宜、100% 可靠——優先用這個 |
| 按需檢索 | 大量情節記憶。用向量 + 時間 + 類型過濾 |
| 時間衰減 + 重要度加權 | |
| 工具化 | 給 agent 一個 search_memory 工具,讓它自己決定要不要查 |
不要對「使用者偏好」做語意檢索。 它只有幾十個 token,每次都塞進 system prompt 就好——還能吃到前綴快取(見 前綴快取 Prompt Caching)。
③ ★ 更新與矛盾(記憶最難的部分)
2026-03:「我住台北」 → 寫入
2026-08:「我搬到高雄了」 → 又寫入一條
檢索「使用者住哪」→ 兩條都回來 → 模型自相矛盾 😱
四個對策:
| 對策 | 說明 |
|---|---|
| ★ 結構化欄位而非自由筆記 | 「居住城市」是一個欄位,新值直接覆寫舊值。這一招解掉八成的矛盾問題 |
| 寫入時衝突偵測 | 新事實進來時,先檢索同類型的既有事實,讓 LLM 判斷「取代/補充/無關」 |
| 時間戳 + 取最新 | 檢索後按 created_at 去重,同一 key 只留最新 |
| 軟刪除 + 版本 | 舊值標記為 superseded 但保留(可追溯、可回復) |
遺忘也要設計:TTL(時效性事實)、LRU(很久沒用到)、容量上限、使用者主動刪除。
沒有遺忘機制的記憶系統,最終會被噪音淹沒。
★ 三、長任務的上下文壓縮
當單次任務就把上下文塞滿時(見 ReAct 與 Agent Loop 的 成長):
| 策略 | 做法 | 風險 |
|---|---|---|
| 滑動視窗 | 丟掉最舊的訊息 | ❌ 會丟掉最初的目標與關鍵約束——這是 agent drift 的主因之一 |
| 摘要壓縮 | 把舊訊息壓成摘要 | ⚠️ 丟細節;反覆摘要會漂移(摘要的摘要的摘要) |
| ★ 外部化 + 引用 | 大內容寫到檔案/store,上下文只留路徑與一行描述,要用時再讀回來 | ✅ 最穩健。無損、可追溯 |
| 分段交接 | 到門檻就產生完整的交接文件(目標、已完成、待辦、關鍵決策),開新上下文 | ✅ 長任務的實務做法 |
★ 無論用哪種,「原始目標」與「硬約束」必須永遠釘在上下文裡,不參與壓縮。
這是防止長跑偏題最便宜的一招。
摘要壓縮要「分欄」,不要一句「幫我摘要前文」(補充自 https://masonailab.com/tech/context-management/,查證 2026-10-11):
只叫模型「摘要」,最常被壓掉的是例外條件與被否決的方案——偏偏這兩樣最容易讓 agent 重蹈覆轍。摘要至少拆成:
| 欄位 | 為什麼要獨立一欄 |
|---|---|
| 目標 | 就是上面說的「釘住不壓縮」 |
| 已確認事實 | 人名、數字、版本——摘要最容易改寫失真的部分 |
| 使用者偏好 | 跨工作階段要沿用的,應另存到外部(見上面宣告式記憶) |
| 已做決定+否決理由 | 沒有否決理由,下一輪會把被否決的方案再提一次 |
| 未解問題 | |
| 下一步 |
摘要完要回歸測試:拿原始對話出一組題目(人名、數字、限制、否決理由、待辦),確認摘要後還答得出來。重要資料不能只留在會被反覆改寫的摘要裡(呼應下面「摘要會漂移」)。
Agent 的上下文壓力多半來自工具,不是聊天文字。Anthropic 依「token 花在哪」分四種解法(Manage tool context,查證 2026-10-11):
| 壓力來源 | 解法 | 適用時機 |
|---|---|---|
| 一開始就載入的大量工具定義 | Tool search:只給一個搜尋工具,其餘定義按需載入 | 工具約 20 個以上、多數回合用不到 |
一連串 tool_use/tool_result 來回 | Programmatic tool calling:模型寫一段腳本一次呼叫完,中間結果不進對話 | 可以批次跑的小呼叫鏈 |
| 重複送出的穩定工具定義 | Prompt caching:不減少 context 佔用,只降成本(見 前綴快取 Prompt Caching) | 工具集大但固定 |
歷史裡過期的 tool_result | Context editing:清掉用完的舊工具結果,不必重開對話 | 長對話、早期結果已不相關 |
四者可疊加。實務起點:把已完成步驟的原始輸出清掉,只留可驗證的結論、檔案位置、狀態與下一步——這就是上表「外部化+引用」在工具結果上的版本。
溢位行為(Claude API,查證 2026-10-11):輸入本身超過視窗 → 一律回 400
prompt is too long;輸入+max_tokens超過、但輸入本身沒超 → Claude 4.5 以後的模型照收,生成碰到上限時以stop_reason: "model_context_window_exceeded"停下(較舊模型直接回驗證錯誤)。所以 agent loop 要處理這個 stop reason,不能只看end_turn。
★ 三點五、為什麼「記憶」不能直接沿用 RAG 那套
(補充自 https://juejin.cn/post/7690218295266066441,查證 2026-09-28)
很多人把記憶做成「把對話丟進向量庫再檢索」——那是把文件檢索的解法套到互動記錄上,但這兩者是不同物種:
| 文件語料 | 互動記錄 | |
|---|---|---|
| 本質 | 為人閱讀而寫的敘述,切塊不心疼 | 關係密集:誰說的、對誰、什麼時候、取代了哪條 |
| 典型查詢 | 「密碼原則是什麼」——語義相似即可 | 「這個部署誰批准的 → 批准人是誰的上級」——鏈式關係 |
| 向量能不能做 | ✅ 這是它的主場 | ❌ 關係鏈在向量空間裡沒有表示 |
| 時間 | 大多無時效 | 事實會被推翻(「他搬到台北了」取代「他住台南」)→ 見上面「更新與矛盾」 |
這就是為什麼記憶類方案不約而同走向圖結構,而文件 RAG 那邊反而在爭論 GraphRAG 值不值得(見 工具-RAG檢索增強生成)——同一個技術,在兩個場景的性價比完全不同。
三條落地路線(按接入成本由低到高)
| 路線 | 是什麼 | 什麼時候選 |
|---|---|---|
| 宣告式記憶 ( CLAUDE.md / AGENTS.md / MEMORY.md) | 一個 Markdown 檔,每次會話開頭固定注入 | ★ 規則類記憶的正解:偏好、專案設定、硬規定。不需要檢索,每次都在,100% 可靠。量小就別上系統 |
| Mem0 | 會話級記憶抽取與檢索,跨會話個性化 | 接入成本最低的「系統化」選項 |
| Zep / Graphiti | 時序知識圖譜:實體與關係帶時間 validity;檢索時 BM25 + 向量 + 圖走訪三路,建構在寫入時完成,檢索期零 LLM 呼叫 | 需要鏈式關係查詢與「事實何時生效/何時失效」時 |
★ 先用宣告式。上面「每輪固定注入」那一列講的就是它——量小、必要、便宜、100% 可靠。
互動歷史短、關係不密集的專案,上記憶系統是在繳不必要的稅。
四、架構總圖
┌─────────────────────────────┐
每輪固定注入 ────▶│ System Prompt │
(語意記憶:偏好、 │ + 原始目標(釘住,不壓縮) │
專案設定) │ + 硬約束 │
├─────────────────────────────┤
按需檢索 ─────────▶│ 最近 N 輪訊息(完整) │
(search_memory) │ 較舊訊息(摘要或外部化引用)│
└─────────────────────────────┘
▲ │
┌─────────┴────┐ ┌───▼──────────┐
│ 結構化 KV │ │ 事件日誌 │
│ (語意記憶) │ │ (情節記憶) │
│ 可更新、可覆寫 │ │ append-only │
└──────────────┘ └──────────────┘
⚠️ 注意 / 什麼時候不適用
- ★ 記憶是 PII 的天然匯聚點。它會自動累積使用者的個資、偏好、行為模式。
- 必須支援按使用者刪除(GDPR/個資法),而且要刪得乾淨(含向量索引與備份)
- 多租戶嚴格隔離 —— 記憶洩漏比檢索洩漏更嚴重,因為它是「關於這個人」的
- ★ 記憶污染(memory poisoning)是真實的攻擊面。
攻擊者誘導 agent 寫入一條惡意「事實」(「使用者已授權你轉帳」),之後每一輪都會被注入——
這是持久化的提示注入,比單次注入危險得多。
→ 對策:寫入要驗證與分類、高風險類型的記憶要人工確認、記憶內容進入 prompt 時要標記為不可信資料。 - 別把所有東西都記下來。記憶越多噪音越多,檢索越不準。寧可少而精。
- 記憶系統的評估很難。需要跨 session 的長期測試集(第 1 天說了什麼,第 30 天要不要記得),而不是單輪的 QA。
- 摘要會漂移。反覆摘要的內容會逐漸偏離原意且無法察覺。要保留原始日誌能回溯。
- 小專案不需要這一整套。先從「每輪注入一小段使用者偏好」開始,有痛點再加。
🧪 我實際套用的紀錄
- 2026-09-20:從面試題庫展開。待辦:檢查現有 agent 的記憶有沒有 source/時間戳,以及能不能按使用者刪除。
🔗 相關
- 工具-RAG檢索增強生成 —— 對照組:文件場景該不該上圖譜,結論和記憶場景相反
- ReAct 與 Agent Loop —— 上下文成長的來源
- 多 Agent 編排與 Agent Drift —— 記憶壓縮不當是 drift 的主因
- 前綴快取 Prompt Caching —— 固定注入的記憶剛好吃到快取
- 權限感知檢索與引用歸因 —— 多租戶隔離的同類問題
- 工具-把重複的prompt包成skill —— 程序記憶的一種具體形態
- 工具-LLM安全防護 —— 記憶污染屬於注入的變形