Mem0
上下文窗口的局限性
目前主流的大型语言模型(LLM)如 GPT 系列在对话生成上表现突出,但它们的固定上下文窗口(context window)导致无法有效记忆和利用跨多轮、跨会话的历史信息。随着对话长度增长,模型只能保留窗口内的有限内容,一旦超出窗口范围,早期信息就被截断,影响对话连贯性与一致性。
长期对话一致性的挑战
在构建需要与用户进行多会话、长时交互的 AI Agent 时,如何让模型"记住"先前会话中重要的细节(如用户偏好、长期项目进展等)成为亟待解决的问题。
Mem0 提出了一种**"记忆中心化"**的架构,通过动态地抽取、整合并检索对话中的关键信息,实现对超出上下文窗口之外的长期记忆管理。其核心流程包括:
- 动态抽取(Extraction):在对话进行时,自动识别并提取潜在的"记忆片段"(memory snippets),如实体关系、用户需求、关键事实
- 记忆整合(Consolidation):将新提取的信息归入已有记忆库,合并重复或相关内容,保持记忆的精简性与一致性
- 记忆检索(Retrieval):在生成回复时,根据当前对话上下文动态召回最相关的历史记忆,将其作为增强的上下文输入给 LLM
在基础的 Mem0 之上,作者进一步提出了图结构记忆版本:
- 记忆节点(Memory Nodes):代表对话中的实体、概念或事件
- 记忆边(Memory Edges):表示节点间的各类关系(如时间先后、因果关系、共现关系等)
- 图操作:利用图神经网络(GNN)或图检索算法,捕获复杂的多跳、多实体之间的关联,使检索出的记忆片段更符合当前对话意图
框架
抽取阶段
触发条件
每当系统收到一对新的消息 (m_{t−1}, m_t)(通常是一条用户输入和紧接着的助手回复),就启动一次抽取流程。这保证了每个完整的"问答单元"都被检视。
上下文构建
为了让记忆抽取兼具全局视角和局部时序两方面的信息,系统准备了三部分内容组成最终的提示 P:
全局摘要 S
- 通过"异步摘要生成模块"定期(独立于主流水线)更新(比如每隔 N 分钟或每隔 K 条新消息)重新调用 LLM,保证概括了当前会话从头到现在的主题脉络
- 作用:快速传达会话的大体走向和核心话题,避免每次都重新回顾所有历史消息
异步摘要生成模块
- 独立于主流水线:
- 主流水线负责实时的"消息对抽取 → 候选记忆生成 → 更新",必须尽可能低延迟
- 摘要模块则在后台以异步方式运行:它定期(比如每隔 N 分钟或每隔 K 条新消息)重新调用 LLM,对截至上次摘要后的增量消息做"摘要增量"或"全文重摘要"
- 这样,主流程无需等待摘要完成就能继续抽取;而摘要模块又能在下一轮给出更贴近当前进展的全局视图
生成策略
- 增量式摘要
- 对话刚开始时,先从头生成第一版摘要 S₀
- 随后对话新增消息时,只对"最新一段增量"进行摘要,生成 ΔS,然后与旧摘要合并或替换不相关部分,得到新摘要 S₁
- 优势:每次处理的文本量有限,效率更高;但要确保"合并"后摘要仍连贯完整
- 重摘要
- 当对话主题大幅转折、增量累积到一定阈值时(如增量消息超过 500 条或摘要长度超过 1000 token),直接对整段历史做一次新摘要,舍弃旧版摘要
- 优势:保证摘要不失真、不偏题;缺点是计算量大,触发频率要谨慎设置
抽取方式
- 抽取式(Extractive)摘要
- 从原文中挑选最具代表性的若干句子或片段,按原文顺序或重要度排序拼接
- 优点:生成速度快,信息真实不"编造";缺点:可能过长且不够连贯
- 生成式(Abstractive)摘要
- 利用 LLM 重新用简练语言复述对话核心观点
- 优点:更简洁、连贯,可融合关键信息;缺点:依赖模型生成质量,偶有信息丢失或"幻觉"
近期消息序列
{m_{t−m}, …, m_{t−2}}- "m"为超参数(论文中实验取
m=10),表示回溯多少条最近消息 - 作用:补充摘要中可能遗漏的、尚未整合但又很新鲜的细节
- "m"为超参数(论文中实验取
当前消息对
(m_{t−1}, m_t)- 最新的用户-助手问答对,是抽取的主要来源
将这三部分拼接后,得到完整提示:
P = ( S, {m_{t−m}, …, m_{t−2}}, m_{t−1}, m_t )抽取函数 ϕ
- 实现方式:调用 LLM(论文中用的是 GPT-4o-mini)
- 输入:提示
P - 输出:一组"候选记忆"Ω = {ω₁, ω₂, …, ωₙ},每个 ωᵢ 都是从这次对话里提炼出的"潜在要存入知识库"的事实/事件/偏好等
更新阶段
目的
对刚抽取得到的每条候选记忆 ωᵢ,判断是要"新增"、还是"补充""删除"或"忽略",以保证:
- 一致性(没有前后冲突)
- 无冗余(不重复存储同一信息)
检索候选对比集
- 取超参数
s=10(实验中同样设为 10),在向量数据库里用 ωᵢ 的 embedding 搜索出「与之语义最相近的前 10 条」已存记忆作为对比集
LLM "工具调用"决策
- 将候选记忆 ωᵢ 和检索到的 10 条相似记忆一起,通过一个**函数调用接口(tool call)**提交给同样的 LLM
- LLM 根据它们之间的语义关系,直接输出四种操作之一:
- ADD:当在相似记忆中找不到等价或重复的信息时,新增该记忆条目
- UPDATE:当候选事实与某条已存记忆部分互补(例如补充更多细节)时,对其进行更新
- DELETE:当新信息"推翻"了旧记忆(比如用户修改了偏好)时,删除被冲突的旧条目
- NOOP:新旧信息既无冲突,也无增补价值,维持现状无需变动
注意:这里并没有单独训练一个分类器,而是直接借助 LLM 的推理能力,让它在理解语义后自行给出操作决策
执行操作
系统根据 LLM 返回的操作指令,真正对向量数据库/知识库内的条目进行增删改,从而完成一次一致且时序正确的记忆维护
增加记忆(ADD)
为什么要"智能"添加?
事实抽取(Fact Extraction)
- 原始消息往往很冗长,只有其中的某些"事实"真正有价值
infer=True时,系统会先让 LLM 从messages里提炼出一组"可存储事实"(facts)
旧记忆检索(Memory Retrieval)
- 每提炼出一个新事实,就去向量库里找最相关的历史记忆(最多 5 条)
- 这样可以判断:是不是之前就存过类似内容,或者有更详细/更旧的版本需要更新
更新决策(Add/Update/Delete)
- LLM 再根据「新事实 vs. 旧记忆」做三种动作的决策:
- ADD:新事实是全新内容,要插入
- UPDATE:新事实更详细或更准确,需要替换掉旧记忆
- DELETE:新事实表明某记忆已经过期或不再成立,要删掉旧条目
- NONE:什么也不做
- LLM 再根据「新事实 vs. 旧记忆」做三种动作的决策:
执行多步存储操作
- 把上面决策结果逐一对应地调用
_create_memory、_update_memory、_delete_memory
- 把上面决策结果逐一对应地调用
processed_metadata 会包含 { "user_id":…, "agent_id":…, "run_id":…, …},作为后续写入记忆时的 payload,确保每条新记忆都打上这三层作用域标签。
effective_filters 同样包含这三层标签,用于后续在同一作用域内检索历史记忆(只搜索这位用户、这个 Agent、这个会话里的内容)
添加到向量数据库(_add_to_vector_store)
filter = user_id, agent_id, run_id,检索时先在filter固定一个范围,然后再使用query检索
对话/会话记忆
原始的用户–助手交互消息,逐条存储。
事实/语义记忆
由 LLM 从对话中抽取出来、去重后的"核心事实"或"知识片段"
图结构的记忆形式
记忆图 G = (V, E, L)
- 节点 V:每个节点 v 代表一个"实体"(entity),如人名 Alice、地点 San_Francisco、事件 "Trip_to_Paris" 等
- 有向边 E:边 (vs → vd) 捕捉实体间的"语义关系",比如 Alice —[lives_in]→ San_Francisco
- 标签 L:为节点和/或边赋予类型信息,如节点 Alice 标记为 Person,边 lives_in 标记为"居住关系"
实体节点的三要素
- 实体类型(如 Person、Location、Event)
- 语义向量 embedding e_v:可以理解为根据上下文对这个实体的解释,嵌入向量
- 元数据 meta:包括创建时间戳 t_v 等,用于后续的时间推理和冲突检测
关系三元组 (v_s, r, v_d)
- v_s → 源实体节点,v_d → 目标实体节点
- r → 关系标签,如 "prefers"、"happened_on"、"owned_by" 等
从文本到图的两阶段管道
实体抽取(Entity Extraction)
先去向量数据库查找记录
- 目标:从对话文本中识别所有值得长期记忆的"离散实体"——不仅包括人和地点,还涵盖概念、产品、偏好、事件等
- 实现:调用 LLM(GPT-4o-mini)或专门的实体识别模块,通过提示(prompt)让模型输出
[
{ "entity": "Paris", "type": "Location" },
{ "entity": "jazz clubs", "type": "Activity" },
{ "entity": "next month", "type": "Date" },
…
]关系生成(Relationship Generation)
目标:在已抽取的实体间显式建立语义联系,形成图的边
实现:对每一对实体,LLM 会被提示"基于上下文,这两者之间是否有合理关系?如果有,请给出标签"
- 例如,在"我下个月要去巴黎看爵士乐"这句话中,实体对 ("I", "Paris") → "planning_to_visit",对 ("I", "jazz clubs") → "likes"
[
{ "src": "I", "rel": "planning_to_visit", "dst": "Paris" },
{ "src": "I", "rel": "interested_in", "dst": "jazz clubs" },
…
]在图结构记忆中,关系三元组通常写作 (src, rel, dst),它们分别表示:
src(Source):源实体节点,也就是关系的"起点"或"主语"rel(Relation):关系类型标签,描述源实体和目标实体之间的语义联系,相当于谓词或动词dst(Destination):目标实体节点,也就是关系的"终点"或"宾语"
图的存储与更新
节点去重与合并
- 对每个新三元组,首先计算 src、dst 两个节点的 embedding
- 在图数据库(Neo4j)中检索相似节点(相似度 > 阈值 t),决定是:
- 复用现有节点
- 创建新节点
- 只创建缺失的那端节点
冲突检测
- 如果新三元组与已有边在语义或时间上冲突(如用户曾说"I don't like jazz clubs"又说"I love jazz clubs"),系统会标记旧边为"失效"(invalid),而不是直接删除
- 依赖 LLM 的推理能力判断"旧关系是否被新信息推翻"
边的管理
- 新增(ADD):没有同义或等价的旧边时才添加,如果你查了一下,发现 Alice→Paris 没有任何
lives_in的活跃边,就会走到 ADD,新增一条lives_in边 - 更新(UPDATE):当新信息只是补充细节时,例如"Jazz clubs are in the Latin Quarter"可以在已有 "I likes jazz clubs" 边上附加"location=Latin Quarter"元数据。如果 Alice→Paris 已经有一条活跃的
lives_in边,但接下来来了一个新的lives_in信息(比如 "我搬去 Berlin"),那就不会再 ADD,而是把旧的那条打标记为「失效」(invalid),再创建一条新的活跃边 - 标记失效:对于被推翻的关系,仅作失效标记以保留时序考据
- 忽略(NOOP):信息重复或与已有事实无增益时跳过
- 新增(ADD):没有同义或等价的旧边时才添加,如果你查了一下,发现 Alice→Paris 没有任何
图检索(Memory Retrieval)
实体中心检索
流程:
- 从用户查询中抽出关键实体词(如 "Alice"、"Paris")
- 将每个实体编码成向量,在向量库里做最近邻检索,得到对应的实体节点 ID
- 在图数据库里针对这些节点做邻域展开(取它们的入/出边和相关节点),拼出一个"实体子图"
适用场景:
- 用户聚焦于某个或某几个实体,想回溯它们的关系历史,如"上次 Alice 在哪些城市访问过?"
语义三元组检索
步骤:
- 将用户的整句问题或者构造的
(src, rel, dst)文本(比如"Alice planning_to_visit Paris")编码成一个向量 - 在向量库中对所有已存的三元组向量做相似度匹配,直接召回与用户查询最贴近的 top-K 三元组
- 如有需要,再在图数据库里把这些三元组对应的节点属性或关联边补充出来
- 将用户的整句问题或者构造的
适用场景:用户问"有哪些艺术相关推荐?"这种更宽泛、主题化的概念查询,三元组匹配更灵活
实体+语义三元组
流程:
- 同时做一次实体向量召回(得到关键节点)和一次三元组向量召回(得到相关关系)
- 在图数据库里对召回的节点做邻域展开,对召回的三元组提取更多上下文
- 合并这两部分结果,去重并根据相似度、时间戳、图跳数等打分排序,选出最有用的子图或事实片段
适用场景:
- 既要精准找回特定实体的信息,又想利用三元组捕捉更深层的语义模式和多跳推理,比如"哪些人在他们居住的城市里参加过爵士节?"
实验
数据集
LOCOMO 将问题分为四大类,每类考察模型的不同记忆或推理能力:
| 问题类型 | 含义 | 举例 |
|---|---|---|
| 单跳(Single-hop) | 答案可直接从对话中的某一句话或某个记忆片段中取得,不需要跨步联想 | "用户在第 2 会话中提到了哪座城市?" |
| 多跳(Multi-hop) | 需要将两条及以上事实串联起来才能得出答案 | "用户在第一次提到'巴黎',之后提到的交通方式是什么?" |
| 时间型(Temporal) | 答案依赖事件的先后顺序、时间点或时间段推断 | "第 3 会话后,用户过了几天又提到同一个话题?" |
| 开放域(Open-domain) | 结合对话内容与外部常识或大型模型自身知识回答,不只是纯记忆回顾 | "用户最初谈到的爱好是什么?为什么这可能影响他接下来的选择?" |
评测指标
性能指标:回答的质量
LLM-as-a-Judge (J):用一个与被测模型不同、能力更强或专门微调过的 LLM(Judge)来打分。
每个测试包括:用户提问、标准答案、模型生成答案
部署维度指标:运行成本和效率
Token 消耗
统一使用 OpenAI 官方
cl100k_base编码(tiktoken)对所有文本编码,统计:- Memory 模型:检索到的「记忆片段」中包含的 token 数
- RAG 模型:检索到的「原文 chunk」中包含的 token 数
延迟
- Search Latency(检索延迟)
- Total Latency(总延迟)= Retrieval Time(检索时间)+ Generation Time(LLM 基于检索上下文生成回答的时间)
图结构
先去向量数据库查询语义关系,再去图数据库查询关系,并行
将该“添加”事件写入 SQLite 历史表:
memory_id:这条记忆的 UUID。None:旧值(因为是新增,所以旧值为空)。data:新值,即文本内容。"ADD":事件类型。created_at、可选actor_id、role等一起写入,保证全链路可追溯。