[!Mem0 记忆管线的黑话]

  • extraction phase: 从新一轮对话里提炼候选事实, 写入路径的第一步
  • update phase: 候选事实和已有记忆对账, 决定 ADD / UPDATE / DELETE / NOOP
  • Ω\Omega: extraction 吐出的候选记忆集合
  • SS: 异步刷新的全局对话摘要, 提供"远景"
  • tool call: LLM 自己选操作并执行的 function-calling 接口
  • Mem0g^g: 把记忆表示从自然语言换成知识图谱的变体

Mem0 在 memory 光谱里的位置

agent memory 这一堆框架, 区分它们最有用的是两个轴: 记忆的表示 (自然语言 / 向量 / 图 / 时序图) 和记忆的生命周期 (写入 \rightarrow 检索 \rightarrow 更新 \rightarrow 遗忘);

Mem0 base 在表示这一轴上是最朴素的一档: 记忆就是一条条自然语言 fact, 存在一个普通向量库里, 检索走 RAG 式的 ANN; 论文原话是向量库 “employs dense embeddings to facilitate efficient similarity search”, 所以平时说的 “Mem0 dense” 并不是论文给的变体名 (论文只有 Mem0 和 Mem0g^g 两个名字), 而是借 IR 里 dense retrieval 的说法指代它——用稠密向量 embedding 做语义检索, 区别于 BM25 那种 sparse retrieval; 它真正下功夫的地方在生命周期这一轴——怎么选择性地写, 怎么在矛盾时更新和删除;

这里要先把一个很容易混的东西拆开:

  • 写入路径 (memory formation): 对话来了, 怎么把它变成记忆并维护好——也就是 extraction + update 这两个 phase
  • 读取路径 (retrieval): 用户提问时, 怎么从库里捞出相关记忆塞进 prompt

本篇只讲写入路径, 因为这才是 Mem0 真正做"管理"的地方; query 时的检索注入是另一条独立的 RAG 链路, 不在这两个 phase 里;

论文定位

Mem0 不是一个新模型, 不是新的 attention 变种, 也不是一个新的存储引擎; 它是一个架在普通向量库之上的 memory management pipeline; 核心主张是:

  • 与其每轮把整段对话历史塞回 context (full-context, 又贵又会超窗), 不如让 LLM 增量地把对话蒸馏成结构化 fact
  • 让 LLM 自主维护这些 fact, 该加的加, 该改的改, 该删的删
  • 整套维护尽量不拖慢主链路

顺手把一个常见误解先纠了: base Mem0 的创新不在"记忆的内部结构", 而在"记忆的数据流和操作策略" 加上一组很强的 empirical 结果; Mem0g^g 只是把同一套流程的表示换成图, 不是"更高级的正确答案"; 这点后面专门展开;

论文解决的核心问题

现有方案的不足:

  • full-context: 每轮把全部历史重喂, 吃 O(n2)O(n^2) 的 attention 成本, token 账单线性膨胀, 而且对话够长仍然会超窗
  • 朴素 RAG 记忆 (无脑 append): 只增不删, 矛盾的事实共存, 冗余不断堆积, 检索质量随时间退化成垃圾场
  • 固定摘要: 压是压下去了, 但丢细节, 而且不区分"该记 vs 不该记"

Mem0 试图回答:

  1. 怎么只把值得记的抽出来, 而不是整段塞进去?
  2. 新事实和旧记忆矛盾 / 重复时, 谁来裁决加改删?
  3. 这套维护怎么做到不拖慢主链路?

两阶段总览

mem0_pipeline.png

原论文 Figure 2: Mem0 写入路径的总览 — 左半 Extraction Phase 把 Messages 连同 Summary 和 Last mm messages 一起喂给 LLM, 吐出 new extracted memories; 右半 Update Phase 对每条候选去 Database 取 Top-ss similar memories, 再由 LLM 通过 Tool Call 选 ADD / UPDATE / DELETE / NOOP; 中间那个 Database 和异步的 Summary Generator 是两个 phase 共享的基础设施; 这张图是下面 extraction / update 两节的视觉总索引;

一句话读图: 写入路径 = Extraction (提炼候选) \rightarrow Update (对账落库), 中间隔着一个向量库, 旁边挂一个异步摘要器;

Extraction Phase: 在全局背景下只抽新一轮

直觉先行: 这一步像开会时做会议记录——你只记"刚才这一轮说了啥", 但记的时候脑子里得装着整场会的背景, 否则容易记歪, 把代词指错人, 把上下文丢掉;

所以抽取函数 ϕ\phi 吃的不是一条孤立消息, 而是三路上下文拼成的 prompt:

