Mem0源码解析:构建大模型外部记忆的add与search链路
发布时间:2026/10/21 8:38:53 锦皓数字建站

1. 这个项目到底在解决什么问题我最近把 Mem0 的源码从头到尾翻了一遍。说实话刚开始看到这个名字的时候我以为它只是又一个记忆插件但真正读完 search 和 add 的核心链路之后我发现它本质上做的是一件很实际的事让大模型在对话之外拥有一套可持续读写的外部记忆。先解释一下背景。过去我们做 AI 应用所谓的“记忆”基本靠拼 prompt 上下文。聊得长了塞不进去聊得久了前面的关键事实被后续内容冲淡。更麻烦的是这种记忆是一次性的下次会话又得从零开始。Mem0 的思路是把这个过程显式化用专门的数据结构存记忆用专门的抽取逻辑从对话里提炼事实用专门的检索逻辑在需要的时候把相关记忆找回来。这套东西适合谁读如果你在做智能助手、客服机器人、知识库问答或者任何需要跨会话保持用户特征的 Agent 应用那 Mem0 的源码值得认真看一遍。它不复杂但把“记忆”这件事拆成了几个关键环节写入、抽取、存储、检索。这四个环节的取舍和实现方式基本就是所有记忆系统的通用骨架。读完源码之后我最深的感受是记忆系统最难的其实不是存而是“什么时候记住什么”和“什么时候想起什么”。这两个问题Mem0 分别用 add 和 search 两条链路来回答。接下来的内容我会按源码的实际执行顺序来拆。2. 源码总览与模块边界2.1 从入口到核心类的依赖关系Mem0 的源码入口并不花哨。最上层暴露出来的接口就是add、search和get_all这类方法。以 Python 版本为例核心类维护了三个关键组件一个负责和大模型对话的接口层一个负责向量化和向量检索的存储层还有一个负责记录关系和实体之间的图存储层。这种分工一开始可能让人困惑为什么要同时用向量库和图库我的理解是向量库擅长做“语义相似”召回适合回答“哪条记忆和当前问题相关”图库擅长做“关系扩散”适合回答“这条记忆涉及的人和事还有什么关联”。Mem0 把两者串起来先用向量库粗筛再顺着图关系往外延展最终汇总成上下文。这正好解释了为什么它在很多评测里比纯向量检索效果好。纯粹靠 embedding 召回你只能拿到语义上像的片段但如果这条记忆链接到了用户最近提到的某个项目顺着图边就能把那个项目的背景也拉进来。这就是“记一件事”和“把一件事周围的背景也记下来”的区别。2.2 配置层与可插拔设计源码里有一个很明显的设计取向存储层和 LLM 层全部做成了可替换的接口。比如向量存储可以换成不同的实现图存储也可以换LLM 调用也可以接不同的后端。这样做不是单纯为了炫技而是因为记忆系统的性能瓶颈通常不在算法而在底层存储和大模型调用的稳定性。我在看的时候特别注意了配置加载部分。它把数据库地址、模型名称、向量维度、超时时间这类参数都收敛到一个配置对象里。开发者不需要改业务代码只改配置就能切换到不同的存储组合。这一点对于生产环境很重要你在本地测试时可能用轻量库线上想换成高可用集群如果源码里到处硬编码了连接方式那改造代价会大得惊人。2.3 记忆单元的数据结构源码里定义了一套记忆的数据结构核心字段包括记忆文本、关联的实体、所属会话、时间戳、以及用于向量化的 embedding。我读的时候特意画了一下这个结构的关系发现它其实像一个“以实体为中心”的知识图谱每条记忆都挂在若干实体下面。这里有个值得学习的点它并没有把记忆当成一串隐藏向量而是保留了原始文本和结构化信息。原始文本用来给大模型阅读embedding 用来给向量检索比对实体和关系用来给图库扩散。三份数据各司其职既有冗余又互相补充。很多团队自己实现记忆系统时只存 embedding结果召回的结果虽然相似但缺少解释性也很难做去重和更新。3. add 链路拆解一条记忆是怎么被写进去的3.1 输入预处理与历史消息拼接add 方法的输入并不是已经整理好的记忆句子而是原始对话历史。这一点和很多人的直觉不一样。你以为要先由开发者写好“记忆要点”再传给 Mem0实际上它接收的是消息列表具体要记什么由它自己抽。源码里第一步是把 messages 做标准化。每条消息要标明角色是用户还是助手文本要清洗掉多余的空白和控制字符。然后它会把多轮对话拼接成一个带角色标记的长文本作为后续抽取的输入。这个拼接过程看起来简单但角色标记很重要因为抽取逻辑需要知道哪些话是用户说的哪些是助手生成的。用户说的往往是需要长期记住的偏好或事实而助手的话更多是上下文补充。我在实验的时候发现消息顺序对抽取结果影响很大。如果最近几轮消息本来就是在纠正之前的误解那么只截取最后一段文本会丢掉纠正过程。Mem0 的处理方式是把整段历史压缩后交给抽取而不是简单截尾这一点在长对话场景下非常关键。3.2 向量化与存储索引的构建在抽取实体之前add 链路会先把拼接后的对话文本送给 embedding 接口得到一段向量。这个向量并不是最终记忆的向量而是用于临时比较和后续写入的辅助信息。源码里对向量化的细节做了封装但核心逻辑很明确凡是进入记忆系统的文本都必须先有一套统一的向量表示。向量化之后文本内容会被写入向量存储同时计算出这段内容的唯一标识。这个标识在后续图关系引用中反复出现。我在读的时候有个感触很多人以为 add 就是“插一条记录”但实际上它需要先完成“语义定位”。也就是判断新文本是不是已经有高度相似的内容存在如果有就应该走更新而不是新增。这个判断通常靠向量相似度阈值完成阈值设置得过高会产生大量重复记忆阈值设置得过低又会把真正不同的事实错误合并。3.3 实体抽取与关系识别这是 add 链路里最核心的一段。源码会把拼接好的文本交给 LLM通过特定的 prompt 让它抽出实体、关系和事实。实际返回的结果是一个结构化 JSON包含实体列表、实体间的关系、以及每条记忆的摘要句子。这里有一个细节我特别留意实体抽取不是简单地做命名实体识别而是按“可被引用”和“可被关联”两个标准来选择。比如“用户喜欢喝美式咖啡”会抽出一个“用户”实体和一个“美式咖啡”实体两者之间建立“偏好”关系。至于那些一次性的、无法稳定复用的细节比如“用户今天下午三点说了某句话”通常不会被抽成长期实体。实体关系的构建直接决定后续 search 能扩散多远。如果实体粒度太细图会变得碎片化如果太粗图会丢失关键区分度。Mem0 的做法是在 prompt 里明确要求“仅提取关键实体避免抽入明显的一次性临时信息”同时用温度参数调低生成随机性让输出尽量稳定。3.4 增量更新与去重策略等实体和关系抽完之后add 还不会马上结束。它会先去图库里检查这些实体是否已经存在。如果已经存在就把新关系合并进去如果不存在才新建实体节点。合并逻辑涉及到去重这是我在实际使用中最经常遇到问题的环节。源码里有一个相似度合并判断当新记忆和已有记忆语义高度重合时它会选择保留“信息更完整”的那一条并把旧记录的引用转移到新记录上。这个策略的优点是能防止记忆无限膨胀缺点是如果判断条件太宽松可能把不同语境下的相似句子错误合并。我在一次测试中故意输入“用户喜欢简洁的界面”和“用户偏好极简风格”发现系统能够识别两者相关并做了合并。但换成“用户不喜欢等待”和“用户讨厌这个产品的等待时间”时它会把两者合到同一个实体下虽然没有造成严重后果但你也应该意识到这只是启发式策略不是绝对的事实判定。4. search 链路拆解一次 Query 怎么翻出记忆4.1 查询向量化和初步召回search 的入口接收一个 query 字符串。源码会先对 query 做同样的向量化处理得到查询向量然后在向量存储里做相似度召回。召回数量通常不是只取 top1而是取 topK这个 K 值决定了后续图扩散的上限。K 值的选择直接影响效果。我测试过把 K 从 5 调到 20发现 K 太大时后面的关系扩散会把很多弱关联的内容都拉进来导致最终拼给大模型的上下文过长反而降低回答准确率。当前源码默认的 K 相对保守实际使用时需要根据自己的数据量调。数据量大、记忆条目多K 可以适当调大如果记忆只有几百条K 设太大纯粹是浪费。初步召回的结果是一个候选记忆列表每条都带相似度分数。这个阶段的目标是“别漏掉”哪怕召回了一些不太准确的内容也没关系因为后面还有过滤和重排。4.2 基于实体关系的图扩散拿到候选记忆之后search 会提取这些记忆关联的实体然后去图存储里查询这些实体周围的邻居。这一步才是 Mem0 区别于普通向量检索墙的关键。图扩散的逻辑可以这么理解有一条记忆说“用户和 A 共同参与某项目”当你问“用户最近在忙什么”时向量召回可能只找到直接提到项目的片段。但通过实体“用户”和“某项目”之间的边系统能继续找到“该项目的上线时间”“用户在该项目里负责的部分”等相关记忆这些在字面上未必和当前查询高度相似但在关系上是强相关的。源码里对图扩散的层数做了限制。默认只扩散一层也就是直接邻居。我试过改成两层发现能找回更多背景信息但连接顺序很容易发散。尤其是当某个实体关联了特别多记忆时两层扩散可能把无关的内容也带出来。所以默认一层其实是平衡效果和精度的选择。4.3 融合排序与上下文构造图扩散结束之后search 手上有两批候选纯向量召回的以及通过图关系扩展出来的。源码会计算两个集合的并集同时给每条候选记忆附上一个分数。打分公式大致是向量相似度权重加上关系亲密度权重。不同版本的源码细节有差异但方向一致既不能只看语义相似也不能只看关系远近。如果查询本身是个很具体的事实问题向量相似度的权重应该更高如果查询是个开放性问题比如“聊聊这个人的做事风格”那关系扩散的作用会更大。最后一步是把选中的记忆按时间戳顺序排列再拼接成一段上下文文本。拼接时它会刻意控制总长度避免把记忆一股脑全塞进去。这个拼接结果最终作为系统提示的一部分传给大模型让模型在回答时“知道它应该知道的东西”。4.4 无结果时的降级路径search 还有一个容易被忽略的分支当向量召回的结果相似度都低于某个阈值时源码不会强行返回低质量记忆而是返回空结果。这种情况下上层应用可以选择完全依赖模型自身知识来回答而不是被一段不相关的记忆干扰。这个降级路径我觉得特别实用。很多记忆系统的问题是无论查什么都会回几条不太相关的记录反而污染了模型的判断。Mem0 用阈值来控制“有多确定才使用记忆”从产品体验上说这比“永远有结果”更靠谱。5. 记忆抽取的 Prompt 设计与输出解析5.1 Prompt 模板的作用记忆抽取的效果有很大一部分不是靠模型能力而是靠 prompt 怎么问。源码里的 prompt 写得非常结构化它会明确告诉模型你需要从对话中抽取出实体和关系并以 JSON 格式返回。同时它还会附上一些例子用 few-shot 的方式引导模型输出符合预期的格式。我看这段代码的时候特别注意到一个关键词它要求模型把记忆分成“当前会话相关的短期记忆”和“值得长期保留的核心记忆”。这个区分直接关系到 add 后是否真的写入持久化存储。对于那些聊天气氛、临时情绪、或者一次性事件抽取结果会被标记为临时不会被放进长期图关系里。在实际部署中你可以调整这个 prompt 来改变记忆抽取的口径。比如在客服场景你希望多抽取用户的身份特征和购买偏好在医疗场景你希望把症状描述和用药历史放在最前面。源码允许开发者覆盖默认 prompt这个灵活性很重要毕竟没有一个通用抽取规则能适应所有领域。5.2 结构化输出解析与容错LLM 输出 JSON 并不总是合法的。我读源码时专门找了解析这块发现它做了好几层容错。首先它要求模型用严格的 JSON 格式接着在解析时捕获解析异常如果失败会把原始输出扔给一个轻量的修复逻辑尝试从文本块里截取 JSON 片段。这种容错不是无脑重试。重试大模型调用会引入额外延迟和成本而截取片段的方式在大多数情况下能解决 90% 的格式问题。我一开始以为它会直接抛异常后来发现它连“代码块包裹的 JSON”这种常见错误都处理了比如返回了带 json 标记的文本它也能剥掉外壳再解析。但这里有个坑如果 LLM 输出的是合法 JSON但缺少必要字段源码不会纠正。它只是在这条记忆上标记为“部分有效”然后选择性地跳过缺失关系的内容。这意味着你不能完全依赖模型正确输出最好在业务层也做一次字段校验哪怕 Mem0 内部已经做了容错。5.3 抽取与搜索之间的反馈循环我第一次读源码时以为 add 和 search 是两条完全独立的链路后来才发现它们共享了同一个实体库和关系图。这就形成了一个隐性的反馈循环add 写得越精确search 能扩散的图越可靠而 search 反馈的查询结果如果被用户标注为“有用”或“无用”又可以反过来指导 add 阶段抽取哪些实体。源码里目前没有做复杂的强化学习但这种共享结构的含义已经很明显了。只要 add 阶段建立的实体关系是干净的、可追溯的search 阶段的质量就有保障。反之如果 add 阶段抽了一堆噪声实体那 search 再努力也会把噪声扩散出去。所以你在使用中要重点监控实体抽取的质量而不是只看搜索结果好不好看。6. 我在阅读源码过程中踩过的几个坑6.1 向量维度不匹配导致写入失败我第一次跑通 add 的时候遇到一个很典型的问题配置里指定的向量维度是 768但实际 embedding 接口返回的向量只有 384 维。写入向量库时直接报错。排查半天发现是初始化时传错了模型名称它使用的模型是另一个文本模型。这个问题的根因是源码对向量维度做了严格校验它宁可直接抛错也不愿意用一个错误维度的向量污染整个索引。这从工程角度说是好事但从使用者角度来说报错信息一开始有点抽象。后来我总结出一个习惯在你更换 embedding 模型之后第一件事就是打印一条原始向量的 shape确认维度再继续。6.2 实体抽取反复出现同一实体大小写变体在另一组测试里我发现同一个名字在抽取结果中出现了“张三”“张三(zhang san)”“Zhang San”三种写法。这让图库里出现了三个看似不同、其实指向同一个人的节点导致 search 扩散时不能很好地聚合。这个问题在源码里有一定程度的归一化处理但它默认的规则比较简单主要靠文本小写化和去除首尾空格。如果想真正解决同义词和别名问题你需要在外层再做一遍实体统一。我自己在项目里接了一个轻量的指代消解规则效果立竿见影。别指望默认实现能覆盖所有业务里的别名场景。6.3 时间戳导致的记忆覆盖顺序异常add 链路在合并相似记忆时会参考时间戳来判断哪条更新。我遇到的坑是导入历史数据时所有消息都用了同一个时间戳导致合并时它认为每一条都是最新的结果后导入的内容覆盖了先前的内容把之前保留的关键信息洗掉了。后来去源码里确认它的时间戳默认取系统当前时间而不是消息里自带的时间。如果你要导入的是存量历史数据一定要在调用 add 时显式传入时间戳并且要保证时间递增否则记忆的新旧判断会完全错乱。6.4 高并发写入时的图关系锁冲突我尝试用多线程并发调用 add 时偶尔会触发图存储端的写入冲突。原因是两条记忆几乎同时尝试创建同一个实体节点但图存储端对同一主键的创建操作没有做幂等保护。这个在单机开发环境不敏感但只要你上了并发就会暴露。源码在设计时可能默认使用方通过队列或者串行方式写入。我在生产环境里的做法是加了一层内存队列把并发写改成了批量串行写。这样既保证了实体创建的顺序也避免图存储被并发请求打挂。6.5 对 LLM 返回结果的过度信任最后这个坑不是源码的 bug而是使用心态问题。Mem0 的抽取结果本质上是大模型生成的大模型有可能幻觉出用户从未提过的实体。源码不会去验证实体是否真实存在它只会尽量把这些实体纳入关系中。所以在某些敏感场景下你必须对外层做事实校验。我建议在实际项目里加一道“人工反馈”通道对于高风险的记忆写入先让用户确认再真正写入长期存储。不是说 Mem0 不好而是它把“抽取”这个动作交给模型之后本身就带着不确定性设计系统的人要意识到这一点而不是把一个概率性过程当成确定性事实。最后一点心得和后续可以玩的方向如果你要把这套源码的机制拆开重做建议不要一上来就追求复杂。先实现一个最小版本向量存储加图存储再加一个 prompt 抽取外层。把 add 和 search 的链路跑通之后再逐步调优实体抽取的策略和排序权重。我在实际使用中发现最影响体验的不是某一个独立模块而是实体抽取的稳定性和阈值设置。这两个参数直接决定了记忆系统会不会变成“垃圾进垃圾出”。Mem0 的默认参数挺好但不要迷信一定得拿你自己的数据去压测。后续我计划做的事情是把抽取出的实体关系做成可视化让用户能看到自己的长期记忆图谱。很多人对自己的记忆只有抽象感觉一旦可视化出来交互会完全不一样。如果你也在做类似的事欢迎沿着这个方向继续折腾。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。