大多數人給 Agent 加記憶的方式,是把對話歷史一路 append 進 prompt,等到爆 context 了再加一個摘要函式。 真正的答案是:記憶不是「更長的上下文」,它是一個獨立的儲存與檢索問題——你需要決定什麼值得記、記成什麼形狀、怎麼在下一次對話中只取回相關的那幾條。 把歷史全塞進去只需要一行
messages.append()。記憶層需要一整套子系統。 這個系列拆解的是後者。
前言:這個系列要做什麼
Mem0 (Apache-2.0,2023-06 開源)是目前最多人用的開源 AI Agent 記憶層:GitHub 上約 6.5 萬顆星、7.6 千 fork,官方定位是「drop-in memory infrastructure for AI agents and apps」。
它值得逐層讀完,理由不是星星數,而是:它把「Agent 記憶」這個很虛的詞,收斂成了兩個非常具體的工程問題——什麼進來、什麼出去。 而且它的答案在 2026 年做過一次大幅度的自我否定(v2 → v3),把原本論文裡的兩階段 ADD/UPDATE/DELETE 管線整個換掉。讀懂那次改動為什麼發生,比讀懂任何單一功能都有價值。
這個系列分成五篇:
| Part | 主題 | 對應原始碼 |
|---|---|---|
| Part 1(本篇) | 全景架構、兩條路徑、作用域模型、部署形態 | mem0/memory/main.py、mem0/configs/base.py |
| Part 2 | 寫入路徑:ADD-only 萃取管線的八個階段 | Memory._add_to_vector_store()、configs/prompts.py |
| Part 3 | 讀取路徑:語意 + BM25 + 實體的多訊號融合 | Memory._search_vector_store()、mem0/utils/scoring.py |
| Part 4 | 儲存層與後端選型:三個 store、25 種向量庫、自架 server | mem0/vector_stores/、mem0/memory/storage.py、server/ |
| Part 5 | 生產部署:OSS vs Platform、進階能力、評測與踩坑 | docs/、evaluation/ |
本篇的目標很單純:讀完之後,你能在腦中畫出 Mem0 的方塊圖,說得出一句「我喜歡靠走道的位置」是怎麼變成一筆可被檢索的記憶,並且知道 user_id / agent_id / run_id 這三個參數各自該放什麼。
本文以 mem0 main 分支(2026 年 9 月,pyproject.toml 版本 2.0.20,對應官方所稱的 v3 記憶演算法)為準。這個專案迭代極快且剛經歷破壞性改版,細節請以你 pip install 的版本為準。
一、核心問題:messages.append() 在哪裡壞掉
先講清楚 Mem0 存在的理由。一個「教學版 Agent 記憶」長這樣:
history = []
def chat(user_input):
history.append({"role": "user", "content": user_input})
resp = llm.chat(history) # ← 整段歷史每次都重送
history.append({"role": "assistant", "content": resp})
return resp
這條路徑在 demo 上完美運作,在真實產品上會在四個地方同時壞掉。
1.1 成本是二次方成長的
第 1 輪:送 100 token
第 2 輪:送 300 token
第 3 輪:送 600 token
...
第 N 輪:送 ≈ N²/2 × 平均輪長
100 輪對話、每輪 200 token:
累計送出 ≈ 100 × 100 × 200 = 200 萬 token
而「有用的資訊」可能只有 30 條事實,約 1,500 token
你為了記住 1,500 token 的東西,付了 200 萬 token 的錢。 這不是優化問題,是架構問題。
1.2 四個壞點
壞點一:Context window 是有上限的,而對話沒有。 128K 看起來很大,但一個用了三個月的助理、一份跑了 200 輪的 agent trace、一個客服帳號的完整歷史,都會輕鬆超過。到那時你需要一個淘汰策略——而「淘汰最舊的」幾乎總是錯的,因為使用者最重要的偏好往往是第一次對話說的。
壞點二:長上下文會稀釋注意力。 這是實證問題不是理論問題:把一句「我對花生過敏」埋在 80K token 的中段,模型找到它的機率顯著低於放在開頭或結尾。塞得越多,模型越容易忽略關鍵的那一句。
壞點三:跨 session 就斷了。 使用者關掉分頁,明天回來,history 是空的。你得自己決定要把什麼持久化、怎麼持久化、以及下次怎麼載回來——這正是 Mem0 在做的事,只是多數人會先自己寫一個很爛的版本。
壞點四:摘要會遺失結構。 常見的補救是「每 20 輪叫 LLM 摘要一次」。問題是摘要是有損且不可逆的:一旦「使用者住在里斯本」被摘進一段敘述裡,你就沒辦法在使用者說「我搬到柏林了」的時候精準地更新那一條——你只能再摘要一次,而誤差會累積。
Mem0 的四個核心主張,正好一一對應:
| Mem0 主張 | 對應壞點 | 實作位置 |
|---|---|---|
| 萃取事實而非保存逐字稿 — 把對話蒸餾成一條條可獨立檢索的事實 | 壞點一、四 | _add_to_vector_store() |
| 按查詢取回,而非全量重播 — 每次只取相關的 top-k | 壞點二 | _search_vector_store() |
| 持久化到外部儲存 — 向量庫 + SQL + 實體庫 | 壞點三 | mem0/vector_stores/、storage.py |
| 作用域隔離 — user / agent / run 三層識別 | 壞點三 | _build_filters_and_metadata() |
官方文件把價值主張總結成一張對照表,我覺得它比任何架構圖都說得清楚:
| 沒有記憶層 | 有 Mem0 |
|---|---|
| 把對話歷史不斷接到 prompt 後面 | 事實只存一次,之後按查詢取回 |
| 讓模型重讀舊的對話輪次 | 只給模型相關的那幾條記憶 |
| session 結束就失去上下文 | 用 user_id / agent_id / run_id 與 metadata 劃分作用域 |
而它付出的代價寫在官方評測文件的第一段:平均每次檢索呼叫用掉不到 7,000 token,而全上下文方法在同樣的 benchmark 上通常要 25,000+ token。 這個「token 效率」才是 Mem0 真正的賣點——不是準確率更高,是在可接受的成本下維持準確率。
二、全景圖:兩條路徑,三個儲存
以下是 Mem0 的方塊圖。每個方塊標了對應的原始碼位置,之後四篇會逐個拆開。
┌──────────────────────────────────────────────────────────┐
│ 你的應用程式 / Agent │
│ │
│ 互動結束後 ──┐ ┌── 呼叫模型之前 │
└───────────────┼────────────────────┼──────────────────────┘
│ add() │ search()
▼ ▼
╔═══════════════════════════╗ ╔═══════════════════════════╗
║ 寫入路徑(Extraction) ║ ║ 讀取路徑(Retrieval) ║
║ ║ ║ ║
║ ① 情境蒐集(近 10 則訊息)║ ║ ① 查詢預處理(詞形還原、 ║
║ ② 既有記憶檢索(top 10) ║ ║ 實體抽取) ║
║ ③ 單次 LLM 萃取(ADD-only)║ ║ ② 語意檢索(over-fetch) ║
║ ④ 批次嵌入 ║ ║ ③ 關鍵字檢索(BM25) ║
║ ⑤ hash 去重(md5) ║ ║ ④ 實體加權 ║
║ ⑥ 批次寫入向量庫 ║ ║ ⑤ 加權融合排序 ║
║ ⑦ 實體抽取與連結 ║ ║ ⑥ 取 top-k ║
║ ⑧ 寫 history + 訊息窗 ║ ║ ║
╚═════════════╤═════════════╝ ╚═════════════╤═════════════╝
│ │
┌─────────────┴──────────────────────────────┴─────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ 向量資料庫 │ │ 實體儲存 │ │ SQL 資料庫 │
│ (Qdrant 預設) │ │ (同一個向量庫的 │ │ (SQLite 預設) │
│ │ │ 另一個 collection)│ │ │
│ · 記憶文字 │ │ · 實體文字 + 向量 │ │ · history 事件記錄 │
│ · 嵌入向量 │ │ · entity_type │ │ · messages 滾動窗 │
│ · metadata │ │ · linked_memory_ids │ │ │
│ · hash │ │ │ │ │
│ · text_lemmatized │ │ │ │ │
├──────────────────┤ ├────────────────────┤ ├────────────────────┤
│ 語意相似 + 關鍵字 │ │ 實體共現加權 │ │ 稽核軌跡 + 去重情境 │
└──────────────────┘ └────────────────────┘ └────────────────────┘
│
┌─────────────────────────┴─────────────────────────┐
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ LLM 供應商 │ │ Embedding 供應商 │
│ 萃取用(20 種) │ │ 向量化用(12 種) │
│ mem0/llms/ │ │ mem0/embeddings/ │
└────────────────────┘ └────────────────────┘
六個元件,一句話各自的職責:
Memory/AsyncMemory(mem0/memory/main.py) — 整個 OSS 版本的門面,也是唯一一個 3,868 行的巨型檔案。所有公開 API(add/search/get/get_all/update/delete/delete_all/history)都在這裡,兩個類別各實作一次同樣的邏輯(同步與非同步)。- 向量資料庫 — 記憶的主儲存。注意這點與多數人的直覺不同:Mem0 的事實文字本身就存在向量庫的 payload 裡,不是只存向量。 預設 Qdrant,可換 25 種。
- 實體儲存 — 不是一個獨立的資料庫,而是同一個向量庫裡的另一個 collection(
_entity_collection_name()產生名稱)。每個實體一列,payload 帶linked_memory_ids反向指回記憶。 - SQL 資料庫(
mem0/memory/storage.py) — 預設~/.mem0/history.db(SQLite)。兩張表:history記錄每次 ADD 事件(稽核用),messages保存最近的原始訊息(給下一次萃取當去重情境用)。 - LLM — 只在寫入路徑用到,做事實萃取。預設 OpenAI,支援 18 種供應商(含 Anthropic、Bedrock、Ollama、LM Studio、vLLM、LiteLLM)。
- Embedder — 寫入與讀取都用到。預設
text-embedding-3-small,支援 12 種。
2.1 三個最容易被誤解的架構事實
事實一:Mem0 不是一個資料庫,是一個編排層。 它自己不存任何東西,所有持久化都委託給你配置的後端。這代表它的可靠性上限等於你選的向量庫的可靠性上限——選 FAISS(本機檔案)跟選 Qdrant Cloud,是完全不同的生產等級。
事實二:讀取路徑完全不呼叫 LLM。 search() 只做嵌入、向量檢索、BM25、實體加權、排序。沒有 LLM 呼叫,所以它很快也很便宜。 昂貴的是 add()。這個不對稱決定了你該把 add() 放在請求路徑之外(見 2.2)。
事實三:v3 之後,OSS 版本沒有圖資料庫了。 舊版可以接 Neo4j / Memgraph / Kuzu / Apache AGE 做 graph memory,v3 把這個整合從 OSS 移除,改成內建的「實體連結」——仍然抽取實體、仍然用實體加權排序,但沒有可查詢的圖、沒有 relations 欄位。完整的圖記憶成為 Platform 的功能。這是遷移時最容易踩的一個雷,Part 5 會詳述。
2.3 為什麼要三個儲存而不是一個
一個很自然的問題:記憶就是一段文字,為什麼要拆成三個地方存?
答案是三種查詢模式,而沒有任何一種資料庫三種都擅長:
| 你想問的問題 | 需要的查詢 | 誰來答 |
|---|---|---|
| 「使用者對飲食有什麼偏好?」 | 語意近似 | 向量庫(餘弦相似) |
| 「他提過的那個專案代號 PX-441 是什麼?」 | 精確詞匹配 | 向量庫的 BM25 索引(text_lemmatized 欄位) |
| 「關於 Alice 我們知道些什麼?」 | 從實體反查記憶 | 實體庫(linked_memory_ids 反向索引) |
| 「這條記憶是什麼時候、從哪次對話寫進來的?」 | 時序稽核 | SQL 的 history 表 |
| 「上一次萃取時他剛講過什麼?」 | 最近 N 筆原始訊息 | SQL 的 messages 表 |
第三行是關鍵,也是純向量方案做不到的:「Alice」這個詞可能根本不出現在「她推薦了那家拉麵店」這條記憶的文字裡,但只要寫入時把兩者連過,實體查詢就能撈到它。這是 Mem0 在 v3 移除外部圖資料庫之後,用來保留部分圖能力的替代方案——保留了「共現加權」,放棄了「多跳遍歷」。
第四、五行則說明為什麼還需要一個關聯式資料庫:向量庫不擅長「按時間排序列出事件」,而稽核與去重情境都需要這種查詢。用 SQLite 解決是最省事的選擇。
2.2 add() 該放在哪裡
因為 add() 要跑 LLM 萃取,它的延遲是秒級的。正確的放法:
❌ 錯誤:擋在使用者面前
使用者發問 → search() → LLM 回答 → add() → 回傳給使用者
↑ 使用者多等 1–2 秒
✅ 正確:回應之後非同步做
使用者發問 → search() → LLM 回答 → 立刻回傳
└─▶ 背景工作:add()
官方評測文件的第一階段就叫 "Store New Memories:
Conversation enters the pipeline asynchronously (after the agent responds)"
OSS 提供 AsyncMemory 讓你在 asyncio 環境裡 await,但**「非同步方法」不等於「非同步架構」**——你仍然需要把它丟到背景 task 或佇列,而不是 await 在回應路徑上。
三、一個事實的生命週期
把方塊圖換成時間軸。假設使用者在對話中說了一句「我下個月要去里斯本,我對花生過敏」。
t=0 使用者送出訊息,Agent 正常回答並回傳(使用者已經看到答案)
│
│ ┌─ 背景工作:m.add(messages, user_id="u_123") ──────────────────┐
│ │ │
t=0 ├─▶ ① 情境蒐集 │
│ │ db.get_last_messages(session_scope, limit=10) │
│ │ ← 從 SQLite 撈最近 10 則訊息,給 LLM 當背景 │
│ │ │
│ ├─▶ ② 既有記憶檢索 │
│ │ 把這次的對話整段嵌入,向量庫 top_k=10 │
│ │ ← 目的是「別重複記」,不是「要更新誰」 │
│ │ UUID 被映射成 "0","1","2"… 再交給 LLM(防幻覺) │
│ │ │
t≈0.8s├─▶ ③ 單次 LLM 萃取(唯一一次 LLM 呼叫) │
│ │ ADDITIVE_EXTRACTION_PROMPT,回傳 JSON: │
│ │ {"memory": [ │
│ │ {"text": "User is travelling to Lisbon next month"}, │
│ │ {"text": "User is allergic to peanuts"} │
│ │ ]} │
│ │ ← 注意:只有 ADD,沒有 UPDATE / DELETE │
│ │ │
│ ├─▶ ④ 批次嵌入 embed_batch([...], "add") │
│ │ │
│ ├─▶ ⑤ hash 去重 │
│ │ md5(text) 比對既有 hash 與本批內 hash,撞了就丟掉 │
│ │ │
│ ├─▶ ⑥ 批次寫入向量庫 │
│ │ payload = {data, text_lemmatized, hash, created_at, │
│ │ updated_at, user_id, ...} │
│ │ ← text_lemmatized 是給 BM25 用的詞形還原版本 │
│ │ │
│ ├─▶ ⑦ 實體抽取與連結(spaCy,不是 LLM) │
│ │ "Lisbon" → PROPER "peanuts" → TOPIC │
│ │ 在實體庫查:精確命中,或語意相似度 ≥ 0.95 │
│ │ ├─ 命中 → 把新的 memory_id 併進 linked_memory_ids │
│ │ └─ 未命中 → 新增一列實體 │
│ │ │
│ └─▶ ⑧ 寫 history(event="ADD")+ 保存原始訊息到滾動窗 │
t≈1.2s │
└──────────────────────────────────────────────────────────────┘
────────── 三週後,使用者換了一個 session ──────────
t=0 使用者問:「幫我找里斯本的餐廳」
│
│ ┌─ m.search(query, filters={"user_id": "u_123"}) ──────────────┐
│ ├─▶ 詞形還原 + spaCy 抽出查詢實體 "Lisbon" │
│ ├─▶ 嵌入查詢,向量庫 over-fetch(max(top_k×4, 60) 筆) │
│ ├─▶ BM25 關鍵字檢索(若後端支援) │
│ ├─▶ 實體庫查 "Lisbon" → 拿到 linked_memory_ids → 加權 │
│ ├─▶ 三個訊號相加後除以自適應分母,排序取 top-k │
│ └─▶ 回傳 │
t≈120ms │
└──────────────────────────────────────────────────────────────┘
│
├─▶ 你把結果組進 prompt:
│ 「已知關於使用者:下個月要去里斯本、對花生過敏」
│
▼
LLM 回答時自動避開有花生的餐廳 —— 而使用者從來沒有再提過一次
三個細節值得單獨標出來:
細節一:② 的既有記憶檢索,目的不是「找出要改哪一條」。 在 v2 它是——LLM 會被要求對每一條既有記憶決定 ADD / UPDATE / DELETE / NOOP。v3 把它降級成純粹的去重參考:提示詞明確寫著「Use these ONLY for deduplication and linking — do NOT extract new memories from Existing Memories」。這個降級是整個 v3 改版的核心,Part 2 會解釋為什麼。
細節二:⑦ 的實體抽取用 spaCy,不用 LLM。 這是一個很務實的選擇:實體抽取是一個成熟的 NLP 任務,用 en_core_web_sm 這種小模型做,延遲是毫秒級、成本是零。抽三類東西:專有名詞(大寫多字序列)、引號內文字、以及帶特定修飾語的複合名詞片語。代價是它是英文導向的——中文語料上這條訊號的效果會弱很多,Part 3 會談。
細節三:搜尋只花 120ms,因為完全沒有 LLM。 這讓你可以在每次呼叫模型前都 search 一次,而不用擔心延遲預算。
四、作用域模型:三個 ID 該怎麼配
這是 Mem0 API 裡最容易用錯的部分,而用錯的代價是記憶洩漏到別的使用者身上。
4.1 三個識別碼
┌────────────────────────────────────────────────────────────────┐
│ user_id —— 跨所有 session 持續存在的「這個人」 │
│ 例:u_8f3a、或你系統裡的使用者主鍵 │
│ 存:偏好、過敏、職稱、長期目標、家庭成員 │
├────────────────────────────────────────────────────────────────┤
│ agent_id —— 「這個 AI 助理自己」 │
│ 例:travel_agent、code_reviewer │
│ 存:這個 agent 的人設、學到的工作方法 │
├────────────────────────────────────────────────────────────────┤
│ run_id —— 「這一次 session / 任務」 │
│ 例:一次規劃行程的完整對話 │
│ 存:只在這次任務內有意義的中間狀態 │
└────────────────────────────────────────────────────────────────┘
至少要給一個。組合起來會收窄範圍:
user_id + run_id = 「這個人在這次對話裡的事」
原始碼裡對這件事非常嚴格(_validate_and_trim_entity_id):ID 會被 trim,空字串或只有空白會直接 ValueError,含內部空白也會被拒。這是刻意的防呆——一個 user_id=" " 會讓所有使用者的記憶混在同一個桶裡。
4.2 一個 v3 的陷阱:add() 與 search() 的參數位置不一樣
這是遷移到 v3 最常見的 crash:
1# add():三個 ID 是「頂層關鍵字參數」
2m.add(messages, user_id="u_123") # ✅
3
4# search() / get_all():三個 ID 必須放進 filters
5m.search("里斯本", filters={"user_id": "u_123"}) # ✅
6m.search("里斯本", user_id="u_123") # ❌ ValueError
原始碼裡有一個專門的守衛函式 _reject_top_level_entity_params(kwargs, "search") 來擋這件事。為什麼要搞得不一致?因為 search 的 filters 還支援 metadata 過濾與布林組合,把實體 ID 也收進去讓語意統一:filters 的意思永遠是「限定在哪個範圍內找」。
filters 支援的運算子相當完整:
1m.search("專案進度", filters={
2 "AND": [
3 {"user_id": "u_123"},
4 {"category": {"in": ["work", "project"]}},
5 {"created_at": {"gte": "2026-01-01"}},
6 {"NOT": [{"source": "imported"}]},
7 ]
8})
9# 支援:eq / ne / in / nin / gt / gte / lt / lte / contains / icontains
10# 萬用字元 "*"、以及 AND / OR / NOT 組合
4.3 記憶型別:只有一種真的存在
Mem0 的 MemoryType 列舉裡有語意記憶、情節記憶、程序記憶,但只有 procedural_memory 真的被實作——其他值傳進去會在驗證階段被拒絕:
1if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
2 raise Mem0ValidationError(...)
程序記憶(_create_procedural_memory())是給 agent 記「怎麼做一件事」的步驟流程,需要 agent_id,且只有 Python OSS 的 Memory 類別支援。
除此之外,所有記憶都是同一種東西——一條文字事實加上 metadata。「這是偏好還是事件」不是用型別區分的,是用 metadata 或 Platform 的 categories 區分的。這個設計選擇讓 API 很小,代價是你得自己在 metadata 裡建分類體系。
五、三個演進階段
同一個 Mem0,在不同規模下該長成完全不同的樣子。
╔═══════════════════════════════════════════════════╗
║ Phase 1:POC —— 函式庫模式、單機、< 1 萬筆記憶 ║
╚═══════════════════════════════════════════════════╝
┌────────────────────────────────────────────────┐
│ 你的 Python 程式 │
│ ┌──────────────────────────────────────────┐ │
│ │ from mem0 import Memory │ │
│ │ m = Memory() │ │
│ │ │ │
│ │ 預設堆疊: │ │
│ │ · LLM OpenAI │ │
│ │ · Embedder text-embedding-3-small │ │
│ │ · Vector Qdrant(本機 / 記憶體內) │ │
│ │ · History ~/.mem0/history.db (SQLite) │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
1pip install "mem0ai[nlp]" # [nlp] 才有 spaCy,實體訊號才會啟用
2python -m spacy download en_core_web_sm
3export OPENAI_API_KEY=sk-...
1from mem0 import Memory
2
3m = Memory()
4m.add([{"role": "user", "content": "我對花生過敏"}], user_id="u_123")
5print(m.search("飲食限制", filters={"user_id": "u_123"}))
- 可接受的捷徑:全部跑在同一個進程、SQLite 在本機檔案、Qdrant 可以用記憶體內模式、沒有備份、
add()同步呼叫。 - 成本:每次
add()一次 LLM 呼叫(約 $0.001–0.01,視模型與對話長度)+ 兩次 embedding 批次。每次search()只有一次 embedding(約 $0.00002)。 - 能撐多久:比你以為的久。一個使用者累積一年也很難超過幾千條記憶,本機 Qdrant 處理十萬筆向量毫無壓力。這個階段不要過早上雲。
- 還沒解決什麼:程序重啟就得重建(除非把 Qdrant 指到持久化路徑);沒有多進程支援;
add()擋在請求路徑上會讓使用者等。
一個必踩的坑:不裝 [nlp] extras 的話,extract_entities() 會直接回傳 []——實體訊號整條靜默失效,搜尋品質下降但不會有任何錯誤訊息。裝 spaCy 模型是必要步驟,不是選配。
╔═══════════════════════════════════════════════════╗
║ Phase 2:MVP —— 自架 server、pgvector、多應用共用 ║
╚═══════════════════════════════════════════════════╝
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Web App │ │ Slack Bot│ │ 排程任務 │
└─────┬────┘ └─────┬────┘ └─────┬────┘
└─────────────┼─────────────┘
│ REST
┌─────────▼──────────────────────┐
│ Mem0 Server (FastAPI) │
│ server/main.py :8000 │
│ · JWT 認證 (auth.py) │
│ · 限流 (rate_limit.py) │
│ · Alembic 遷移 │
└─────────┬──────────────────────┘
│
┌─────────────┴──────────────┐
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ PostgreSQL │ │ LLM / Embedding │
│ + pgvector (pg17) │ │ 供應商 │
│ 向量 + 實體 + 歷史 │ │ │
└────────────────────┘ └────────────────────┘
相對 Phase 1 新增的元件:
| 新增 | 為什麼 |
|---|---|
server/ 的 FastAPI 服務 | 多個應用(web、bot、批次)要共用同一份記憶,不能各自抱一個本機 SQLite |
| PostgreSQL + pgvector | 一個資料庫同時當向量庫、實體庫、歷史庫——少一個要維運的系統 |
| Alembic 遷移 | schema 會變,尤其在 v2→v3 這種改版上 |
| JWT 認證 + 限流 | 記憶層存的是個人資料,不能裸奔 |
add() 丟進佇列 | 把秒級延遲移出請求路徑 |
| 監控:LLM 呼叫量、記憶成長曲線 | 成本會從這裡失控 |
官方的 server/docker-compose.yaml 已經把這套配好:pgvector/pgvector:pg17 + uvicorn + alembic。
- 成本 delta:一台小型 Postgres($50–200/月)+ LLM 萃取成本。後者才是大頭,且與對話量成正比。1 萬次
add()/天、每次 3K token 輸入 → 每天約 3,000 萬 token,用便宜的小模型也要每月數百美元。 - 複雜度 delta:從「一個 import」變成「一個要部署的服務 + 一個資料庫 + 一套遷移流程」。
- 解決了什麼:多應用共用、持久化、可備份、可稽核。
- 還沒解決什麼:沒有圖記憶、沒有時間推理、沒有記憶衰減、沒有背景整合——這些在 v3 之後都是 Platform 專屬。而 ADD-only 架構會讓記憶只增不減,三個月後你會發現同一個使用者有 40 條語意相近的偏好。
╔═══════════════════════════════════════════════════╗
║ Phase 3:Scale —— 多租戶、大量記憶、品質治理 ║
╚═══════════════════════════════════════════════════╝
┌──────────────────────────────────────────────┐
│ API Gateway:租戶認證、配額、稽核 │
└───────────────────┬──────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 讀取路徑 │ │ 寫入佇列 │ │ 背景整合作業 │
│ search() │ │ (Kafka / SQS) │ │ │
│ 無 LLM、~100ms │ │ worker 池跑 add()│ │ · 去重合併 │
│ 可水平擴充 │ │ 可控制 LLM QPS │ │ · 過期清理 │
└───────┬───────┘ └────────┬─────────┘ │ · 摘要壓縮 │
│ │ │ · 品質抽查 │
└─────────┬─────────┴────────────┴──────────────────┘
▼
┌───────────────────────────────────────────────────┐
│ 受管向量庫(Qdrant Cloud / Pinecone / pgvector) │
│ 依租戶分 collection 或以 filter 隔離 │
└───────────────────────────────────────────────────┘
│
┌──────────────▼────────────────────────────────────┐
│ 資料治理:PII 偵測、刪除權(GDPR)、保留期限 │
│ delete_all(user_id=...) 必須真的刪乾淨 │
└───────────────────────────────────────────────────┘
相對 Phase 2 新增的元件:
| 新增 | 為什麼 |
|---|---|
| 讀寫路徑分離擴充 | search() 無 LLM 可以瘋狂擴;add() 受 LLM 供應商 rate limit 限制,要靠佇列削峰 |
| 背景整合作業 | ADD-only 架構的必要補償。沒有它,記憶會無限增長並互相干擾 |
| 租戶隔離策略 | 用 filter 隔離(簡單但共用索引)vs 用 collection 隔離(貴但乾淨),要選一個 |
| PII 與刪除權流程 | 記憶層是個資的集中地。GDPR 的刪除請求必須能穿透向量庫、實體庫、歷史庫三處 |
| 品質評測迴圈 | 用自己的資料跑 LoCoMo 式評測,確認換模型/改提示詞沒有讓召回退化 |
- 成本 delta:向量庫成本隨記憶數線性成長,但 LLM 萃取成本才是主導項,且它與對話量成正比而非記憶量。
- 複雜度 delta:這是質變。你現在維運的是一個有狀態、存個資、且品質無法用單元測試驗證的系統。
- 解決了什麼:規模、隔離、合規、以及 ADD-only 的長期熵增。
- 還沒解決什麼:最難的那題——怎麼知道記憶層有沒有在幫倒忙。取回一條過時的偏好,比沒有記憶更糟。
什麼時候該考慮直接用 Platform 而不是自架? 三個訊號任一出現:(a) 你發現自己在寫背景去重與「哪條才是最新」的邏輯——那正是 Platform 的 Dream 與 Temporal Reasoning 在做的事;(b) 你需要可查詢的圖記憶——OSS 沒有了;(c) 你的團隊沒有人想維運一個向量庫。Part 5 有完整對照。
六、Benchmark:該怎麼讀那些數字
Mem0 官方公布的評測結果(v3 演算法,2026 年):
| Benchmark | 分數 | 平均 token/查詢 |
|---|---|---|
| LoCoMo | 92.5(舊版 71.4) | 6,956 |
| LongMemEval | 94.4(舊版 67.8) | 6,787 |
| BEAM (1M) | 64.1 | 6,719 |
| BEAM (10M) | 48.6 | 6,914 |
四個讀法:
讀法一:關鍵欄位是右邊那一欄,不是左邊。 官方自己說得很直白:「Most AI agent memory systems retrieve information by maximizing context window size. That works on benchmarks but not in production, where every token adds cost.」在 LoCoMo 上拿 92 分不難,難的是用 7K token 拿 92 分,而全上下文方法要 25K+。
讀法二:BEAM 才是有意義的那個。 LoCoMo 與 LongMemEval 的規模小到「把 context window 開大」就能作弊。BEAM 在 1M 與 10M token 規模測試,而 10M 的總分掉到 48.6——這才是記憶系統在真實資料量下的實際水準。從 1M 到 10M,時序推理從 61.8 崩到 16.3、事件排序從 53.6 崩到 20.2。官方誠實地說這是全領域的未解問題。
讀法三:注意那條免責聲明。 官方文件明寫:「Scores reflect Mem0’s managed platform, which includes proprietary optimizations not available in the open-source SDK. Open-source users should expect directionally similar gains but not identical numbers.」你自架的 OSS 不會拿到這些分數。
讀法四:ADD-only 的代價寫在 LongMemEval 的分項裡。 「Knowledge update」是所有分項中最低的 93.6,官方的解釋是:「older facts are preserved rather than overwritten, so semantically similar prior facts can still surface alongside newer ones.」這一條值得記住——它是 Part 2 與 Part 5 的核心張力。
還有一個更根本的問題:這些 benchmark 全是英文的。中文語料上,spaCy 的 en_core_web_sm 抽不出實體、BM25 的詞形還原也不適用——多訊號檢索會退化成單一語意檢索。Part 3 會給幾個補救方向。
七、為什麼選 Mem0 不選 X
選擇 選 Mem0 的理由 不選對方的理由 / 對方何時更好
────────────────────────────────────────────────────────────────────────────────
Mem0 token 成本降一個量級(7K vs 25K+) 全上下文:實作零成本,
vs 全上下文 跨 session 持久化 無資訊遺失,短對話下
注意力不被稀釋 準確率是天花板
─────────────────────────────────────────────────────────────
翻轉條件:對話總長穩定在 20K token 以內、且不跨 session——
直接全塞。別為了一個 10 輪的客服對話架記憶層。
Mem0 專為「事實記憶」設計:萃取、去重、 自建 RAG:你完全掌控,
vs 自建 RAG 實體連結、作用域隔離都內建 沒有黑箱,能接受任意
一週能上線 schema
─────────────────────────────────────────────────────────────
翻轉條件:你要記的東西不是「對話中的事實」而是「文件」——
那是 RAG 的領域,用 RAGFlow / LlamaIndex 之類的專門工具。
記憶與檢索增強是兩件事:前者的輸入是對話,後者的輸入是語料。
Mem0 專注做好記憶這一件事,可以嵌進任何 LangMem / 框架內建記憶:
vs 框架內建記憶 框架(LangGraph、CrewAI、AutoGen…) 與框架整合更緊、概念一致
後端選擇多達 25 種向量庫 ─────────────────────────
─────────────────────────────────────────────────────────────
翻轉條件:你已經全押在某個框架上、且它的記憶模組夠用——
少一個依賴永遠是好事。當你需要跨框架共用同一份記憶時再換。
Mem0 輕量、無狀態編排層、易嵌入 Zep / Letta(MemGPT) 等
vs 其他記憶服務 OSS 與 Platform 同一套 API 專用系統:時序圖模型
遷移路徑清楚 或自主記憶管理更成熟
─────────────────────────────────────────────────────────────
翻轉條件:你的核心需求就是「事實隨時間怎麼演變」的時序查詢,
那些以時序知識圖為核心的系統在模型上更貼合。Mem0 OSS 在 v3
之後反而退掉了圖能力。用你的真實對話各跑一次再決定。
Mem0 OSS 資料留在自己手上、成本可控 Mem0 Platform:圖記憶、
vs Mem0 Platform 可換任意 LLM 與向量庫 時間推理、記憶衰減、
無廠商鎖定 Dream 背景整合都是
───────────────────────────────────────────────────────────── Platform 專屬
翻轉條件:見 Part 5 的完整對照表。簡短版:如果你開始自己寫
「哪條記憶才是最新」的邏輯,你正在重造 Platform 的功能。
Mem0 成熟、社群大、整合多(LangGraph、 什麼都不做:對很多產品
vs 不做記憶 CrewAI、MCP、Claude Code…) 而言,記憶帶來的體驗
───────────────────────────────────────────────────────────── 提升不值得這份複雜度
翻轉條件:如果你說不出「記住 X 之後,使用者的下一次互動會
具體變好在哪裡」,就先別做。記憶層會引入一整類新的失敗模式:
記錯、記過時的、記到別人的——**取回一條錯的記憶比沒有記憶更糟**。
八、系統效應:Mem0 相對 naive 做法改了什麼
| 環節 | naive 做法(append 歷史) | Mem0 做法 | 效果 |
|---|---|---|---|
| 輸入 | 整段逐字稿重送 | 萃取成獨立事實 | 100 輪對話從 200 萬 token 降到約 1,500 token 的事實庫 |
| 每次查詢的成本 | 隨輪數二次方成長 | 固定 ≈ 7K token(官方評測值) | 成本從發散變成常數 |
| 跨 session | 斷掉 | 持久化到向量庫 | 三個月後仍記得第一次說的偏好 |
| 檢索 | 無(全給) | 語意 + BM25 + 實體三訊號融合 | 只給相關的,注意力不被稀釋 |
| 更新 | 靠摘要,有損且不可逆 | ADD-only,新舊並存 | 保留時序脈絡,但需要外部治理 |
| 去重 | 無 | md5 hash + LLM 層去重指引 | 同一句話不會存兩次 |
| 隔離 | 靠你自己 | user / agent / run 三層作用域 | 不會把 A 的記憶給 B |
| 稽核 | 無 | SQLite history 記錄每次 ADD | 「這條記憶哪來的」可回答 |
| 實體關聯 | 無 | spaCy 抽實體 + 反向連結 | 「關於 Alice 的事」能一次撈齊 |
| 後端 | 綁死 | 25 種向量庫 / 18 種 LLM / 12 種 embedder | 不被任一供應商鎖定 |
九、系列導航
本篇建立了地圖:兩條路徑、三個儲存層、三個作用域識別碼、三個部署階段,以及那組該小心解讀的 benchmark 數字。
接下來四篇逐層下鑽:
- Part 2 — 寫入路徑:
_add_to_vector_store()的八個階段逐行拆解、為什麼 v3 放棄了論文裡的 ADD/UPDATE/DELETE/NOOP 四動作模型、UUID 映射成整數的防幻覺技巧、md5 去重的邊界、實體連結的批次化,以及一次add()到底花多少錢。 - Part 3 — 讀取路徑:九步檢索流程、BM25 的 sigmoid 正規化與查詢長度自適應參數表、實體加權的完整公式(含那個
1/(1+0.001×(n-1)²)的懲罰項)、score_and_rank的自適應分母,以及explain=True怎麼用來調參。 - Part 4 — 儲存層與後端選型:三個 store 的實際 schema、25 種向量庫裡哪 15 種支援 BM25、SQLite 的 history 與 messages 兩張表、配置系統的完整結構,以及自架 server 的部署。
- Part 5 — 生產部署:OSS 與 Platform 的完整差異表、Graph Memory / Temporal Reasoning / Memory Decay / Dream 四個 Platform 專屬能力、v2→v3 破壞性變更遷移清單、ADD-only 的長期代價與補償策略,以及生產檢查清單。
→ Mem0 Intro Part 2 — 寫入路徑 — ADD-only 萃取管線與那次自我否定
本文基於 mem0 main 分支(2026 年 9 月,pyproject.toml 版本 2.0.20)原始碼與官方文件撰寫。所有函式名稱、參數預設值與流程階段皆從 mem0/memory/main.py、mem0/configs/ 與 docs/ 實際核對;成本與延遲數字為量級估算。Mem0 剛經歷 v2→v3 的破壞性改版且仍在快速演進,細節請以你安裝的版本為準。
