2026-08-26 · 上下文专题 · 业界项目剖析(第一篇) 剖析对象:mem0ai/mem0: Universal memory layer for AI Agents(Apache-2.0) 本地源码:
/root/projects/mem0/(浅克隆,本剖析所有行号均实测自该副本) 与专题主线的连接:业界"LLM 主判"记忆路线的代表,对照见文末第七节。
Mem0 是给 AI agent 加"记忆层"的开源项目:对话进来不存原文,由 LLM 提取成一条条事实;写入时先对已有记忆做"增/改/删"决策;读取时多信号混合检索。
它不是向量数据库,也不是 RAG 框架,而是架在 LLM + 向量库之上的一层"记忆语义层"——核心贡献是写时思考:每次 add 都是一次记忆对账,而不是无脑追加。
- 出身:YC S24 孵化,前身是 MemGPT(记忆分页思想的商业化)
- 形态:polyglot monorepo —— Python SDK(
mem0ai)+ TypeScript SDK + 自托管 server(FastAPI + pgvector + Neo4j)+ CLI + 各类 agent 集成 - 可插拔生态:LLM×24、向量库×30、embedding×15、图库×4、重排器×5(全部 provider 化,一个 base 抽象类 + 每 provider 一个文件)
Mem0 的"记忆"不是单一存储,是三层分工:
| 层 | 存什么 | 作用 |
|---|---|---|
| 向量库 | 提取后的事实条目(embedding) | 检索主战场(30 种向量库可换) |
| 图库(可选) | 实体-关系(4 种图库可换) | 关系感知检索,补充向量而非替代 |
| SQLite | 原始对话消息 + 记忆历史 | 可追溯、去重、审计 |
关键点:原始对话也落库(db.save_messages)——即使 LLM 没提取出任何事实,对话也留痕。提取层出问题,原始层兜底,这是防信息丢失的最后防线。
add() 走 _add_to_vector_store()(mem0/memory/main.py:879),V3 流水线(:916 起):
Phase 0 上下文收集 取该 scope 最近 10 条消息(记忆不是只看这一句) :919-921
Phase 1 现有记忆 新对话 embedding → 向量检索已有记忆 top10 :923-931
记忆 id 从 UUID 映射成整数(防 LLM 幻觉编 id) :933-938
Phase 2 LLM 提取 把 [新消息 + 已有记忆 + 最近消息] 一次喂给 LLM, :940-989
输出 JSON:ADD / UPDATE / DELETE 指令 + hash
Phase 3 批量 embedding 提取出的记忆文本 :991-1003
Phase 4-5 hash 去重 + 落库 :1005+
灵魂在 Phase 2——LLM 输出的是三类指令,不是一条追加:
ADD:全新事实,直接写入UPDATE:与已有记忆冲突/过时,覆盖旧条目(用户先说"住北京"后说"搬去上海",结果是改,不是并存两条矛盾记忆)DELETE:用户明确否定/改主意,删除旧记忆
这个"提取 + 对账"是 Mem0 与普通向量记忆的本质区别:存原文就没有"更新"概念,只有提取成结构化事实,才能做增改删。
设计细节(源码实锤):
- 单次调用:已有记忆(top10)+ 新消息 + 最近 10 条历史,一次 LLM 调用全部决策,不逐条问(省成本)
- 上下文注入:
infer=True(默认)才走提取;infer=False则消息原样入库(:880-914),是个可用的逃生门 - 错误不静默:LLM 提取失败抛
LLMError(:963-969),让调用方区分"没提取出事实"和"LLM 挂了",不吞错 - 失败兜底:提取为空时仍保存原始消息(
:986-989),对话不丢
search()(main.py:1379)参数:top_k(默认 20)/ threshold(默认 0.1)/ rerank / explain / filters。
流程:向量检索 → 阈值门槛 → 可选 rerank。
底层的混合打分在 mem0/utils/scoring.py:60 score_and_rank:
combined = (semantic + bm25 + entity_boost) / max_possible
- 语义分(向量):主信号,且先过 threshold 门槛才进入融合——语义上不相关的候选,BM25/实体加成拉不回来
- BM25(关键词):专有名词、代码、人名这类向量不敏感的 token 靠它兜底
- 实体加成(entity boost):与记忆中的实体链接命中的加权
打分顺序有讲究:门槛在融合之前。这不是加分排序,是"先筛后加"——语义底线守不住,其他信号没有资格参与排序。
① 为什么提取事实,不存原文? 记忆密度。原文是噪音,事实是信号。"我今天吃了碗面"不该占一条记忆。只有提取成结构化事实,才能做 UPDATE/DELETE 这种语义级操作。
② 为什么写时就要检索已有记忆? 解决记忆矛盾。没有这一步,新旧事实只会堆积冲突;有这一步,每次写入都是一次记忆对账。代价是写成本高:每次 add 多一次向量检索 + 一次 LLM 调用(且要喂已有记忆)。
③ 为什么 user/agent/run 三层作用域?
记忆不是全局一锅粥。用户 A 的偏好不能污染用户 B;agent 的程序性记忆与用户事实分开。search() 强制要求至少一个作用域(:1457),防止跨主体串记忆。
- UUID→整数映射(
:933-938):把记忆 id 换成0,1,2...再喂 LLM,防止模型幻觉编造不存在的 id 去 UPDATE——LLM 对短整数比对 UUID 老实得多 - hash 去重(
:1007):同一事实被重复提取时,hash 相同直接跳过,防止记忆通胀 - 原始消息落 SQLite(
:988):提取层与原始层分离,可追溯、可审计 - 时间能力在 OSS 版是关闭的:
timestamp/reference_date直接抛ValueError(:817-818、:1432-1433),只有expiration_date(过期隐藏)可用——时间感知检索是托管平台专有能力,README 明说"OSS 版方向一致、数字不同"
Mem0 是业界**"LLM 主判"记忆路线**的代表作,与总纲第一篇《记忆分层规则引擎:决策思路复盘》正好是两种路线:
| 维度 | Mem0(LLM 主判) | 我们的设计(规则主判) |
|---|---|---|
| 判定主体 | LLM 每次 add 现判 | 规则引擎确定性分流(宪法)+ LLM 兜底(司法解释) |
| 可审计 | ❌ 说不清"为什么记这条" | ✅ rule_chain 可回放 |
| 生命周期分层 | ❌ 无 L1/L2/L3,只有可选 expiration_date | ✅ 三层存储 + TTL + 后验修正 |
| 冲突处理 | UPDATE/DELETE(LLM 判,黑盒) | 用户纠正 P3 覆盖一切(确定性) |
| 写成本 | 每次 add 一次 LLM 调用 + 一次检索 | 规则零成本,LLM 只碰灰色地带 |
结论:Mem0 的"写时增改删"和"提取事实"思路值得抄,它的 UPDATE/DELETE 机制本质上就是我们设计的 P3(用户纠正覆盖)的自动化弱化版——但"判定不可审计、无生命周期先验、写成本高"是它的结构性短板。我们的规则引擎路线,正是针对这些短板的批判性升级。
反过来,Mem0 也验证了我们的判断:提取层(LLM 切句)与判断层(分层)必须分离——Mem0 把两件事都压在 LLM 一次调用里,导致无法解释、无法修正;我们让 LLM 只做提取(切事实),分层交给可测试的规则。
mem0/memory/main.py 核心:add(:760) / search(:1379) / _add_to_vector_store(:879)
mem0/memory/storage.py 图库 + 向量库的读写封装(graph + vector 双写)
mem0/memory/base.py 记忆工具对外的工具接口(给 agent 调用的 schema)
mem0/utils/scoring.py 混合打分(semantic + bm25 + entity boost)
mem0/llms/ 24 个 LLM provider
mem0/vector_stores/ 30 个向量库 provider
mem0/graphs/ 4 个图库 provider(可选图记忆层)
evaluation/ memory-benchmarks 子模块(LoCoMo / LongMemEval / BEAM)
建议阅读顺序:先 main.py 的 add(写路径 5 Phase)→ scoring.py(读路径打分)→ 再回 main.py 的 search 串全链路。图记忆层(graphs/)是进阶,理解向量层之后再碰。
本文行号基线:/root/projects/mem0/ 浅克隆副本(2026-08-26)。仓库演进后行号可能漂移,以 grep -n "def add" mem0/memory/main.py 重新定位为准。