混合记忆:向量 + 图 + KV


文档摘要

混合记忆:向量 + 图 + KV 本节摘要:单一存储对 Agent 的记忆需求总是错的。语义相似性问「我们上周聊过 Agent 漂移的什么?」——向量库赢;事实查找问「用户电话多少?」——KV 库赢;关系推理问「哪些客户共用同一计费实体?」——图库赢。生产 Agent 一个会话里就同时发这三类查询,单一存储对其中两类总是错的。Mem0(Chhikara 等人, 2025)的贡献是把三个后端(向量做语义相似、KV 做快速事实查找、图做实体关系推理)并行跑在一个统一的 / 面后,再用一个融合打分层(Fusion Scoring)——相关性、重要性、近期性三因子的加权和——在检索时合并结果。

混合记忆:向量 + 图 + KV

本节摘要:单一存储对 Agent 的记忆需求总是错的。语义相似性问「我们上周聊过 Agent 漂移的什么?」——向量库赢;事实查找问「用户电话多少?」——KV 库赢;关系推理问「哪些客户共用同一计费实体?」——图库赢。生产 Agent 一个会话里就同时发这三类查询,单一存储对其中两类总是错的。Mem0(Chhikara 等人, 2025)的贡献是把三个后端(向量做语义相似、KV 做快速事实查找、图做实体关系推理)并行跑在一个统一的 add/search 面后,再用一个融合打分层(Fusion Scoring)——相关性、重要性、近期性三因子的加权和——在检索时合并结果。本节吃透为什么单存储不够、Mem0 的三库并行写入与检索、融合打分为何是加权和而非层级、Mem0g 的时序作废机制,并用标准库从零实现一个玩具三库记忆:add() 同时写三库、search() 融合三者结果。读完本节,你应能识别何时该上混合记忆、如何调融合权重、如何防嵌入漂移与图爆炸。

