長期記憶 — RAG#
CH06 中期記憶 解決「特定對話接續」,但跨對話的事實依然丟失:
session A: "我叫 Alex"
session B: "我叫什麼?" → 模型不知道解法是 RAG(Retrieval-Augmented Generation):抽出跨對話穩定的事實,存進記憶庫,需要時撈回來。 加上 RAG 之後,同一組 session 變這樣:
session A: "我叫 Alex" → model 呼叫 remember → 存進記憶庫
... 之後任何 session ...
session B: "我叫什麼?" → model 呼叫 recall → "你叫 Alex"7.1 整體設計#
兩個工具#
RAG 在 minimal-agent 裡是兩個工具:
| 工具 | 何時 | 做什麼 |
|---|---|---|
| remember | user 說出值得記的事 | 把事實存進記憶庫 |
| recall | model 想撈跨 session 事實 | 撈出語意最相關的記憶 |
實作上就是 CH02 那份工具清單再多兩個分支。 Model 根據工具的 description 判斷什麼時候應該呼叫哪個工具:
| |
RAG 工作流程#
整個流程涉及四個零件:Embedding + Voyage AI 負責字串 → 向量;JSONL 負責持久化;Cosine 算相似度。 7.2 ~ 7.5 各自展開。
整個 round 涉及四個角色(User、Executor、Model、Voyage AI):
%%{init: {'sequence': {'noteAlign': 'left'}}}%%
sequenceDiagram
participant User
box AI Agent
participant Executor
end
participant Model
participant Voyage as Voyage AI
User->>Executor: 訊息
Executor->>Model: messages + tools (remember / recall)
Note over Model: 看 description<br/>決定叫哪個
alt remember
Model-->>Executor: tool_use: remember(text)
Executor->>Voyage: embed(text, input_type="document")
Voyage-->>Executor: vector
Note over Executor: append 一行到 store.jsonl
Executor->>Model: tool_result: "Remembered"
else recall
Model-->>Executor: tool_use: recall(text)
Executor->>Voyage: embed(text, input_type="query")
Voyage-->>Executor: query_vec
Note over Executor: cosine 比對 + 取 top-K
Executor->>Model: tool_result: 相關記憶
end
Model-->>User: 答案7.2 為什麼需要 Embedding#
關鍵字搜尋有限制:
存的: "User name is Alex"
查的: "what does the user call themselves"
關鍵字命中:0 ← 連 "user" 都沒重疊到嗎?實際情況更慘Embedding 把句子映射成 向量(vector) — 也就是高維語意空間裡的座標。 語意接近的句子(「user’s name」、「what’s their name」、「他叫什麼」)在這個空間裡會落在很近的位置,即使字面上完全不重疊。
Text Action Vector
"User name is Alex" ──embed──▶ [0.12, -0.45, 0.81, ...]
"What's the user's name?" ──embed──▶ [0.10, -0.43, 0.79, ...] ← 語意接近
"How's the weather today?" ──embed──▶ [0.85, 0.02, -0.31,...] ← 天差地別7.3 Voyage AI 的角色#
Voyage AI 是 純粹的 text → vector 翻譯機:你給它 text,它把每段 text 透過 embedding 算成 vector 回傳 — Voyage 端不存任何資料。 拿到的 vector 你自己存到本地(本章用 JSONL,見 7.4)。
text ──Voyage embed──▶ vector ──▶ 本地存(7.4 JSONL)Anthropic 自己沒有 embedding API、官方推薦 Voyage:
- 申請快、免費 50M tokens / 月
voyage-3-lite速度快、品質夠- 包很輕(
pip install voyageai)
為什麼存 vector 而不是即時算#
| 方案 | 每次 recall 的 API call 數 |
|---|---|
| 存 vector(本章) | 1 次(只 embed query) |
| 即時算 | N+1 次(embed query + 重新 embed 每筆記憶) |
幾百筆的記憶就讓「即時算」貴上幾百倍、慢上幾百倍。 embedding 算一次存著用 是 RAG 的標準做法 — 也是為什麼 7.1 序列圖裡 recall 分支只 embed query、不重新 embed 每筆記憶。
為什麼存入跟查詢要分開 embed#
Voyage(以及多數現代 embedding model)支援 asymmetric retrieval:存入時用 input_type="document"、查詢時用 input_type="query"。 模型內部對這兩種做不同優化(query 通常較短、語意較開放;document 較長、結構較穩定)。 7.1 序列圖的兩個分支用不同 input_type 就是這個原因。
7.4 儲存格式:JSONL#
從 Voyage 拿到的 vector(JSON 裡放在 embedding 欄位 — 業界慣稱)連同原文、id、時間戳打包成一行 JSON,append 到 memory/store.jsonl — 每筆記憶一行:
memory/store.jsonl
{"id": "abc12345", "text": "User name is Alex", "embedding": [...], "tags": [], "created": ...}
{"id": "def67890", "text": "Prefers Python over...", "embedding": [...], ...}
{"id": "ghi54321", "text": "Working on minimal-agent project", ...}- append-only:每次
remember加一行,從不改舊資料 - 可肉眼 inspect:純文字,可 grep、cat、git diff
- 零依賴:不需要資料庫、向量索引、ANN library
- 載入快:啟動時讀整個檔案到記憶體 list
幾百筆記憶用 linear scan + cosine 全掃一次 < 10ms。要到幾百萬筆才需要 FAISS / Chroma / pgvector。
7.5 為什麼需要 Cosine#
recall 把 query text 傳給 Voyage 後拿回 query vector(7.1 序列圖 recall 分支的第一步),但 vector 本身只是個高維座標 — 要拿它跟 store.jsonl 裡每筆記憶 的 vector 比一輪、挑出語意最接近的前 K 筆(top-K)才有用。 「比相似度」就是 cosine 出場的地方。
兩向量的「相似度」用夾角衡量:
| 夾角 | 語意 | cos |
|---|---|---|
| 0° | 完全相同 | 1.0 |
| 90° | 無關 | 0.0 |
| 180° | 完全相反 | -1.0 |
cos 在 [0°, 180°] 單調遞減:值越大越像。 完美的排序信號 — 而且只要點積 + 範數,計算便宜,高維向量比快。
為什麼不是 Euclidean distance? 對 normalize 過的向量(多數 embedding model 都會 normalize),cosine 跟 Euclidean 排序結果等價(cos 越大 ⟺ 距離越小)。 但 Euclidean 要開平方根、cosine 不用 — 算幾百萬筆時這個常數差就有感。
為什麼只回 top-K 不全部回#
cosine 算完之後手上有每筆記憶 的相似度,但不會把全部塞給 model — 按相似度排序、只回前 K 筆(K 是預設值、例如 3 或 5)。 兩個原因:
- 省 token — 把幾十、上百筆記憶全塞 context 會擠走真正有用的訊息
- 避免雜訊稀釋 — 排到後段的記憶可能根本不相關,model 看到反而干擾判斷
也有實作改用「相似度門檻」(例如只回 cosine ≥ 0.7 的)取代固定 K — trade-off 是低相關度查詢可能一筆都回不出來,model 拿到空陣列要自己處理。 minimal-agent 選固定 K,對幾百筆規模夠用。
7.6 設計取捨:Tool-Driven vs Auto-Injection#
兩種 RAG 模式:
| 模式 | 做法 | 優缺 |
|---|---|---|
| Auto-Injection | 每次 user message 之前,自動 recall 注入 system context | 不會漏;但每次都消耗 token,且模型沒得選 |
| Tool-Driven | 把 remember / recall 當工具暴露,模型自己決定何時呼叫 | 可觀察、省 token、模型有判斷力;但偶爾會漏 |
我們選 Tool-Driven。理由:
- 省 token — 用不到的時候不浪費
- 可觀察 — 每次決策是顯式 tool_use,能看見軌跡
- 保留控制權 — 模型判斷「值不值得存」、「該不該查」
讓 tool-driven 真的能 work 的關鍵是 description 寫得精準(範例見 7.1)— 模型靠那段話判斷何時叫。
7.7 跨 session 場景#
兩個 session 看 remember / recall 怎麼讓事實跨對話存活:
| |
關鍵:session B 是完全新的對話、messages list 看不到 session A 的歷史,但 model 透過 recall 從 JSONL 撈出跨 session 的事實,依然答得出來 — 這就是長期記憶在做的事。
階段檢查點#
到這裡你應該理解:
- 長期記憶 = 跨對話的事實 — 名字、偏好、決策⋯⋯不綁特定 session
- embedding 算一次存著用 — 記憶 vector 永遠不重算,retrieval 階段只 embed query
- JSONL append-only 就夠 — 純文字、可 inspect、零依賴,幾百萬筆才換 FAISS / Chroma
- Tool-driven 不是 Auto-injection — 選 tool 讓 model 自己決定何時叫,省 token、可觀察
三層記憶組起來#
從 CH05 短期記憶 到本章,三層記憶的全貌:
| 維度 | 短期記憶(CH05) | 中期記憶(CH06) | 長期記憶 RAG(CH07) |
|---|---|---|---|
| 儲存位置 | self.messages(記憶體) | sessions/<name>.json | memory/store.jsonl + embedding |
| 生命週期 | 程式跑著的時候 | 跨 session(手動 save / load) | 永久 |
| 容量 | 受 context window 限制 | ~ context window | 幾乎無限(語意檢索) |
| 用途 | 當前對話的脈絡 | 接續一段未完成的對話 | 事實、偏好、跨對話的知識 |
層之間的轉換靠這兩個機制:
- 短期 → 中期:使用者打
/save <name>把當前messageslist 寫到磁碟 - 任意 → 長期:模型呼叫
remember(text)工具把該記住的事實塞進 RAG store
每層內部的零件:
短期(CH05):context window 管理
├── token 預算追蹤 + 自動 trim
└── /compact 壓縮歷史
中期(CH06):對話持久化
├── /save → sessions/<name>.json
├── /load → 載回 messages list
└── --resume → CLI 捷徑
長期(CH07):RAG
├── remember → embed + JSONL append
├── recall → embed + cosine + top-K
└── Voyage AI + JSONL append-only store三層各司其職、不互搶責任:
- 短期 — 治當下對話太長超出 context window
- 中期 — 治單一對話跨 session 接續
- 長期 — 治知識跨任何對話累積
加總起來:agent 同時知道「現在講到哪、上次聊到哪、用戶長期是誰」。
RAG 延伸方向#
本章用 Voyage + JSONL + linear scan 做出 minimum viable RAG。 若要往更大規模 / 更精緻的方向走:
- Auto-RAG — 不靠 model 決定,每次 user message 之前自動 recall 注入 system context(7.6 tool-driven 的反向)
- 替換 embedding model —
voyage-3-lite換成voyage-3提升品質、或改本地 embedding model 省 API 成本 - ANN library — 幾百萬筆規模時 linear scan 太慢,改用 FAISS / Chroma / pgvector 加 index
- 記憶整理 — 重複合併、舊記憶過期、分類 permanent / temporary
- 多用戶隔離 — 多人共用 agent 時記憶按 user_id 分開存