混合记忆:向量 + 图 + KV 本节摘要:单一存储对 Agent 的记忆需求总是错的。语义相似性问「我们上周聊过 Agent 漂移的什么?」——向量库赢;事实查找问「用户电话多少?」——KV 库赢;关系推理问「哪些客户共用同一计费实体?」——图库赢。生产 Agent 一个会话里就同时发这三类查询,单一存储对其中两类总是错的。Mem0(Chhikara 等人, 2025)的贡献是把三个后端(向量做语义相似、KV 做快速事实查找、图做实体关系推理)并行跑在一个统一的 / 面后,再用一个融合打分层(Fusion Scoring)——相关性、重要性、近期性三因子的加权和——在检索时合并结果。
本节摘要:单一存储对 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)。
阅读完本节,你应当能够:
add() 同时写三库,search() 融合结果。任何单一存储,对三类查询里的一类是对的,对另外两类是错的:
生产 Agent 一个会话里就同时发这三类查询。所以单一存储对其中两类总是错的。Mem0 的贡献,是把三个存储接到一个统一的 add/search 面后,再用一个打分函数融合它们。
Mem0(arXiv:2504.19413, 2025 年 4 月)在 add(text, user_id, metadata) 时:
(user_id, fact_type, entity),供 O(1) 查找。在 search(query, user_id) 时:
(user_id, type, entity) 返回精确命中。score = w_relevance * relevance(q, record) + w_importance * importance(record) + w_recency * recency(record)
权重按产品调:聊天型 Agent 调高 w_recency;合规型 Agent 调高 w_importance;检索型 Agent 调高 w_relevance。
💡 设计要点:为什么是加权和而不是「先按相关性筛、再按近期性排」的层级?因为层级意味着先一刀切掉一批候选,可能把近期又重要的丢掉。加权和让三个维度同时影响排序,任何一维强都能把记录拉上来,但单独一维强又不足以压倒另外两维的组合。这是多信号排序的通用心法。
Mem0g 加了一个冲突检测器(Conflict Detector)。当一条新事实与一条已存在的边矛盾时,旧边被标记为失效但不删除。时序查询(「用户三月份住在哪个城市?」)遍历「该时刻有效」的子图。
这正是第 08 节 Letta 作废模式的合规级强化:不删,只标记,可回溯。
Mem0 论文(2025)报告:
对照基线(全上下文 128k LLM、扁平向量库、扁平 KV)都低 10 分以上。基准本身不决定选型——运维形态才决定——但这些数字说明融合设计不是误差项。
Mem0 按范围切分记忆:
user_id。每次写入选一个范围。检索可跨范围查询,带每范围的权重。不假思索地混范围就是「助手把 Bob 的项目告诉了 Alice」这类事故的来源。
(user_id, type, entity) 看着简单,直到每个团队都加自己的 type。对策:每季度审计一次 type 集合。add 限制图写入条数;丢弃低置信度边。原课程 code/main.py 用标准库实现三库模式:
VectorStore —— 朴素的 token 重叠相似度,作为嵌入的替身。KVStore —— 以 (user_id, fact_type, entity) 为键的字典。GraphStore —— 类型化边 (subject, relation, object, valid)。Mem0 —— 顶层门面,带 add()、search()、融合打分、范围感知检索。核心骨架如下,用伪代码展示。
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))
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 | 向量+KV+图 | 内置 | Mem0g 冲突检测 | 自托管/托管 |
| Letta | 自带后端 | 需配 | 块级 | 自托管/托管 |
| Zep | 时序 KG | 内置 | 边带有效期 | 商业云 |
| 自建 | 全控 | 全控 | 全控 | 全控 |
本节产出一份可复用技能(原课程 outputs/skill-hybrid-memory.md):
skill-hybrid-memory.md:生成一个三库记忆脚手架,内置融合打分器、范围分类、时序作废接线。包含一份选型清单(是否真的需要三库?哪种查询占比最高?重要性怎么标?),以及一份运维清单(嵌入多久重嵌一次?type 集合多久审计?图写入每次限几条?)。Python 代码(code/main.py)是独立可运行的三库骨架,门面、打分、范围、作废都是后端无关的;把玩具相似度换成真实嵌入、把内存字典换成 Postgres/Qdrant/Neo4j 即可投入生产。
search(query, as_of=timestamp),只返回当时或之前有效的记录。哪个存储改动最大?user_feedback 维度(对检索记录点赞)。怎么防止被钻空子(Agent 只返回它已经喜欢的记录)?docs.mem0.ai)。把玩具移植成 mem0 客户端调用。在同样 20 条测试查询上对比检索质量。add/search 面后。下一节,我们离开记忆,转向「技能库与终身学习」——Voyager 让 Agent 在 Minecraft 里自己写代码、验证、把可复用技能存进技能库,下次遇到类似任务直接复用,把单次 Agent 的学习能力扩展到终身。