Skip to content

Ch 13 — Memory & RAG

75-90 分鐘。讀完你會懂:LLM 為什麼沒記憶、session memory / long-term memory / RAG / contextual retrieval 各是什麼、怎麼選 vector DB、實作一個個人助理 agent 有記憶。

動手練習:寫一個有 session memory 的 agent、加一個 RAG layer、做 contextual retrieval 對比。

前置:完成 Ch 12 — 有自己的 mini framework 能加 memory。

🛠 Starter code: starter-code/ch13_memory_rag/ — session memory (SQLite-backed) + Chroma RAG pipeline + contextual retrieval demo。


1. 「LLM 沒有記憶」回顧 + 為什麼需要 memory

Ch 1 §3 提過:LLM 每次 API call 是獨立的——你看 ChatGPT 記得你前面講什麼,是因為它後端把對話歷史塞進 context。

但 context 有兩個限制:

  • 大小:200K-1M token,塞滿了
  • 成本:每多一輪、前面對話都重新進 context 重新算錢

所以 production agent 必須有 memory 層——把對話 / 文件 / 知識存外面、需要時撈相關片段進 context。


2. 三種 memory

類型範圍例子實作
Session memory一次對話內「剛剛你說我要訂下週五的會議室」累積 messages array,可摘要
Long-term memory跨對話「使用者是 Wei、住台北、偏好繁中」寫進 DB / vector store / 結構化 store
RAG(檢索增強)外部知識「公司 SOP / 產品文件 / 過去案例」vector DB + similarity search

實務上三種混合:對話進行用 session memory,講到特定使用者偏好查 long-term,要查公司知識用 RAG。


3. Session Memory: 累積 + 摘要

最簡單的版本就是不刪 messages:

python
messages = []
while True:
    user_input = input("> ")
    messages.append({"role": "user", "content": user_input})
    resp = client.messages.create(model="...", messages=messages, ...)
    messages.append({"role": "assistant", "content": resp.content[0].text})
    print(resp.content[0].text)

問題:對話到 50 輪後 context 100K+ token,每輪 $0.10-0.30。

解法:摘要老對話

python
SUMMARIZE_THRESHOLD = 20  # 20 條 messages 就壓縮

if len(messages) > SUMMARIZE_THRESHOLD:
    # 把舊的 10 條摘要成 1 條 system note
    old = messages[:10]
    summary_resp = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[
            {"role": "user", "content": f"Summarize this conversation in 100 字 繁中 keep key facts:\n{format_messages(old)}"}
        ],
    )
    summary = summary_resp.content[0].text
    messages = (
        [{"role": "user", "content": f"[CONVERSATION SUMMARY]\n{summary}"}]
        + messages[10:]
    )

LangGraph / Claude Code 內部 compaction 是這個機制的進階版(保留結構化資料 / tool call 摘要)。


4. Long-term Memory: 結構化 vs 向量

4.1 結構化(適合確定欄位的事實)

python
# 簡單 SQLite
import sqlite3
conn = sqlite3.connect("memory.db")
conn.execute("""
    CREATE TABLE IF NOT EXISTS facts (
        user_id TEXT,
        key TEXT,
        value TEXT,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (user_id, key)
    )
""")

# 寫
conn.execute("INSERT OR REPLACE INTO facts VALUES (?, ?, ?, ?)",
             ("wei", "city", "Taipei", datetime.now()))

# 讀
row = conn.execute("SELECT value FROM facts WHERE user_id=? AND key=?", ("wei", "city")).fetchone()

agent 對話開始時 load 該 user 所有 facts 進 system prompt:

python
facts = conn.execute("SELECT key, value FROM facts WHERE user_id=?", (user_id,)).fetchall()
system_prompt = "User facts:\n" + "\n".join(f"- {k}: {v}" for k, v in facts)

4.2 向量 memory(適合無結構的事件 / 對話)

把「使用者過去說過的話」存 vector store,需要時 semantic search 找相關。

例:使用者今天說「我喜歡日式風格」,下次他問「推薦一家餐廳」時 agent 撈到這條 memory → 推薦日式餐廳。


5. RAG(Retrieval-Augmented Generation)

大量外部知識(公司 SOP / 產品 doc / 法規)切塊、embed 進 vector DB,agent 收到問題時找相關 chunk 塞進 context。

標準 RAG pipeline