P=(S, {mtm,,mt2}, mt1, mt)P = (S,\ \{m_{t-m}, \dots, m_{t-2}\},\ m_{t-1},\ m_t)

  • 当前消息对 (mt1,mt)(m_{t-1}, m_t) — 真正要抽取的焦点, 也就是最新这轮 user 和 assistant 的交换
  • 对话摘要 SS — 浓缩整段对话历史语义的远景
  • 最近消息窗口 {mtm,,mt2}\{m_{t-m}, \dots, m_{t-2}\} — 提供近期局部的近景, 超参 m=10m = 10

设计意图很干净: 摘要给远景, 滑窗给近景, 消息对是焦点; 后两路只是给抽取提供背景, 它们本身不会被重新抽取——每轮只处理新增内容, 而不是把整段历史每次重抽一遍, 这是个增量设计;

ϕ(P)\phi(P) 产出一组候选记忆:

Ω={ω1,ω2,,ωn}\Omega = \{\omega_1, \omega_2, \dots, \omega_n\}

论文强调它们 “specifically from the new exchange”, 并且是 candidate facts——注意是候选, 不是直接入库;

这里有两个容易看走眼的点:

  • 摘要 SS 是异步生成的: 由一个独立于主链路的模块周期性刷新, 抽取时直接读一份"够新但不保证最新"的缓存摘要, 而不是同步等一次 summarization; 这是典型的用一点 staleness 换 latency
  • 近期窗口不是"筛"出来的: {mtm,,mt2}\{m_{t-m}, \dots, m_{t-2}\} 就是按时间卡最近 10 条, 纯机械滑窗, 没有 LLM 参与; 真正的 LLM 自主判断只发生在 ϕ\phi 决定"哪些 salient"这一步

extraction 只负责产候选, 不去重不消解, 脏活全部留给 update;

Update Phase: 让 LLM 自己当记忆管理员

候选 Ω\Omega 不能直接灌库, 得先和库里已有的记忆对账; 这一步是 Mem0 真正"管理"记忆的地方;

机制上, Ω\Omega 里每一条 ωi\omega_i, 先用 vector embedding 检索 top-ss 条语义最相似的旧记忆 (s=10s = 10); 注意是逐候选做的, 不是把整个 Ω\Omega 一把去比;

拿到"候选 + 它的 10 个邻居"之后, LLM 在四个操作里选一个:

操作 触发条件 (论文原话) 直觉
ADD no semantically equivalent memory exists 没匹配 \rightarrow 新建
UPDATE augmentation of existing memories with complementary information 匹配且互补 \rightarrow 把新信息补进去
DELETE removal of memories contradicted by new information 新信息和旧记忆矛盾 \rightarrow 删掉旧的
NOOP candidate fact requires no modification 不需要动 \rightarrow 什么都不做

最关键的是 DELETE: 很多人会把它和 UPDATE 揉在一起, 但两者语义不同——UPDATE 是互补 (旧记忆没错只是不全), DELETE 是矛盾 (新信息推翻旧记忆); 这俩是不同的语义关系;

实现上还有两个要点:

  • 没有独立分类器: 论文明说 “rather than using a separate classifier, we leverage the LLM’s reasoning capabilities to directly select the appropriate operation”; 也就是 LLM 在 context 里直接推理语义关系来选操作
  • 裁决和执行一气呵成: 选哪个操作是通过 function-calling (tool call) 暴露的, LLM 选完, 系统紧接着执行并改写向量库以 “maintain knowledge base coherence”; 这就是 LLM-as-memory-manager——LLM 既当裁判又当执行者

正是 DELETE 加 NOOP 让 Mem0 区别于"只增不删的朴素 RAG 记忆"——它真正在做 consolidate (合并) 和 forget (遗忘), 这才是"管理"区别于"堆积"的地方;

Mem0g^g: 同一套骨架, 把表示换成知识图谱

mem0g_graph_architecture.png

原论文 Figure 3: Mem0g^g 的图记忆架构 — Extraction Phase 拆成两段, Entity Extractor 先从消息里识别实体 (nodes), Relations Generator 再生成关系 triplets; Update Phase 用 Conflict Detector 对着已有 graph nodes 查矛盾, 再由 Update Resolver 合并进图; 对比 Figure 2 看, 抽取→对账的骨架没变, 变的只是记忆的表示;

Mem0g^g 不是另起炉灶, 而是把同一套"抽取 \rightarrow 对账"骨架整体换成图原生实现; 注意一个常见误解: 它不再用 base 那套 ADD / UPDATE / DELETE / NOOP 的 tool call, 而是两边机制都被换掉了;

节点 (node) 由四部分构成: 实体名, 实体类型 (Person / Location / Event 之类), 语义 embedding eve_v, 以及 metadata (创建时间戳 tvt_v); 其中类型是 LLM 分类出来的, 而 embedding 是算出来的, 时间戳是系统盖的——不是都靠 LLM “认”, 所以"node 的 type/metadata 认识"这句只有 type 那一半归 LLM;