对应原课程:Phase 14 · Lesson 09 · hybrid-memory-mem0(原英文 phases/14-agent-engineering/09-hybrid-memory-mem0/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 解释为什么单一存储(纯向量、纯图、纯 KV)不足以支撑 Agent 记忆。
  2. 说出 Mem0 的三个并行存储以及各自优化的查询类型。
  3. 描述 Mem0 的融合打分——相关性、重要性、近期性——以及为什么是加权和而非层级。
  4. 用标准库实现一个玩具三库记忆:add() 同时写三库,search() 融合结果。
  5. 诊断嵌入漂移、KV 模式蔓延、图爆炸三大反模式,并给出对应的工程对策。

一、问题与直觉

单一存储总是错的

任何单一存储,对三类查询里的一类是对的,对另外两类是错的:

  • 语义相似性(Semantic Similarity) —— 「我们上周聊过 Agent 漂移的什么?」向量库赢;KV 和图都答不上。
  • 事实查找(Fact Lookup) —— 「用户电话多少?」KV 库赢;向量浪费,图大材小用。
  • 关系推理(Relationship Reasoning) —— 「哪些客户共用同一计费实体?」图库赢;向量和 KV 都答不上。

生产 Agent 一个会话里就同时发这三类查询。所以单一存储对其中两类总是错的。Mem0 的贡献,是把三个存储接到一个统一的 add/search 面后,再用一个打分函数融合它们。

三库并行写入

Mem0(arXiv:2504.19413, 2025 年 4 月)在 add(text, user_id, metadata) 时:

  1. 从文本里抽取候选事实(一步 LLM 驱动)。
  2. 每条事实写进向量库(嵌入),供语义搜索。
  3. 每条事实写进 KV 库,键为 (user_id, fact_type, entity),供 O(1) 查找。
  4. 每条事实写进图库(Mem0g),作为类型化边,供关系查询。

search(query, user_id) 时:

  1. 向量库按嵌入余弦返回 top-k。
  2. KV 库按查询派生的 (user_id, type, entity) 返回精确命中。
  3. 图库返回从查询实体可达的子图。
  4. 打分层融合三者。

融合打分:加权和,不是层级

score = w_relevance * relevance(q, record) + w_importance * importance(record) + w_recency * recency(record)
  • 相关性(Relevance) —— 向量余弦、KV 精确匹配、图路径权重。
  • 重要性(Importance) —— 写入时打标或学习得到(有些事实更要紧:姓名、ID、策略)。
  • 近期性(Recency) —— 自上次写入或读取以来的时间指数衰减。

权重按产品调:聊天型 Agent 调高 w_recency;合规型 Agent 调高 w_importance;检索型 Agent 调高 w_relevance

💡 设计要点:为什么是加权和而不是「先按相关性筛、再按近期性排」的层级?因为层级意味着先一刀切掉一批候选,可能把近期又重要的丢掉。加权和让三个维度同时影响排序,任何一维强都能把记录拉上来,但单独一维强又不足以压倒另外两维的组合。这是多信号排序的通用心法。

Mem0g 与时序推理

Mem0g 加了一个冲突检测器(Conflict Detector)。当一条新事实与一条已存在的边矛盾时,旧边被标记为失效但不删除。时序查询(「用户三月份住在哪个城市?」)遍历「该时刻有效」的子图。

这正是第 08 节 Letta 作废模式的合规级强化:不删,只标记,可回溯。

基准数字

Mem0 论文(2025)报告:

  • LoCoMo(长对话记忆):91.6
  • LongMemEval(长程情景记忆):93.4
  • BEAM 1M(百万 token 记忆基准):64.1

对照基线(全上下文 128k LLM、扁平向量库、扁平 KV)都低 10 分以上。基准本身不决定选型——运维形态才决定——但这些数字说明融合设计不是误差项。

范围分类:谁看得到什么

Mem0 按范围切分记忆:

  • 用户记忆(User Memory) —— 跨会话持久,键为 user_id
  • 会话记忆(Session Memory) —— 单线程内持久。
  • Agent 记忆(Agent Memory) —— 单 Agent 实例的状态。

每次写入选一个范围。检索可跨范围查询,带每范围的权重。不假思索地混范围就是「助手把 Bob 的项目告诉了 Alice」这类事故的来源。

这个模式在哪里出错

  • 嵌入漂移(Embedding Drift) —— 向量结果在前几百次查询看着对,语料一增长就退化。对策:周期性地对高频使用的 top-N 记录重新嵌入。
  • KV 模式蔓延(KV Schema Creep) —— (user_id, type, entity) 看着简单,直到每个团队都加自己的 type。对策:每季度审计一次 type 集合。
  • 图爆炸(Graph Explosion) —— 一个噪声大的抽取器每条消息加 50 条边。对策:每次 add 限制图写入条数;丢弃低置信度边。

二、从零实现

原课程 code/main.py 用标准库实现三库模式:

  • VectorStore —— 朴素的 token 重叠相似度,作为嵌入的替身。
  • KVStore —— 以 (user_id, fact_type, entity) 为键的字典。
  • GraphStore —— 类型化边 (subject, relation, object, valid)
  • Mem0 —— 顶层门面,带 add()search()、融合打分、范围感知检索。
  • 一段多用户、多会话对话的演示轨迹。

核心骨架如下,用伪代码展示。

Step 1:三个存储

class VectorStore: def __init__(self): self.records = [] def add(self, text, meta): self.records.append((text, meta)) def search(self, q, top_k=5): qtok = set(q.lower().split()) scored = [(len(qtok & set(t.lower().split())), t, m) for t, m in self.records] scored.sort(reverse=True) return [(s, t, m) for s, t, m in scored[:top_k] if s > 0] class KVStore: def __init__(self): self.kv = {} def put(self, user_id, ftype, entity, value): self.kv[(user_id, ftype, entity)] = value def get(self, user_id, ftype, entity): return self.kv.get((user_id, ftype, entity)) class GraphStore: def __init__(self): self.edges = [] # (subj, rel, obj, valid) def add_edge(self, s, r, o): # 冲突检测:已有 (s, r, *) 则作废旧边 for i, (ss, rr, oo, vv) in enumerate(self.edges): if ss == s and rr == r and oo != o: self.edges[i] = (ss, rr, oo, False) # 标记失效不删 self.edges.append((s, r, o, True))

Step 2:门面 + 融合打分

class Mem0: def __init__(self, w=(0.5, 0.3, 0.2)): # relevance, importance, recency self.vec, self.kv, self.graph = VectorStore(), KVStore(), GraphStore() self.w = w def add(self, text, user_id, metadata): facts = extract_facts(text) # LLM 步:拆成 (entity, rel, fact) for entity, rel, fact in facts: self.vec.add(fact, metadata) self.kv.put(user_id, rel, entity, fact) self.graph.add_edge(user_id, rel, entity) def search(self, query, user_id, top_k=5): v_hits = self.vec.search(query) kv_hit = self.kv.get(user_id, derive_type(query), derive_entity(query)) g_hits = [e for e in self.graph.edges if e[3]] # 只取有效边 fused = self._fuse(v_hits, kv_hit, g_hits, query) return fused[:top_k] def _fuse(self, v_hits, kv_hit, g_hits, query): scored = [] for rel, text, meta in v_hits: recency = decay(meta.get("ts")) importance = meta.get("importance", 1.0) s = self.w[0]*rel + self.w[1]*importance + self.w[2]*recency scored.append((s, text)) # KV 命中给满分相关性,图命中按路径权重...... return sorted(scored, reverse=True)

运行 python3 code/main.py 会展示三条独立的召回路径加上融合后的 top-k。把 main() 顶部的打分权重翻一下,看排序怎么变。

💡 设计要点:add() 的「事实抽取」是承重的一步。它决定了写进三库的是什么。抽得太粗(整段原文)则 KV 和图形同虚设;抽得太碎(每个字一条事实)则图爆炸。生产里这步通常是「LLM 抽取 + 规则校验 + 置信度过滤」的组合。

三、框架对比

  • Mem0(Apache 2.0) —— 生产就绪。可用 Postgres + Qdrant + Neo4j 自托管,或用托管云。
  • Letta —— 三层 core/recall/archival;自带向量与图后端。
  • Zep —— 商业替代,带时序知识图与事实抽取。
  • 自建 —— 当你需要精确控制抽取器(合规)或融合权重(语音 Agent 里近期性主导)时。
系统 三库 融合打分 时序作废 运维形态
Mem0 向量+KV+图 内置 Mem0g 冲突检测 自托管/托管
Letta 自带后端 需配 块级 自托管/托管
Zep 时序 KG 内置 边带有效期 商业云
自建 全控 全控 全控 全控

四、可复用产物

本节产出一份可复用技能(原课程 outputs/skill-hybrid-memory.md):

  • skill-hybrid-memory.md:生成一个三库记忆脚手架,内置融合打分器、范围分类、时序作废接线。包含一份选型清单(是否真的需要三库?哪种查询占比最高?重要性怎么标?),以及一份运维清单(嵌入多久重嵌一次?type 集合多久审计?图写入每次限几条?)。

Python 代码(code/main.py)是独立可运行的三库骨架,门面、打分、范围、作废都是后端无关的;把玩具相似度换成真实嵌入、把内存字典换成 Postgres/Qdrant/Neo4j 即可投入生产。

五、练习

  1. (Easy) 把玩具向量相似度换成真实嵌入模型(sentence-transformers、Ollama、OpenAI embeddings)。在合成长对话上测 recall@10。1000 次写入后排序会漂移吗?
  2. (Medium) 加一个时序查询:search(query, as_of=timestamp),只返回当时或之前有效的记录。哪个存储改动最大?
  3. (Medium) 实现冲突检测:若进入的事实与图边矛盾,作废旧边并记录两者。在「用户住柏林 → 用户住里斯本」上测试。
  4. (Hard) 给融合打分加一个 user_feedback 维度(对检索记录点赞)。怎么防止被钻空子(Agent 只返回它已经喜欢的记录)?
  5. (Hard) 读 Mem0 文档(docs.mem0.ai)。把玩具移植成 mem0 客户端调用。在同样 20 条测试查询上对比检索质量。

本节要点回顾

  1. 单存储总是错的:语义相似向量赢、事实查找 KV 赢、关系推理图赢,生产三类查询一个会话都有。
  2. Mem0 三库并行:向量做语义、KV 做 O(1) 事实查找、图(Mem0g)做关系,统一在 add/search 面后。
  3. 写入四步:LLM 抽事实 → 写向量 → 写 KV → 写图。
  4. 检索三路融合:各库返回候选,打分层合并。
  5. 融合是加权和不是层级:三因子 relevance + importance + recency 同时影响,避免一刀切丢候选。
  6. 权重按产品调:聊天调高 recency、合规调高 importance、检索调高 relevance。
  7. Mem0g 时序作废:矛盾边标记失效不删,时序查询遍历有效子图,合规级可回溯。
  8. 范围三分类:user / session / agent,写入选一个,检索可跨范围带权重;乱混范围是越权事故之源。
  9. 三大反模式:嵌入漂移(定期重嵌 top-N)、KV 模式蔓延(季度审计 type)、图爆炸(每次 add 限边数)。
  10. 事实抽取承重:抽太粗 KV/图虚设,抽太碎图爆炸,生产用「LLM + 规则 + 置信度」组合。

下一节,我们离开记忆,转向「技能库与终身学习」——Voyager 让 Agent 在 Minecraft 里自己写代码、验证、把可复用技能存进技能库,下次遇到类似任务直接复用,把单次 Agent 的学习能力扩展到终身。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U