1. 文件 → chunk(500-1500 token / chunk)
2. chunk → embedding(OpenAI text-embedding-3 / Cohere / 本地)
3. embedding → vector DB(Pinecone / Weaviate / pgvector / Chroma)
4. 查詢時:query → embedding → similarity search → top-K chunk
5. 把 top-K chunk 塞進 prompt 給 LLM

💡 步驟 1 的隱形坑:你的「文件」可能是 PDF / docx / pptx / xlsx / image。直接餵 raw bytes 不行——LLM 看不到 binary。推薦 microsoft/markitdown(122K★, MIT)一鍵把 17 種格式(PDF / Office / image / audio / HTML)轉成乾淨的 markdown,再切 chunk。pip install markitdown && markitdown report.pdf > report.md

簡化範例(chromadb)

python
import chromadb
from chromadb.utils import embedding_functions

client = chromadb.PersistentClient(path="./chroma_db")
embedder = embedding_functions.OpenAIEmbeddingFunction(api_key=os.environ["OPENAI_API_KEY"])

collection = client.get_or_create_collection("company_docs", embedding_function=embedder)

# 索引文件
collection.add(
    documents=["產品 A 規格...", "公司假期政策...", "客戶 X 過往案例..."],
    metadatas=[{"src": "doc1.md"}, {"src": "hr.md"}, {"src": "crm.md"}],
    ids=["d1", "d2", "d3"],
)

# 查詢
results = collection.query(
    query_texts=["請假怎麼申請"],
    n_results=3,
)
# results['documents'][0] = ["公司假期政策...", ...]

Vector DB 選擇

DB性質適合
Chroma嵌入式、Python first開發階段 / 小規模
pgvectorPostgres extension已用 PG / 想單一 DB
Pinecone雲端託管不想自運維
Weaviate自託管 / 雲端需要 hybrid search(vector + keyword)
Qdrant高性能、Rust 寫大規模 / 自託管

6. Contextual Retrieval — Anthropic 2024 的優化

標準 RAG 有個常見問題:chunk 失去上下文。

例如某條 chunk 是「該功能於 2026 年 3 月起停用」——但「該功能」指什麼?前面的 chunk 才講。

Contextual Retrieval(Anthropic 2024)做法:embed 之前,先讓 LLM 給每個 chunk 加一段 50-100 字「這個 chunk 在文件中的位置 + 主題」

python
def contextualize(chunk, full_doc):
    resp = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=100,
        messages=[{"role": "user", "content": f"""
給定整篇文件:
{full_doc}

給定其中一個 chunk:
{chunk}

請用 50 字繁中說明這個 chunk 在文件中的位置 + 主題。直接給答案不解釋。
"""}],
    )
    return resp.content[0].text

# 索引時:
for chunk in chunks:
    ctx = contextualize(chunk, full_doc)
    enriched = f"[CONTEXT: {ctx}]\n\n{chunk}"
    collection.add(documents=[enriched], ...)

Anthropic 報告 retrieval 召回率提升 35-49%。成本:每 chunk 多 1 次 LLM call(用 cheap model + prompt caching 大幅減)。


6a. 2026 production memory 生態 — 不一定要自己造輪

§3-6 你會自己拼 session + long-term + RAG,但 2026 已有成熟的「memory-as-a-service」套件可以直接接。三家代表 + Helix 自家做法對照:

系統定位檢索體量主要訴求
mem0Memory layer APIVector + Graph53K★簡潔 API、cloud / self-host 都行
Letta / MemGPTFull agent runtime(自帶 memory)Vector22K★OS-style agent + archival memory + agent runtime 一體
agentmemoryCross-agent MCP memory serverBM25 + Vector + Graph (RRF fusion)4.9K★(2026-02 起)16+ agent 共用一個 memory server (Claude Code / Cursor / Hermes / OpenClaw...)、Session Replay
cocoindexLong-horizon agent 增量 indexing engineIncremental embedding refresh9.6K★大型 codebase / doc corpus 加 file 不用全 reindex;適合 agent 連續跑幾天的場景
Helix MemoryProject-aware persistent memory(本書 V3 case study 用)PG JSONB + pgvector + FTS5 (CJK)自家跟 V3 audit / replay / project boundary 整合