抽取变成两段式: Entity Extractor 先识别实体加类型 (产 nodes), Relations Generator 再用 LLM 从实体和上下文里生成关系三元组 (vs,r,vd)(v_s, r, v_d) (产 edges); 两段都是 prompt 驱动的 LLM stage, 是"先抽实体再抽关系", 不是对边做什么强化;

写入侧 (add) 靠相似度阈值解析节点: 对每条新 triplet, 给源实体和目标实体各算 embedding, 在已有节点里搜语义相似度超过阈值 tt 的; 按节点是否已存在, 系统或建两个新节点, 或建一个, 或复用已有节点, 然后再连边——这一步替代了 base 的 top-ss 检索加四操作;

更新侧靠冲突检测 + 软失效: 新信息进来时 Conflict Detector 找出可能冲突的旧关系, 再由一个 LLM-based update resolver 判断哪些该作废; 关键在于它只把旧关系标记为 invalid, 不物理删除 (“marking them as invalid rather than physically removing them to enable temporal reasoning”);

[!base 硬删 vs Mem0g 软失效]

  • base Mem0 的 DELETE 是硬删: 矛盾就把旧 fact 抹掉
  • Mem0g^g软失效: 旧关系打 invalid 标记后保留, 配合时间戳就能做时序推理 (用户"以前在 A 公司, 现在在 B 公司", 两条都留, 旧的标 invalid)
  • 这正是 Mem0g^g 开始向 Zep / Graphiti 那种时序知识图谱靠拢的地方; base Mem0 没有这个能力

对应回 memory 的"表示"轴, 这是从 vector 挪到 graph 的一步; 而软失效加时间戳, 又在"生命周期"轴上多点亮了 temporal reasoning;

图不是免费的: 论文自己的数据显示, graph 版在多数任务上性价比并不划算 (检索约慢 3x, token 约贵 2x), 主要在关系 / 多跳 / 时序查询上才显出优势; 所以别把 Mem0g^g 当成"更高级的正确答案", 它是一个用更高成本换关系推理能力的 trade-off 变体;

base vs Mem0g^g

维度 Mem0 (base) Mem0g^g
记忆表示 自然语言 fact 实体 + 关系三元组 (vs,r,vd)(v_s, r, v_d)
存储后端 向量库 (dense embedding) 图库 (节点带 embedding)
extraction 产物 Ω\Omega (NL facts) nodes (实体+类型) + relation triplets
写入/更新机制 top-ss 检索 + 四操作 tool call (ADD/UPDATE/DELETE/NOOP) 阈值 tt 节点解析 (建/复用) + Conflict Detector
删除语义 硬删 (物理移除) 软失效 (mark invalid, 留作时序推理)
强项 通用 QA, 低延迟低成本 关系 / 多跳 / 时序推理
代价 关系推理偏弱 更慢更贵

Mem0 vs MemGPT: 管理知识 vs 管理上下文

两篇都顶着"给 LLM 加记忆"的帽子, 但解决的根本不是一回事; 一句话区分: MemGPT 管的是"上下文" (raw content 放在哪), Mem0 管的是"知识" (该记哪些 fact, 怎么对账);

维度 MemGPT Mem0
记忆是什么 原始内容本身 (messages / 文档 / 自写笔记), 分层放在 main / external context 从对话蒸馏出的结构化 fact (NL 或图)
核心动作 分页 (paging): 在 RAM (窗口) 和 disk (外部库) 之间搬运 raw content 抽取 + 对账: 提炼候选 Ω\Omega 再 ADD / UPDATE / DELETE / NOOP
谁来管, 何时管 LLM 自己在推理回合里用 function call 主动触发 (interrupt / heartbeat 驱动), 模型即 kernel 一条独立的后台 pipeline, LLM 只是其中的工具 (抽取器 / 裁决器)
目标 把有限窗口撑成看起来无限, 服务单 session / 长文档 跨 session 的持久, 去重, 无冲突长期记忆, 抠成本和延迟
对待冗余 / 矛盾 不主动去重, 旧内容 FIFO 归档后可召回 显式 DELETE / NOOP 做 consolidate 和 forget

