Skip to content

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_{t−1}, m_t)

    • 最新的用户-助手问答对,是抽取的主要来源

将这三部分拼接后,得到完整提示:

text
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 根据它们之间的语义关系,直接输出四种操作之一:
    1. ADD:当在相似记忆中找不到等价或重复的信息时,新增该记忆条目
    2. UPDATE:当候选事实与某条已存记忆部分互补(例如补充更多细节)时,对其进行更新
    3. DELETE:当新信息"推翻"了旧记忆(比如用户修改了偏好)时,删除被冲突的旧条目
    4. NOOP:新旧信息既无冲突,也无增补价值,维持现状无需变动

注意:这里并没有单独训练一个分类器,而是直接借助 LLM 的推理能力,让它在理解语义后自行给出操作决策

执行操作

系统根据 LLM 返回的操作指令,真正对向量数据库/知识库内的条目进行增删改,从而完成一次一致且时序正确的记忆维护

增加记忆(ADD)

为什么要"智能"添加?
  1. 事实抽取(Fact Extraction)

    • 原始消息往往很冗长,只有其中的某些"事实"真正有价值
    • infer=True 时,系统会先让 LLM 从 messages 里提炼出一组"可存储事实"(facts)
  2. 旧记忆检索(Memory Retrieval)

    • 每提炼出一个新事实,就去向量库里找最相关的历史记忆(最多 5 条)
    • 这样可以判断:是不是之前就存过类似内容,或者有更详细/更旧的版本需要更新
  3. 更新决策(Add/Update/Delete)

    • LLM 再根据「新事实 vs. 旧记忆」做三种动作的决策:
      • ADD:新事实是全新内容,要插入
      • UPDATE:新事实更详细或更准确,需要替换掉旧记忆
      • DELETE:新事实表明某记忆已经过期或不再成立,要删掉旧条目
      • NONE:什么也不做
  4. 执行多步存储操作

    • 把上面决策结果逐一对应地调用 _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 标记为"居住关系"

实体节点的三要素

  1. 实体类型(如 Person、Location、Event)
  2. 语义向量 embedding e_v:可以理解为根据上下文对这个实体的解释,嵌入向量
  3. 元数据 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)让模型输出
json
[
  { "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"
json
[
  { "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):目标实体节点,也就是关系的"终点"或"宾语"

图的存储与更新

  1. 节点去重与合并

    • 对每个新三元组,首先计算 src、dst 两个节点的 embedding
    • 在图数据库(Neo4j)中检索相似节点(相似度 > 阈值 t),决定是:
      • 复用现有节点
      • 创建新节点
      • 只创建缺失的那端节点
  2. 冲突检测

    • 如果新三元组与已有边在语义或时间上冲突(如用户曾说"I don't like jazz clubs"又说"I love jazz clubs"),系统会标记旧边为"失效"(invalid),而不是直接删除
    • 依赖 LLM 的推理能力判断"旧关系是否被新信息推翻"
  3. 边的管理

    • 新增(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):信息重复或与已有事实无增益时跳过

图检索(Memory Retrieval)

实体中心检索

  • 流程

    1. 从用户查询中抽出关键实体词(如 "Alice"、"Paris")
    2. 将每个实体编码成向量,在向量库里做最近邻检索,得到对应的实体节点 ID
    3. 在图数据库里针对这些节点做邻域展开(取它们的入/出边和相关节点),拼出一个"实体子图"
  • 适用场景

    • 用户聚焦于某个或某几个实体,想回溯它们的关系历史,如"上次 Alice 在哪些城市访问过?"

语义三元组检索

  • 步骤

    1. 将用户的整句问题或者构造的 (src, rel, dst) 文本(比如"Alice planning_to_visit Paris")编码成一个向量
    2. 在向量库中对所有已存的三元组向量做相似度匹配,直接召回与用户查询最贴近的 top-K 三元组
    3. 如有需要,再在图数据库里把这些三元组对应的节点属性或关联边补充出来
  • 适用场景:用户问"有哪些艺术相关推荐?"这种更宽泛、主题化的概念查询,三元组匹配更灵活

实体+语义三元组

流程

  1. 同时做一次实体向量召回(得到关键节点)和一次三元组向量召回(得到相关关系)
  2. 在图数据库里对召回的节点做邻域展开,对召回的三元组提取更多上下文
  3. 合并这两部分结果,去重并根据相似度、时间戳、图跳数等打分排序,选出最有用的子图或事实片段

适用场景

  • 既要精准找回特定实体的信息,又想利用三元组捕捉更深层的语义模式和多跳推理,比如"哪些人在他们居住的城市里参加过爵士节?"

实验

数据集

LOCOMO 将问题分为四大类,每类考察模型的不同记忆或推理能力:

问题类型含义举例
单跳(Single-hop)答案可直接从对话中的某一句话或某个记忆片段中取得,不需要跨步联想"用户在第 2 会话中提到了哪座城市?"
多跳(Multi-hop)需要将两条及以上事实串联起来才能得出答案"用户在第一次提到'巴黎',之后提到的交通方式是什么?"
时间型(Temporal)答案依赖事件的先后顺序、时间点或时间段推断"第 3 会话后,用户过了几天又提到同一个话题?"
开放域(Open-domain)结合对话内容与外部常识或大型模型自身知识回答,不只是纯记忆回顾"用户最初谈到的爱好是什么?为什么这可能影响他接下来的选择?"

评测指标

性能指标:回答的质量

LLM-as-a-Judge (J):用一个与被测模型不同、能力更强或专门微调过的 LLM(Judge)来打分。

每个测试包括:用户提问、标准答案、模型生成答案

部署维度指标:运行成本和效率

  1. Token 消耗

    统一使用 OpenAI 官方 cl100k_base 编码(tiktoken)对所有文本编码,统计:

    • Memory 模型:检索到的「记忆片段」中包含的 token 数
    • RAG 模型:检索到的「原文 chunk」中包含的 token 数
  2. 延迟

    • Search Latency(检索延迟)
    • Total Latency(总延迟)= Retrieval Time(检索时间)+ Generation Time(LLM 基于检索上下文生成回答的时间)

图结构

先去向量数据库查询语义关系,再去图数据库查询关系,并行

将该“添加”事件写入 SQLite 历史表:

  • memory_id:这条记忆的 UUID。
  • None:旧值(因为是新增,所以旧值为空)。
  • data:新值,即文本内容。
  • "ADD":事件类型。
  • created_at、可选 actor_idrole 等一起写入,保证全链路可追溯。

构建时间:11/21/2025, 1:28:39 PM | 本博客内容均为自己学习,如内容涉及侵权,请联系邮箱:pangzl0215@163.com