[!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, {mt−m,…,mt−2}, mt−1, mt)P = (S,\ \{m_{t-m}, \dots, m_{t-2}\},\ m_{t-1},\ m_t)

  • 当前消息对 (mt−1,mt)(m_{t-1}, m_t) — 真正要抽取的焦点, 也就是最新这轮 user 和 assistant 的交换
  • 对话摘要 SS — 浓缩整段对话历史语义的远景
  • 最近消息窗口 {mt−m,…,mt−2}\{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
  • 近期窗口不是"筛"出来的: {mt−m,…,mt−2}\{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(log⁡N)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 定位的由来