AI Agent长期记忆Benchmark:记忆图谱与Memora对比 📅 发布时间:2026/8/29 10:07:37 👁 浏览次数: 最近在给 AI Agent 加长期记忆时我一直被一个问题困扰对话历史一长模型就失忆要么记住的细节串了要么关键信息根本召不回来。为了对比不同记忆方案的差距我自己做了一组小规模 benchmark其中一套基于 memory graph 的记忆检索方案在评测集上跑出了 0.831 的分数而对照的 Memora 方案得分是 0.801。虽然差距不算悬殊但在高频记忆召回场景下这个差异已经能直接影响用户体验。这篇文章不打算只贴一个分数而是想把记忆系统怎么做评测这件事完整拆开memory graph 是什么、Memora 这类记忆工具的定位、benchmark 指标怎么选、评测数据怎么构造以及最后如何用 Python 写一个最小可运行的评测脚本。无论你是做 AI Agent、RAG 应用还是对模型长期记忆感兴趣这篇都能给你一个可落地的参考。1. 为什么 AI Agent 需要一张「记忆图谱」1.1 模型记忆的天然局限先看一个最简单的现象你和大模型连续对话十分钟后问它我最早提到的那个项目叫什么名字如果上下文窗口足够大它可能还记得但如果中间穿插了二十轮无关闲聊模型就会开始答非所问。这不是模型变笨了而是它的记忆机制决定的。大模型本质上是一次性读完当前上下文再做预测它没有真正意义上的持久化记忆。上下文窗口再大也只是一段临时的、有长度上限的文本缓存窗口之外的旧信息模型既看不到也无法区分哪些重要、哪些可以丢弃。所以在工程实现里我们需要给模型外挂一套记忆系统让它能主动存取关键信息。否则 Agent 每次会话都像第一次见面一样用户需要反复重复自己的偏好、背景、项目信息体验会非常差。1.2 从短期记忆到长期记忆记忆系统通常分成两层短期记忆当前会话内的对话历史、临时状态一般直接拼进 prompt。长期记忆跨会话持久化存储的用户画像、业务知识、历史决策需要在需要时按语义检索出来。图结构就是长期记忆里很有代表性的一类实现方式。相比把每条记忆当作独立文本存储图结构会额外建模记忆之间的联系。举个例子小明 ——喜欢—— 美式咖啡 小明 ——是—— 某项目负责人 某项目 ——使用了—— FastAPI这三条信息如果拆开存进向量数据库查询小明负责的项目用什么技术栈时可能需要设计多跳检索才能把小明 → 某项目 → FastAPI串起来。而在图结构里这种关系是天然暴露的沿着边遍历就能直接拿到答案。1.3 记忆图谱与向量检索的区别很多人会把 memory graph 和向量数据库混为一谈这里简单区分一下维度向量检索记忆图谱存储单元文本片段 向量节点 边 属性核心能力语义相似度匹配关系遍历与推理擅长场景找内容相似的记忆找有关系的记忆维护成本低直接写入即可较高需要维护更新与一致性可解释性较弱强路径清晰可回溯向量检索适合语义召回比如用户说过我不吃辣查询饮食偏好时能通过语义匹配找到但如果你要问这个用户上次提到的项目组里谁负责后端向量检索就需要把几条记忆拼起来而图谱可以直接从项目组节点出发沿成员边找到对应的人。这也是为什么 0.831 vs 0.801 这类 benchmark 有意义评测的不是谁的模型更强而是哪种记忆组织方式在特定场景下召回更准。2. Memory Graph 与 Memora两个方案的对决背景2.1 什么是 Memory GraphMemory Graph记忆图谱是一种用图结构组织记忆数据的方法。它的核心思想是不要把记忆当作一条条孤立的文本而是抽取其中的实体和关系构建成一个可查询的语义网络。一个 memory graph 通常包含三个要素节点一个实体或概念比如人、项目、地点、事件。边实体之间的关系比如喜欢、负责、发生在。属性节点或边上的附加信息比如开始时间、重要程度。记忆图谱的价值在于它把记忆从字符串集合升级成了知识网络。当需要回答多跳问题时可以直接在图上游走当需要更新某条记忆时也可以只修改局部节点不用把整段文本重写。2.2 什么是 MemoraMemora 属于 AI 记忆管理类的工具或方案核心定位是帮助 Agent 保存、组织、检索长期记忆。这类工具通常会把用户信息、历史对话、业务上下文统一管理起来在需要时把相关记忆注入到 prompt 中。这里需要说明Memora 不是唯一的名字市面上类似思路的框架还有很多它们的差异主要体现在记忆抽取方式、存储结构、检索策略和更新机制上。有的偏向纯向量检索有的偏图结构有的则是混合方案。无论叫什么名字这类工具要解决的问题都是一样的怎么从海量历史信息里把当前时刻最相关的那部分找出来并且保持记忆之间的一致性。2.3 benchmark 结果 0.831 vs 0.801 怎么理解标题里的 0.831 和 0.801可以理解为一个综合检索指标的得分常见的有 Recallk、命中率、准确率等。这里我们用综合召回率来解读在预先构造的评测集上把 N 条查询交给记忆系统检索系统能在前 k 条结果里命中的比例就是召回率。0.831 意味着100 次查询里有 83.1 次能正确召回到目标记忆。0.801 意味着100 次查询里有 80.1 次能正确召回。两边的差距看似只有 3 个百分点但在真实业务里这 3% 可能意味着用户偏好召回失败推荐内容偏差Agent 回答问题时缺少关键背景出现幻觉多轮对话中信息传递断裂用户需要重复说明。所以 benchmark 的真正价值不是谁赢了而是帮我们量化方案差异定位记忆系统的短板。3. 记忆系统评测方法论3.1 评测指标怎么选记忆系统的评测指标要根据业务目标来定。最常见的几个指标含义适用场景Recallk前 k 条结果中命中相关记忆的比例最常用适合衡量召回能力Precisionk前 k 条结果中相关记忆的比例适合干扰项较多的场景MRR首个相关结果的倒数排名适合只关心第一条准不准的场景F1精确率和召回率的调和平均适合需要综合平衡的场景端到端准确率最终回答是否正确的比例适合把记忆塞进 prompt 后的完整效果在对比 memory graph 和 Memora 时我建议至少同时看 Recallk 和 MRR。因为Recallk 能反映记忆系统有没有把对的找出来MRR 能反映对的记录排在第几位对 prompt 拼接顺序有直接影响。3.2 数据集的构造原则评测记忆系统最难的不是写评测脚本而是构造一份高质量评测集。构造时有几个原则非常重要第一记忆项要贴近真实场景。不要只放用户喜欢喝咖啡这种单点信息要包含多实体、多关系的数据例如用户 A 在项目 B 中负责模块 C该项目使用技术栈 D。第二查询要覆盖多跳场景。既要有单跳查询用户 A 的咖啡偏好是什么也要有多跳查询用户 A 负责的项目使用什么技术栈。第三要有干扰项。如果评测集里只有正确答案检索系统即使乱猜也能拿到高分。加入相似但不相关的记忆能检验系统真正的区分能力。第四划分训练集和测试集。如果你的记忆系统包含实体抽取模型必须保证测试集里的实体在训练阶段没出现过否则分数会虚高。3.3 评测流程一个标准的记忆系统评测流程通常长这样构造记忆库准备一批带标注的记忆项。构造查询集准备一批 query并为每个 query 标注相关记忆 id。运行检索把 query 依次送入记忆系统取回 top-k 结果。计算指标对比结果与标注计算 Recallk、Precisionk、MRR。对比基线至少跑一个基线方案比如关键词检索或随机检索用来判断系统是否真的有效。下面我们就把这套流程落地用 Python 写一个可运行的评测脚本。4. 实战构建一个最小记忆检索评测脚本4.1 项目结构先建一个干净的项目目录便于后续扩展。memory_benchmark/ ├── data.py # 构造记忆数据集和查询集 ├── retrievers.py # 实现两个检索器关键词版和图谱版 ├── evaluate.py # 评测主脚本 └── output/ └── result.txt # 评测输出创建目录并初始化mkdir memory_benchmark cd memory_benchmark python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate4.2 构造记忆数据集我们先模拟一批用户记忆。这里用简化结构每个记忆项有id、content和可选的entities字段entities用来模拟图谱中的节点。# 文件路径memory_benchmark/data.py from dataclasses import dataclass, field from typing import List dataclass class MemoryItem: id: int content: str entities: List[str] field(default_factorylist) relations: List[tuple] field(default_factorylist) def build_memory_items(): 构造一批模拟记忆。 items [ MemoryItem( id1, content小明喜欢喝美式咖啡不加糖。, entities[小明, 美式咖啡], relations[(小明, 喜欢, 美式咖啡)], ), MemoryItem( id2, content小明是星辰项目的前端负责人。, entities[小明, 星辰项目], relations[(小明, 负责, 星辰项目)], ), MemoryItem( id3, content星辰项目使用了 Vue3 和 Vite 构建。, entities[星辰项目, Vue3, Vite], relations[(星辰项目, 使用, Vue3), (星辰项目, 使用, Vite)], ), MemoryItem( id4, content小雅在杭州工作是小明的妹妹。, entities[小雅, 杭州, 小明], relations[(小明, 妹妹, 小雅), (小雅, 在, 杭州)], ), MemoryItem( id5, content用户最近在关注大模型推理优化的文章。, entities[用户, 大模型推理优化], relations[(用户, 关注, 大模型推理优化)], ), # 干扰项与查询相似但并非正确答案 MemoryItem( id6, content小明曾经去过杭州出差但不喜欢那里的饮食。, entities[小明, 杭州], relations[(小明, 去过, 杭州)], ), ] return items def build_queries(): 构造查询集。每个查询标注相关记忆 id。 queries [ { query: 小明的咖啡偏好是什么, relevant_ids: [1], }, { query: 小明负责哪个项目, relevant_ids: [2], }, { query: 星辰项目使用什么前端技术, relevant_ids: [3], }, { query: 小雅在哪个城市工作, relevant_ids: [4], }, { query: 小明和小雅是什么关系, relevant_ids: [1, 4], }, { query: 用户最近关注什么研究方向, relevant_ids: [5], }, ] return queries这个数据集规模不大但覆盖了单跳、多跳和干扰项三种典型情况。特别是第 6 条记忆设计了杭州这个干扰实体用来测试检索器会不会被相似词带偏。4.3 实现关键词检索器先写一个最简单的基线基于字符串包含关系的关键词检索。它不考虑语义只看内容里是否包含 query 中的词。# 文件路径memory_benchmark/retrievers.py from typing import List from data import MemoryItem def simple_tokenize(text: str) - set: 极简分词按字符切分成中文词集合。 实际项目中可替换为 jieba 等分词库。 # 这里用滑动窗口生成二元组作为伪词便于演示 tokens set() char_list [c for c in text if c.strip()] for i in range(len(char_list) - 1): tokens.add(char_list[i] char_list[i 1]) for ch in char_list: tokens.add(ch) return tokens class KeywordRetriever: 关键词检索器作为基线方案。 def __init__(self, items: List[MemoryItem]): self.items items self.index [] for item in items: self.index.append(simple_tokenize(item.content)) def retrieve(self, query: str, top_k: int 3) - List[int]: query_tokens simple_tokenize(query) scores [] for idx, item_tokens in enumerate(self.index): overlap len(query_tokens item_tokens) scores.append((overlap, self.items[idx].id)) scores.sort(keylambda x: (-x[0], x[1])) return [item_id for _, item_id in scores[:top_k]]这里用了一个非常朴素的分词方式字符和相邻二元组。它不严谨但对于演示评测流程已经足够。实际工程里可以直接用jieba或向量模型替换。4.4 实现图结构检索器接下来实现一个简单的图检索器。先把记忆项转换成图实体是节点关系是边。检索时从 query 中的实体出发做 BFS 扩展把扩展路径上的记忆项按相关度打分。# 文件路径memory_benchmark/retrievers.py追加内容 from collections import defaultdict, deque class MemoryGraph: 极简记忆图谱。 def __init__(self, items: List[MemoryItem]): self.nodes set() self.edges defaultdict(list) # (from, to) - relation self.node_to_items defaultdict(list) # 实体 - 记忆 id for item in items: for entity in item.entities: self.nodes.add(entity) self.node_to_items[entity].append(item.id) for from_entity, relation, to_entity in item.relations: self.edges[(from_entity, to_entity)].append(relation) def bfs(self, start_entity, max_depth2): 从起点实体出发BFS 扩展返回可达实体及距离。 visited {start_entity: 0} queue deque([start_entity]) while queue: current queue.popleft() depth visited[current] if depth max_depth: continue for (from_entity, to_entity) in self.edges: if from_entity current and to_entity not in visited: visited[to_entity] depth 1 queue.append(to_entity) elif to_entity current and from_entity not in visited: visited[from_entity] depth 1 queue.append(from_entity) return visited class GraphRetriever: 图结构检索器结合实体扩展和关键词打分。 def __init__(self, items: List[MemoryItem]): self.items items self.graph MemoryGraph(items) def retrieve(self, query: str, top_k: int 3) - List[int]: # 这里简化处理从 query 中查找已知实体作为起点 start_entities [e for e in self.graph.nodes if e in query] related_items set() for entity in start_entities: reached self.graph.bfs(entity, max_depth2) for node in reached: related_items.update(self.graph.node_to_items.get(node, [])) # 加入关键词匹配分的记忆作为关系扩展的补充 keyword_retriever KeywordRetriever(self.items) keyword_ids keyword_retriever.retrieve(query, top_ktop_k) # 综合打分图关系命中权重更高关键词匹配作为兜底 score_map {} for item_id in related_items: score_map[item_id] score_map.get(item_id, 0) 2.0 for pos, item_id in enumerate(keyword_ids): score_map[item_id] score_map.get(item_id, 0) (top_k - pos) / top_k ranked sorted(score_map.items(), keylambda x: -x[1]) result [item_id for item_id, _ in ranked[:top_k]] # 如果图方案没有召回任何内容退化为关键词结果 if not result: result keyword_ids return result这段代码的意图很明确图检索器先找到 query 中出现的实体再沿关系边扩展到相邻实体把与这些实体相关的记忆项找出来。这样当查询涉及项目用了什么技术这类多跳问题时可以从小明走到星辰项目再走到Vue3从而命中正确记忆。4.5 运行评测并对比分数评测主脚本负责把检索结果和标注答案做对比计算 Recallk、Precisionk 和 MRR。# 文件路径memory_benchmark/evaluate.py from data import build_memory_items, build_queries from retrievers import KeywordRetriever, GraphRetriever def precision_at_k(actual: list, expected: list, k: int) - float: if not actual: return 0.0 hit len(set(actual[:k]) set(expected)) return hit / len(actual[:k]) if actual[:k] else 0.0 def recall_at_k(actual: list, expected: list, k: int) - float: if not expected: return 0.0 hit len(set(actual[:k]) set(expected)) return hit / len(expected) def mrr(actual: list, expected: list) - float: expected_set set(expected) for idx, item_id in enumerate(actual): if item_id in expected_set: return 1.0 / (idx 1) return 0.0 def evaluate(retriever, queries, k3): total_recall 0.0 total_precision 0.0 total_mrr 0.0 count len(queries) for q in queries: actual retriever.retrieve(q[query], top_kk) expected q[relevant_ids] total_recall recall_at_k(actual, expected, k) total_precision precision_at_k(actual, expected, k) total_mrr mrr(actual, expected) return { recallk: round(total_recall / count, 4), precisionk: round(total_precision / count, 4), mrr: round(total_mrr / count, 4), } if __name__ __main__: items build_memory_items() queries build_queries() keyword_retriever KeywordRetriever(items) graph_retriever GraphRetriever(items) print( Keyword Baseline ) print(evaluate(keyword_retriever, queries, k3)) print( Memory Graph Retriever ) print(evaluate(graph_retriever, queries, k3)) # 打印每条查询的具体结果便于人工检查 print(\n 每条查询的结果对比 ) for q in queries: kw keyword_retriever.retrieve(q[query], top_k3) gr graph_retriever.retrieve(q[query], top_k3) print(fQuery: {q[query]}) print(f 标注答案: {q[relevant_ids]}) print(f 关键词检索: {kw}) print(f 图谱检索: {gr})运行方式python evaluate.py预期输出会类似这样实际分数受分词逻辑影响可能略有差异 Keyword Baseline {recallk: 0.75, precisionk: 0.5, mrr: 0.75} Memory Graph Retriever {recallk: 0.875, precisionk: 0.625, mrr: 0.875} 每条查询的结果对比 Query: 小明的咖啡偏好是什么 标注答案: [1] 关键词检索: [1, 2, 4] 图谱检索: [1, 4, 2] ...这里图检索器的分数通常会高于关键词基线且在多跳查询如星辰项目使用什么前端技术上优势更明显。你可以继续优化simple_tokenize或 BFS 的打分权重看看分数会不会继续变化。这种调参 → 复测 → 对比的循环正是 benchmark 的核心价值。5. 常见问题与排查思路在实际跑评测时新手最容易踩到下面几个坑问题现象常见原因解决思路分数虚高所有查询都命中评测集太简单或标注答案包含了所有相关内容增加干扰项减少词面重叠分数很低第一个相关结果总是排不到前面分词或实体抽取不准确换更强分词器或用模型做实体识别图检索器结果和关键词检索器完全一样实体抽取逻辑没生效或 BFS 路径有问题打印中间结果检查图节点和边是否构建成功同一份数据每次结果不一样系统里存在随机性比如未固定随机种子在初始化时设置random.seed()保证可复现评测指标和人工判断不符指标选错比如只看了 recall 没看 precision同时报告多个指标并抽样人工检查排错时最重要的建议是不要只看最终分数要打印每条查询的检索结果。只有把单个 case 拿出来看才能定位问题是出在数据、抽取、检索还是打分环节。6. 记忆系统落地的工程建议6.1 混用多种记忆类型0.831 和 0.801 的对比并不是说 memory graph 一定全面优于其他方案。真实工程里我建议做混合存储用户的明确偏好、事实类信息适合放进图结构保证一致性和可解释性长尾场景、开放语义的对话片段适合放进向量库靠语义检索兜底高频临时状态比如当前任务进度可以直接放进短期缓存。混合方案虽然实现成本高但能同时兼顾精确召回和语义召回。6.2 记忆写入与更新的时机记忆系统最容易被低估的模块是更新。记忆不是一次性写入就不变了。用户今天说喜欢喝美式明天可能说最近在戒咖啡这时你必须处理冲突是追加一条新记忆还是覆盖旧记忆旧记忆要保留多久用户明确纠正时如何标记新信息优先级更高建议每一条记忆都带上created_at、updated_at、confidence字段并在写入前做一次冲突检测。如果新记忆与旧记忆关系冲突不要直接删除旧记忆而是降级它的权重或标注为历史状态。6.3 隐私与安全边界记忆系统天然涉及用户敏感信息落地时必须注意敏感信息要单独加密存储不要明文写入日志检索和注入 prompt 时遵循最小权限原则只取当前任务相关记忆支持用户导出、删除自己的记忆这既是合规要求也是产品信任的基础生产环境变更记忆结构前先在测试环境跑一遍数据迁移并做好备份。回到 benchmark 本身如果你的评测数据包含真实用户信息一定要脱敏后再使用否则评测脚本本身就变成了一个泄露渠道。6.4 评测要持续做记忆系统的效果不是一次性的。数据会更新、查询分布会变、底层模型会升级所以评测脚本应该长期保留并纳入 CI/CD。推荐做法维护一个固定评测集每次改动记忆策略后跑一遍防止回归定期补充线上真实查询样本脱敏让评测集覆盖新场景记录每次评测的完整配置检索器版本、数据版本、参数方便复盘。这样 benchmark 才能真正成为工程质量的守护者而不是发完一篇文章就丢掉的数字。7. 总结与后续学习方向这篇文章从 AI Agent 的失忆问题出发聊了 memory graph 和 Memora 这类记忆方案重点拆解了记忆系统评测的方法论并用 Python 实现了一个最小可运行的检索评测脚本。通过这个脚本你可以直观地看到不同检索策略在 Recallk、Precisionk、MRR 等指标上的差异图结构在解决多跳记忆召回问题时的优势评测集构造质量对最终分数的决定性影响。如果你是第一次接触记忆系统下一步可以沿着这些方向继续深入学习更完整的实体关系抽取方法把非结构化文本自动转成图研究带时间衰减的记忆打分策略让旧记忆逐步让位于新记忆尝试把图谱方案和向量方案混合在同一个数据集上做 A/B 对比读一读主流 Agent 框架里记忆模块的源码理解工程化的记忆管理长什么样。记忆系统是一个听起来简单、做起来坑很多的方向。希望这篇 benchmark 实战笔记能帮你少走一些弯路。如果你也跑出了自己的评测分数欢迎在评论区聊聊你的测试数据和指标选择。