研究日期:2026-07-23(Asia/Shanghai)
仓库:mem0ai/mem0
源码快照:ca2abca2b884e038d3e525070e79d3057ef2012c
Python 包版本 / 最新正式版本:2.0.13/v2.0.13(2026-07-22)
TypeScript OSS 包版本 / 最新正式版本:3.1.1/ts-v3.1.1(2026-07-22)
版本与实时校准
本文研究 2026-07-23 抓取的 main。当日 GitHub API 快照为 61,505 stars、7,158 forks、683 个 open issues + PRs;数字只用于固定研究时点,不作为质量结论。
当前快照最后一个提交为:
ca2abca2b884e038d3e525070e79d3057ef2012c
fix(ts-oss/milvus): skip '*' wildcard filter values instead of matching literally (#6508)
2026-07-23T00:41:45+05:30
v2.0.13 的 Python release 同一天发布,主要修复 local Qdrant reset、Pinecone namespace reset、Mochow L2 分数方向、structured OpenAI 环境变量,以及 update metadata 改写 identity 等问题。托管 client 中一个“已转发但从未影响检索”的 retrieval_criteria 参数也被移除。这些 release 细节说明 mem0 现在最值得观察的不只是模型 prompt,而是 provider contract、身份边界和跨存储一致性。
对原 HTML / 旧稿最重要的更新
旧稿的标题“为什么新版记忆算法不再修改旧事实”过度概括,必须纠正:
- 只有自动抽取路径是 ADD-only:
Memory.add(..., infer=True)不再让 LLM 输出 UPDATE / DELETE; - Python OSS 仍公开提供
update()、delete()、delete_all()、reset()与history(); expiration_date可以让结果在默认 search / get_all 中隐藏,但不会物理删除记录;- Platform v3 还有 Temporal Reasoning、Memory Decay、
latest_only/delete_linked等托管能力,不能反推到 OSS; - 2025 年的 Mem0 论文和旧的 ADD / UPDATE / DELETE 决策架构,不再与 2026 年 4 月合并的 OSS v3 主路径一一对应。
更准确的中心论点是:
ADD-only 是自动写入链的一次降复杂度重构。它把“本轮该不该覆盖旧事实”的第二次 LLM 决策移出热路径,却把冲突、增长、时间、删除和跨存储一致性推给了检索与显式治理。
研究提纲
核心论点
mem0 v3 的关键变化不是“选择了 append-only 哲学”,而是重新分配了不确定性:
- 模型只负责从新消息中抽取可复用事实;
- 确定性代码负责 embedding、精确 hash 去重、写入与实体索引;
- semantic、BM25、entity 三路信号在读取时决定哪些事实更值得出现;
- 应用负责显式 update、delete、expiration 与合规治理;
- vector store、SQLite history、recent messages、entity collection 之间没有统一事务。
模型少做了一次 diff,系统没有因此变简单;复杂度从“写入时改哪一条”迁到了“读到矛盾时相信哪一条,以及怎样真正清掉它”。
主要资料来源
- Python OSS 当前默认分支源码与测试;
- OSS v2 → v3 migration guide;
- PR #4805:v3 ADD-only、混合检索、内建实体关联与 8 阶段 batch pipeline;
- Issue #4970:prompt 输出的
linked_memory_ids在 Python / TypeScript OSS 中未被消费; - Issue #4863:新进程中的 delete / update 可能跳过 entity cleanup;
- Issue #4988:entity cleanup 每次最多全扫 10,000 条;
- Issue #6512:Python OSS 的 user-scoped delete 不清除 SQLite history 中的 PII;
- memory-benchmarks:当前 benchmark 的 ingest → search → answer / judge 方法;
- Mem0 论文:历史研究背景,不作为当前 v3 代码说明书;
v2.0.13/ts-v3.1.1release 与 GitHub API 元数据。
关键源码入口
mem0/memory/main.py::Memory.addmem0/memory/main.py::Memory._add_to_vector_storemem0/configs/prompts.py::ADDITIVE_EXTRACTION_PROMPTmem0/memory/main.py::Memory.searchmem0/memory/main.py::Memory._search_vector_storemem0/utils/scoring.py::score_and_rankmem0/utils/entity_extraction.pymem0/utils/lemmatization.pymem0/utils/spacy_models.pymem0/memory/storage.py::SQLiteManagermem0/memory/main.py::Memory._update_memorymem0/memory/main.py::Memory._delete_memorymem0/vector_stores/base.py::VectorStoreBasemem0/vector_stores/qdrant.pymem0/configs/base.py::MemoryConfig
结论先行
mem0 是一层可嵌入的 agent memory infrastructure,不是完整 agent,也不是一张自动保持“当前真相”的 profile 表。
当前 Python OSS 最准确的三层结构是:
- 事实与召回层:主 vector collection,payload 内保存 memory text、hash、identity、时间和 metadata;
- 辅助排序层:同一 vector provider 中的
{collection}_entities,以及 provider 可选的 BM25 / full-text 能力; - 审计与短期上下文层:本地 SQLite 的
history与每个 session scope 最近 10 条messages。
官方 how-it-works 页面仍把 SQL 写成“事实 source of truth”,platform-vs-oss 页面还写 OSS 使用 external graph store;这两处与当前 Python OSS 代码和 v2 → v3 migration guide 不一致。本文以固定源码与 migration 为准,并把 Platform、Python OSS、TypeScript OSS 分开。
产品层、架构层、代码层
产品层
mem0 对应用暴露的核心工作流很小:
一次有价值的交互之后:add(messages, user_id / agent_id / run_id)
下一次模型调用之前:search(query, filters={...})
需要治理时:update / delete / delete_all / reset / history
开发者可以用默认 OpenAI LLM + embedding、embedded Qdrant 快速起步,也可以替换 LLM、embedder、vector store 和 reranker。默认配置并不等于本地离线:默认 LLM 与 embedder provider 都是 OpenAI,默认 vector store 才是本地 /tmp/qdrant。
架构层
application
│
├─ add()
│ ├─ LLM extractor
│ ├─ embedder
│ ├─ main vector collection
│ ├─ entity vector collection
│ └─ SQLite history + recent messages
│
├─ search()
│ ├─ semantic candidates
│ ├─ provider keyword / BM25 scores
│ ├─ entity boosts
│ └─ optional reranker
│
└─ explicit governance
├─ update / delete / expiration
└─ reset / history
这些部件由 Python 进程顺序协调,不共享一个跨后端 transaction。所谓“memory record”其实同时投影到多个存储;任何生产部署都要自己回答重试、对账、备份与彻底删除的问题。
一条真实 add 请求
1. 身份先于事实
Memory.add() 要求 user_id、agent_id、run_id 至少有一个。identity 同时进入 metadata 与 vector filters,用来隔离召回范围。当前 timestamp 明确是 Platform-only,OSS 传入会报错;expiration_date 则由 OSS 规范成 YYYY-MM-DD。
输入可以是字符串、单条 message 或 message list。vision message 会先经过专门解析;若指定 memory_type="procedural_memory" 且有 agent_id,则走 procedural summary 的旁路。普通 infer=True 才进入 v3 pipeline。
2. Phase 0–1:只拿一小圈上下文
系统从 SQLite 取当前 session scope 最近 10 条消息,然后把本轮 message list 解析成文本。它用整段文本生成 query embedding,从主 vector store 取 top-10 既有 memories。
这 10 条同时承担两件事:
- 给 LLM 做语义去重与关联参考;
- 给后续 MD5 精确去重提供 existing hash 集合。
因此精确去重不是全库唯一约束。若一条完全相同的旧 memory 没进 top-10,它仍可能再次写入。
3. Phase 2:一次 LLM 调用,只做抽取
system prompt 的角色非常明确:
Your sole operation is ADD
它要求从 user 与 assistant message 中抽取可复用事实,并避免把已有 memory 本身当成新证据。LLM transport error 会重新包装为 LLMError 抛给调用方,方便上层 retry;但空响应、坏 JSON 或解析异常会记录错误、保存 recent messages,然后返回空结果。两类失败有不同语义。
代码把旧 memory UUID 临时映射为 "0"、"1" 等短 ID,注释称其目的为 anti-hallucination:
uuid_mapping[str(idx)] = mem.id
existing_memories.append({"id": str(idx), "text": ...})
但当前主路径之后从未读取 uuid_mapping。prompt 又要求 LLM 输出 linked_memory_ids,而 Phase 4 构造 payload 时只消费 text 与 attributed_to。Issue #4970 对这个断层的描述与固定快照代码一致:模型生成的 memory-to-memory link 是未落盘的幽灵字段。
真正生效的“linking”发生在 Phase 7,是另一套基于 spaCy entity 的 entity-to-memory 索引。
4. Phase 3–5:batch embedding 与有限精确去重
抽取出的 memory texts 先走 embed_batch();batch 失败后逐条 embed,某条失败就跳过该 memory。
随后确定性代码:
- 对 text 计算 MD5;
- 与 top-10 existing hashes 比较;
- 与本批次
seen_hashes比较; - 生成新 UUID;
- 保存原文、lemmatized text、hash、created / updated time、identity 与
attributed_to。
MD5 在这里不是安全签名,只是精确内容指纹。语义近似事实能否不重复,主要依赖前面的 top-10 召回和 LLM 判断。
5. Phase 6:batch persist 没有提交清单
主 vector batch insert 失败后,代码逐条重试;每条重试失败只记 error,不从 records 中移除。接下来的 history、entity linking 与返回值仍遍历原始 records:
records = 两条候选
batch insert 失败
├─ memory A 单条重试成功
└─ memory B 单条重试失败
history = A + B
return = A + B
本次最小 mock 探针在固定快照上复现了这一行为:第二条 vector 最终未写入,_add_to_vector_store 仍返回两条 ADD,batch_add_history 也收到两条。反方向同样可能发生:vector 已写成功,history 写失败后逐条重试仍可能失败,但请求继续返回成功。
这是典型的多存储一致性问题,不是“batch 性能优化”的小尾巴。调用方若需要 exactly-once 或可审计写入,必须在 mem0 外层增加 idempotency、读后确认或 reconciliation。
6. Phase 7:所谓 graph 是扁平实体倒排
若安装 spaCy 与 en_core_web_sm,mem0 会从每条 memory 中抽取:
- named entities;
- proper names;
- quoted text;
- technical identifiers;
- multi-word topic phrases。
全批次先按规范化 text 去重,entity text 再 batch embed。系统在 {collection}_entities 中做 exact lookup 与 top-1 semantic lookup;相似度至少 0.95 时合并,否则新增 entity record:
{
"data": "Shopify",
"entity_type": "PROPER",
"linked_memory_ids": ["memory-a", "memory-b"],
"user_id": "alice"
}
它没有 entity-to-entity edge、relation type 或图遍历 API。旧 relations 返回字段已经删除。因此“built-in graph memory”更准确的工程描述是:一个实体倒排 collection,为共享实体的 memories 提供排序 boost。
7. Phase 8:保存最近消息
SQLite messages 表按 session_scope 保存 role、content、name、created_at,每次只保留最近 10 条。它是下一次抽取的短期上下文,不是用户可见的完整 transcript。
history 表保存 ADD / UPDATE / DELETE 的 old / new value、时间、actor 与 role;它没有 user / agent / run identity 列。这一 schema 直接解释了为什么 delete_all(user_id=...) 无法按用户物理清除历史。
一条真实 search 请求
1. search filter 是租户边界的一部分
search() 要求 filters 中至少有 user_id、agent_id、run_id 之一;旧式 top-level identity kwargs 会报错。高级 metadata operators 会交给不同 vector store adapter 转换,因此兼容程度受 provider 影响。
默认参数是:
top_k = 20
threshold = 0.1
rerank = False
explain = False
2. 三路信号不是三个候选集
_search_vector_store() 的步骤是:
lemmatize query + extract at most 8 entities
→ query embedding
→ semantic search,internal_limit = max(top_k × 4, 60)
→ provider.keyword_search()
→ BM25 sigmoid normalization
→ entity boost
→ 只从 semantic results 构造 candidates
→ score_and_rank
→ optional reranker
最容易误读的点是:BM25 与 entity 都只是 boost signals,不是 recall expanders。一个只被精确关键词命中、却没进 semantic over-fetch 的 record,无法进入最终结果。Migration guide 已明确写出这条边界。
3. 分数公式里的三个细节
score_and_rank() 使用:
combined = (semantic + normalized_bm25 + entity_boost) / max_possible
其中:
- semantic threshold 在融合前生效;
- BM25 通过与 query term count 相关的 sigmoid 归一;
- entity boost 最大权重为
0.5; max_possible根据当前是否存在任意 BM25 / entity score,在1.0、1.5、2.0、2.5之间变化。
最后一点意味着,只要本轮候选集中有一条拿到 BM25 或 entity signal,所有候选都会使用更大的分母。没有获得该 signal 的 candidate 会被相对压低。这是一种简单的全局归一,不是学习得到的 calibrated probability;版本迁移后不能沿用旧 cosine threshold。
4. entity boost 会惩罚 hub entity
查询最多取 8 个去重 entity,每个 entity 在 entity store 中取最多 500 个匹配。相似度低于 0.5 不加分。每条 memory 的多个 entity boost 取最大值,不累加:
boost = similarity × 0.5 × 1 / (1 + 0.001 × (linked_count - 1)²)
linked_count 越大,权重越低,避免 “Google”“project” 一类 hub entity 把数百条 memory 一起抬高。
5. graceful degradation 依赖 provider 组合
如果 vector store 没实现 keyword_search(),初始化会 warning,search 退到 semantic。Qdrant 还需要 fastembed 创建 BM25 sparse vector slot。
若 spaCy 不可用:
- entity extraction 返回空;
- lemmatizer 返回原始 text;
- entity boost 消失;
- 某些支持 native keyword search 的 store 仍可能在原始 text 上运行 BM25。
所以 migration guide 中“没有 spaCy 就 semantic-only”的表述对所有 provider 组合来说过宽;代码更精确的行为是“无 entity、无词形归一,BM25 是否存在取决于 vector adapter 与其依赖”。
自动 ADD-only 与显式治理
update
update(memory_id, text, metadata, expiration_date):
- 先从 vector store 读取旧 record;
- identity keys 不能改写或注入;
- text 改变时重新 embedding、hash 与 lemmatize;
- vector store update;
- SQLite 写 UPDATE history;
- text 改变时清理旧 entity links,再对新 text 重新关联。
它不是跨存储 transaction。若 vector update 成功、history 或 entity cleanup 失败,状态会分裂。
delete 与 delete_all
delete() 先删除主 vector,再写 DELETE history,最后尽力移除 entity record 中的 memory id。Entity cleanup 被刻意设计为 non-fatal,不能破坏主删除路径。
两个当前开放 issue 与源码相符:
_remove_memory_from_entity_store()在_entity_store is None时直接返回,因此一个刚启动、还没触碰 entity store 的进程可能留下 stale link(#4863);- cleanup 会
list(..., top_k=10000)再在 Python 扫描,超过 10,000 entities 时既慢又可能不完整(#4988)。
delete_all(user_id=...) 会列出该 scope 的 memories,逐个调用 delete。它不会清除 SQLite history 的原文。Issue #6512 把这点与 GDPR / PII 风险联系起来,并请求 Python 增加类似 Node disableHistory 的开关。
reset() 才会丢弃全部 history / messages 与 vector collections,但它不是按用户治理工具。
expiration
expiration_date 只是读取时的软过滤。过期 memory 默认不出现在 get_all / search,show_expired=True 可以重新看到,底层 vector、history 与 entity link 不会自动删除。
同步与异步
Memory 与 AsyncMemory 各自复制了完整 8 阶段 pipeline。异步版把 blocking provider calls 放进 asyncio.to_thread(),entity searches 使用 semaphore / gather;主逻辑基本同构。
测试覆盖了两边的 entity embedding count guard、identity immutability、entity boost 并行、history 与 delete_all race 修复。但“复制两份大函数”仍是一项维护成本:未来某个 fallback、metadata 字段或 error path 很容易只修一边。
代表性代码片段
1. ADD-only 的边界在自动抽取,不在整个 API
response = self.llm.generate_response(
messages=[
{"role": "system", "content": ADDITIVE_EXTRACTION_PROMPT},
{"role": "user", "content": user_prompt},
],
response_format={"type": "json_object"},
)
后面只把抽取结果变成新 UUID;显式 update() / delete() 是另一条 API 路径。
2. BM25 与 entity 不能把候选“救回来”
candidates = []
for mem in semantic_results:
candidates.append(...)
scored_results = score_and_rank(
semantic_results=candidates,
bm25_scores=bm25_scores,
entity_boosts=entity_boosts,
...
)
semantic 是 candidate generator;另外两路只是 ranker features。
3. 部分写入失败没有 reflected commit set
try:
vector_store.insert(all_records)
except Exception:
for record in all_records:
try:
vector_store.insert(record)
except Exception:
log_error()
write_history(all_records)
return all_records
稳健实现应该维护 committed_records,让 history、entity linking 与 response 只消费确认写入的集合,或用 outbox / reconciliation 明确 eventual consistency。
与相邻方案的区别
| 方案 | 主要 memory 单位 | 写入治理 | 检索结构 | 最独特的边界 |
|---|---|---|---|---|
| mem0 | 扁平事实 record | 自动 ADD-only + 显式 CRUD | semantic candidates + BM25 / entity boost | 可嵌入、多 provider |
| OpenHuman | provenance leaf + 多层 summary | admission / compaction | Source Tree + entity / dense | 可下钻回 source_ref |
| Hermes Agent | prompt-prefix memory + sidecar | lifecycle hook | provider-specific recall | 稳定 prompt prefix |
| 传统向量记忆 | text chunk | 应用自行维护 | top-k similarity | 结构最少、运维最简单 |
mem0 的优势是 API 面积小、接入容易、provider 选择多。它不提供 OpenHuman 那样的来源树,也不把 memory 绑定在完整 agent loop 里。代价是应用必须自己定义“当前真相”、删除承诺和跨 store 对账。
适合与不适合
适合
- 客服、销售辅助、个性化助手;
- 需要跨 session 记住偏好、计划、账号或项目事实的 SaaS;
- 想快速在现有 agent 前后插入 memory layer 的开发者;
- 需要自选 LLM、embedder、vector database 的团队;
- 能自己建立 tenant isolation、审计和数据生命周期的工程组织。
不适合直接照搬
- 把 memory 当医疗、法律、财务“当前事实源”的高风险系统;
- 需要严格 transaction、exactly-once 或强合规删除证明的场景;
- 需要关系类型、图遍历、复杂 temporal validity 的知识图谱;
- 只靠默认配置就期待完全本地、完全离线的部署;
- 不愿为不同 vector adapters 做兼容测试和阈值重调的团队。
社会、市场与个人判断
mem0 把“记住用户”从产品功能抽成基础设施,会让越来越多应用共享一种新默认:每次对话不再是一次性 session,而是对用户模型的持续写入。
这会首先打动三类人:
- 开发个性化产品、却不想自己造 memory pipeline 的小团队;
- 已有大量用户交互,希望用历史提高留存与转化的 SaaS;
- 对 agent continuity 有强烈需求的重度 AI 用户。
市场机会不只在“memory API”,还会在 memory observability 与 governance:为什么留下这条、它来自哪里、哪一次检索用过它、删除是否传播、冲突由谁裁决。
最大的社会风险也来自同一个位置。应用一旦把“用户说过什么”持续抽成结构化事实,聊天就从沟通变成隐形 profile 写入。ADD-only 降低了模型静默覆盖历史的风险,却可能放大“永远累计”的风险。真正尊重用户的系统不能只提供一个 delete 按钮;它要证明 vector、entity、history、backup、analytics 与托管衍生数据都遵守同一个删除契约。
局限与未证实项
- 没有运行真实 OpenAI、Anthropic 或本地模型的 extraction quality benchmark;
- 没有启动真实 Qdrant / Pinecone / Milvus 做跨 provider integration;
- 没有独立复现 README 中 LoCoMo、LongMemEval、BEAM 分数;
- README benchmark 是官方 production-representative stack 的报告,不等于默认 OSS 配置成绩;
- Issue #4863、#4988、#6512 是开放社区报告;本文只对其中能由固定源码直接解释的机制做了静态核对;
- 当前代码的
linked_memory_ids未消费是固定快照事实,未来可能通过移除 prompt 字段或真正落盘而改变; - Platform backend 不在本仓库,本文不对其内部 transaction、temporal 或 deletion 实现做推断。
验证记录
当前事实
Python snapshot: ca2abca2b884e038d3e525070e79d3057ef2012c
Python package: 2.0.13
Python release: v2.0.13
TypeScript OSS package: 3.1.1
TypeScript release: ts-v3.1.1
License: Apache-2.0
聚焦测试
项目指导要求使用 Hatch。机器初始没有 hatch,先通过 uvx --from hatch 创建 dev_py_3_12;该环境会安装全部 vector-store / LLM extras,下载 litellm 的 maturin build dependency 时网络中断,未进入测试。为了不让无关 provider 阻塞核心验证,后续使用项目声明的最小 test extra:
uv run --extra test pytest \
tests/utils/test_scoring.py \
tests/memory/test_main.py -q
结果:
69 passed in 17.92s
扩展运行:
uv run pytest \
tests/test_memory.py \
tests/memory/test_storage.py \
tests/utils/test_entity_extraction.py \
tests/utils/test_lemmatization.py -q
初次结果:
73 passed, 19 skipped, 1 failed
唯一失败是最小环境没有可选 langchain-core,测试却显式传入 custom LangChain LLM。补齐该可选依赖后单测通过:
uv run --with 'langchain-core>=0.3.85,<1.0.0' pytest \
tests/test_memory.py::test_async_procedural_memory_langchain_strips_code_blocks -q
1 passed
19 项 skip 来自未安装 spaCy / en_core_web_sm 的 NLP 路径,不计作通过。
一致性探针
另写了一个临时 mock test,完成后已删除。它让:
- LLM 抽取两条 memories,并为两条都返回
linked_memory_ids; - vector batch insert 失败;
- 第一条单独重试成功;
- 第二条单独重试失败;
- entity extraction 为空。
断言确认:
- 方法仍返回两条 ADD;
- SQLite batch history 仍收到两条;
- vector payload 不包含 LLM 输出的
linked_memory_ids。
结果:
1 passed in 0.60s
去重后的验证总计为 144 passed、19 skipped、0 个未解释的代码断言失败。其中 1 项是本文临时一致性探针。
验证边界
测试证明固定快照的单元契约和 mock failure behavior,不证明:
- 真实 provider 的原子性;
- 多进程并发;
- 大规模 entity cleanup 性能;
- extraction / retrieval 的线上准确率;
- Platform 的内部实现;
- GDPR 删除合规。
中文文章结构
- 从“它不再改旧事实”的误解开场;
- 把 ADD-only 定义为自动路径边界;
- 走通 8 阶段 add pipeline;
- 解释幽灵
linked_memory_ids与真正的 entity linking; - 走通 semantic candidate + BM25 / entity boost;
- 展开 update / delete / expiration;
- 用部分写入与 SQLite PII 说明治理成本;
- 比较 OpenHuman、Hermes 与普通向量库;
- 谁适合 / 不适合;
- 社会、市场与个人思考;
- 三个继续追问的问题。
英文文章结构
- Thesis: ADD-only is a write-path simplification, not an immutable-history guarantee;
- public API and product-layer separation;
- an end-to-end
add()request; - the unused LLM link contract versus entity-backed links;
- hybrid retrieval as candidate generation plus feature fusion;
- sync / async and provider degradation;
- explicit CRUD and soft expiration;
- partial-commit and erasure semantics;
- comparison with provenance trees and agent-native memory;
- operational guidance and production checklist;
- further questions.
三个继续追问的问题
- ADD-only 下的矛盾事实,应该由 time-aware ranking、显式 update,还是用户确认来裁决?
- 怎样把 vector、entity、history、backup 与 analytics 放进一个可证明的删除协议?
- 混合检索应继续使用固定公式,还是按租户、语料与 query type 学习校准权重?
主要外部链接
- mem0 repository
- 固定源码快照
- Python v2.0.13 release
- TypeScript v3.1.1 release
- OSS v2 → v3 migration
- PR #4805: v3 pipeline
- Issue #4970: unused extraction links
- Issue #4863: stale entity cleanup in fresh processes
- Issue #4988: entity cleanup scan
- Issue #6512: history and PII erasure
- Memory benchmarks
- Mem0 paper