你的直觉基本对, 但要拧准两处:

  • ✅ “MemGPT 提出怎么管理近乎无限的上下文, 靠 compact” —— 对; 它本质是虚拟内存式的 context paging, 外加 LLM 自己做 summarize 和归档
  • ✅ “内存管理依靠 LLM 主动 summarize” —— 对; MemGPT 的 compaction 是模型自发的 (working context 自编辑, FIFO 满了递归摘要), 这正是它的 agentic 特征
  • ⚠️ “MemGPT 没有 Mem0 那种全局 S 加近期记忆的择优思路” —— 半对; MemGPT 其实也有"摘要 + 近期 + 检索"三件套 (working context = 自维护摘要, FIFO = 近期消息, archival = 按需检索), 但它把这三样组织成模型在其间分页的存储层级, 而不是 Mem0 那种"在三路上下文背景下, 把新一轮择优蒸馏成 fact"的抽取配方; 一句话: MemGPT 搬运 raw content, Mem0 把 content 提炼成去重的 fact

所以更准的对照是: MemGPT = 给单个 agent 一块"看起来无限的工作记忆", 模型自己当内存管理员, 管理动作内联在推理回合里; Mem0 = 给系统一个"可扩展的长期知识库", 用一条离线 pipeline 把对话沉淀成干净 fact, 管理动作和推理解耦; 一个偏 OS 的 virtual memory, 一个偏数据工程的 ETL 加去重;

题外话: 图记忆底下的图数据库 (Neo4j)

Mem0g^g 这种图记忆落到工程上需要一个图数据库来存节点和关系边, Neo4j 是最主流的选择 (Graphiti / Zep 也支持它, 以及更轻的 FalkorDB);

Neo4j 是 property graph 模型: 节点带 label 和 properties, 关系有向, 有类型, 还能带属性, 用声明式查询语言 Cypher (MATCH (a:Person)-[:KNOWS]->(b) 字面就是把要找的模式画出来); 分类上它属于 NoSQL 的图家族 (key-value / document / column-family / graph 四大家族之一), 但和早期 NoSQL 不同, 它是完整 ACID 事务;

它最关键的优点是 index-free adjacency (免索引邻接): 每个节点直接存着指向邻居的指针, 多跳遍历就是顺着指针一步步走, 而不是关系库 (MySQL / PostgreSQL 这类) 里那种一层套一层的 JOIN;

关系型 JOIN 图遍历 (Neo4j)
跟一条关系 索引查找 O(logN)O(\log N), NN = 表大小 跟指针 O(1)O(1)
k 跳查询 k 次自连接, 中间结果组合爆炸 代价 \propto 实际走过的节点 / 边数
代价随什么涨 整个数据集大小 NN 只随局部邻域大小
变长路径 要写递归 CTE, 笨且慢 原生 -[:KNOWS*1..5]->

一句话: 关系型 JOIN 的代价随数据总量涨, 图遍历只随你实际访问的那一小片涨; 数据越大, 跳数越深, 图库优势越明显——这正是"多跳关系推理"型图记忆 (Mem0g^g / Zep) 选它的底层原因; 但要诚实, 浅 JOIN (1~2 跳) 且索引建好时关系库并不慢, 图库真正的胜场是深度 / 递归 / 变长遍历;

几个值得留意的设计点

  1. 异步摘要 = 把 summarization 成本从关键路径摘出去, 一个很 systems 的 latency-staleness 取舍
  2. extraction 和 update 关注点分离: 一个只"看新内容提炼", 一个只"对账旧库裁决", 边界干净
  3. 写入路径 \neq 读取路径: 这两个 phase 全在写入侧 (记忆形成); query 时还有一条独立的 RAG 检索注入路径, 别混
  4. LLM-as-memory-manager 的隐性成本: 每条候选都要一次"检索 + LLM tool call", 写入不再便宜——这反过来解释了为什么异步摘要和选择性抽取这么关键
  5. 它本质是 policy 创新而非 mechanism 创新: 向量库, embedding, function calling 全是现成件, Mem0 的价值在于怎么把它们编排成一条写入管线, 并证明这套编排在 LOCOMO 上打赢 full-context 和其他记忆系统
📝 论文主流观点总结

如果按论文主线浓缩, Mem0 的主流观点可以总结为:

  1. agent 长期记忆的瓶颈不是"存不下", 而是"怎么选择性地记 + 持续地维护"
  2. full-context 不可持续 (成本和窗口), 朴素 RAG append 会越长越脏, 两条都不是答案
  3. 把记忆形成拆成 extraction (提炼候选) 和 update (LLM 对账) 两段, 是一条干净且可扩展的写入管线
  4. 让 LLM 自己用 tool call 做 ADD / UPDATE / DELETE / NOOP, 一次推理同时完成裁决和执行, 比独立分类器更灵活
  5. DELETE 和 NOOP 是关键: 它们带来 consolidate 和 forget, 这才是"管理"区别于"堆积"的地方
  6. Mem0g^g 不是 base 的升级版, 而是把表示换成图的 trade-off 变体, 用更高成本换关系 / 时序推理能力
  7. 整篇的贡献是 systems / design 加 empirical, 而不是新的记忆"机制"或"架构"——这恰恰是它 production-ready 定位的由来