怎麼選

  • 個人專案 / 學習:先自己拼(§3-6)。理解 mechanic 比裝套件重要。
  • 想跨 agent 共用(Claude Code 跟 Cursor 看到同一份 memory)→ agentmemory(MCP 即插即用)
  • 要 cloud-managed 不想自己跑mem0(有 SaaS)
  • 要 agent runtime 一體(連 agent loop 都託付)→ Letta
  • 要跟自家 audit / replay / project boundary 強整合→ 自己寫(像 Helix Memory)

Benchmark caveat

agentmemory 自家 README claim LongMemEval-S R@5 95.2% vs mem0 68.5% / Letta 83.2%(後兩個其實是 LoCoMo dataset,比較不嚴格)——這是供應商自家數字,第三方還沒重現驗證。Ch 12 練習 13.3 contextual retrieval A/B 是同款方法論,自己跑你的資料才知道哪家適合。

💡 學習路徑建議:本章先自拼 (§3-6) → Ch 15 production governance → 上 production 前再決定要不要切到 mem0 / Letta / agentmemory。不要學第一章就先裝套件——失去理解 memory 怎麼運作的機會。


7. 對齊 ai-dict 名詞

本章相關 ai-dict 詞條(繁中版):

  • Section 2 — Sessions, Context Windows & Turns:context window / compaction
  • Section 6 — Memory & Guidance:persistent memory / RAG / contextual retrieval

8. 動手練習

練習 13.1:Session memory 包進 agentz_mini

擴 Ch 12 的 Agent class 加 session_messages 屬性,run(user_message) 後保留對話。實作 SUMMARIZE_THRESHOLD 自動壓縮。

成功標準:跑 30 輪對話,context size 始終 < 50K token。

練習 13.2:用 Chroma 做 RAG

挑你 5-10 個文件(doc / blog / SOP),切 chunk、embed 進 chroma,寫一個 agent 能依問題撈相關 chunk 回答。

成功標準:問一個只能從你私人文件答得出的問題,agent 答對。

練習 13.3:Contextual Retrieval A/B 對比

在練習 13.2 基礎上加 contextual retrieval(每 chunk 加 50 字 context),跑同一組問題 5 個,比較有/沒 contextualize 的召回率。

成功標準:用表格顯示 5 題的 contextualize on/off 結果,能說出哪個版本好、為什麼。


9. 你做完這一章後 ✅

  • [ ] 知道 LLM 沒記憶的本質、為何要 memory 層
  • [ ] 知道 session / long-term / RAG 三種 memory
  • [ ] 會做 session message 壓縮 / 摘要
  • [ ] 寫過 SQLite-backed long-term memory
  • [ ] 跑過 chromadb RAG pipeline
  • [ ] 知道 Contextual Retrieval 為什麼有用
  • [ ] 跑完練習 13.1 / 13.2 / 13.3

打勾 5 個以上,進 Ch 14 — Multi-agent


9a. 常見地雷

地雷症狀解法
chunk 切太大 / 太小retrieval 不準中文 200-500 字 / 英文 500-1000 token, 重疊 10-15%
沒 contextualizechunk 失去上下文加 50-100 字 chunk-position context(§6 contextual retrieval
embedding model 換掉舊資料 retrieval 突然全錯embedding model + version 寫死,每次升級全 re-embed
mixed language中英混搭召回率差用 multilingual model (cohere multilingual / openai 3-large)
沒 metadata filter答案被無關文件淹沒where={"category": "policy"} 等 filter
k 太大 / 太小k=20 太雜、k=3 漏掉從 k=5 起手、看 result 調
沒 reranktop-k 順序很爛加 cohere rerank / cross-encoder 二次排序
session memory 沒截斷第 30 輪對話 context 撞 200K用 summarizer agent 每 10 輪壓縮成 1 段
vector DB 改路徑重啟後 collection 消失PersistentClient(path=...) 用絕對路徑 + 不要砍
chunk overlap 沒設跨 chunk 邊界資訊掉overlap 50-100 char / 50 token
PII 直接進 vector DB隱私外洩索引前 mask(姓名/電話/email/身分證), 或用本地 embedding
RAG 蓋掉 LLM 知識簡單問題給超詳細 RAG 答案LLM 判斷「要不要查 RAG」, 簡單問直答

9b. 在這頁練 contextual retrieval 的 prompt

Contextual Retrieval(§6)的核心是「給 chunk 加一段 50-100 字的位置 + 主題」。試試看:

Ch 13 in-page tryout — contextual retrieval

10. 補充閱讀


🛟 卡關時看這裡

MIT License — 章節內容跟 starter code 都可以 copy 進你自己的商業專案