1. 从一次Agent“失忆”事故说起
去年冬天,我负责的一个客服辅助Agent上线第三天就出了状况。用户上午反馈过“发票抬头要开公司全称”,下午换个会话再问,Agent又老老实实回答“请提供您的抬头信息”。日志里明明存着上午的对话,检索也命中了,但Agent就是没用上。排查到凌晨两点,问题不在模型,也不在检索,而在记忆结构的设计——我们把所有历史对话平铺塞进向量库,检索时按相似度取Top-K,结果“发票抬头”这条被一堆寒暄语挤出了窗口。
这件事让我彻底意识到,LLM Agent的能力上限,很多时候不取决于底座模型多强,而取决于记忆怎么组织、怎么检索、怎么遗忘。标题“Evaluating Memory Structure in LLM Agents”说的正是这件事:不是问“Agent有没有记忆”,而是问“记忆被组织成什么结构,这种结构在什么任务上表现更好、在什么任务上会崩”。
这篇内容适合三类人看:正在搭Agent框架、被上下文窗口和检索效果折磨的工程师;做Agent评测、想知道该测哪些维度的研究者;以及产品侧想理解“为什么我的Agent记不住事”的从业者。我会把记忆结构的几种典型形态拆开讲,给出可复现的评测思路,再分享几个我在实测中踩过的坑。核心关键词就三个:LLM、Agents、Memory Structure,全文围绕它们展开。
先说结论性的判断:记忆结构不是“存什么”的问题,而是“以什么粒度存、以什么关系连、以什么策略取”的三层问题。绝大多数Agent的记忆失效,都能归到这三层里的某一层。
2. 记忆结构到底在解决什么问题
2.1 上下文窗口不是记忆,只是工作台
很多人把“上下文窗口大”等同于“记忆好”,这是个典型误解。上下文窗口是工作台,它决定同一时刻能摊开多少材料;记忆是仓库加索引系统,它决定你能否在需要时把对的材料摊到工作台上。窗口再大,如果检索策略是“按时间倒序塞最近N条”,遇到长周期任务照样抓瞎。
我做过一个对比实验:同一个任务(跨20轮对话追踪一个订单的状态变更),方案A是纯长上下文,把20轮全塞进去;方案B是结构化记忆,每轮抽取“订单号-状态-时间”三元组存进结构化表,检索时按订单号精确取。结果方案A在15轮之后开始出现状态混淆,方案B全程稳定。原因很简单:长上下文里,模型要在噪声中自己做注意力分配,而结构化记忆把“找什么”这件事提前做完了。
所以记忆结构的第一价值,是把检索负担从模型转移到系统。模型擅长推理,不擅长在几千token里精准捞针,那就别让它捞。
2.2 三种记忆形态的分工
从业界常见实践看,Agent记忆大致分三层,这个分层不是学术定义,是工程上好用的划分:
| 记忆层 | 存储内容 | 生命周期 | 典型实现 |
|---|---|---|---|
| 工作记忆 | 当前任务上下文 | 单次会话 | 上下文窗口 |
| 情景记忆 | 具体交互事件 | 跨会话 | 向量库+元数据 |
| 语义记忆 | 抽象事实/偏好 | 长期 | 结构化表/知识图谱 |
工作记忆不用多说,就是prompt里那点东西。情景记忆是“上周三用户说过什么”,语义记忆是“这个用户偏好简洁回复”。关键洞察是:这三层的检索策略应该完全不同。工作记忆靠位置,情景记忆靠相似度+时间衰减,语义记忆靠精确键值或图遍历。把它们混在一个向量库里用同一套检索,是绝大多数记忆失效的根源。
2.3 为什么“结构”比“容量”更值得评测
评测记忆,新手常测“能存多少条”“检索召回率多少”。这些指标有用,但不够。真正决定Agent表现的是结构适配度:你的记忆结构是否匹配任务的依赖模式。
举个例子,多跳问答任务(A依赖B,B依赖C)需要的是链式结构,记忆之间要有显式引用关系;而偏好追踪任务需要的是覆盖式结构,新偏好要能覆盖旧偏好。用链式结构做偏好追踪,会出现“新旧偏好同时被检索出来,模型不知道该听哪个”的尴尬。所以评测记忆结构,本质是评测“结构-任务”的匹配度,而不是孤立地测存储性能。
3. 四种典型记忆结构的拆解与适用边界
3.1 扁平向量记忆:上手最快,崩得也最快
这是最普遍的起点:所有交互文本切块、embedding、存向量库,检索时按余弦相似度取Top-K。优点是实现成本极低,半天能跑通。缺点是三个致命问题。
第一,相似度不等于相关性。用户问“退款流程”,检索可能命中“退款政策咨询”的历史,但那条历史里其实是在问“退款要多久”,语义相似但意图不同。第二,无时间感知。三个月前的偏好和昨天的偏好权重一样,除非你手动加时间衰减。第三,无冲突消解。用户先说“要发票”,后说“不要发票了”,两条都在库里,检索时可能同时返回。
我实测下来,扁平向量记忆在单轮事实查询类任务上够用(比如FAQ),但在多轮状态追踪类任务上,超过10轮就开始明显退化。如果你的Agent任务涉及状态变更、偏好演化、多步依赖,别指望它。
3.2 分层摘要记忆:用压缩换长程
分层摘要的思路是:原始对话先存着,但检索时用的是一层层的摘要。比如每5轮生成一个段落摘要,每20轮生成一个章节摘要,检索先命中摘要,再下钻到原始片段。这解决了长程依赖问题——20轮前的关键信息被压缩进摘要,不会因为token太多被挤掉。
但摘要是有损压缩,损什么、留什么,取决于摘要prompt的设计。我踩过的坑是:摘要prompt写的是“总结对话要点”,结果模型把“用户情绪激动”这类元信息全丢了,只留了事实。后来改成“总结事实要点+用户明确表达的偏好+未解决的问题”,效果才上来。
分层摘要适合长周期、低精度要求的场景,比如周报生成、长期陪伴类Agent。不适合需要精确引用原文的场景,比如合同条款问答。
3.3 图结构记忆:关系推理的利器,维护的噩梦
图结构记忆把实体和关系显式建模:节点是实体(用户、订单、产品),边是关系(属于、偏好、依赖)。检索时用图遍历,能天然支持多跳推理。比如“用户A的订单B涉及的产品C的替代品”,图遍历三步就能拿到,向量检索很难做到。
但图的维护成本高。每轮对话都要做实体抽取和关系抽取,抽取错了图就脏了。而且图schema需要预先设计,任务变了schema可能要大改。我的经验是:只有当任务本身有明确的关系结构时才上图,比如供应链、组织架构、知识依赖。纯闲聊或开放问答,上图是杀鸡用牛刀。
3.4 混合结构:工程上的现实选择
真实项目里,纯用一种结构的很少。常见组合是:向量库做粗召回,结构化表做精过滤,图做关系补全。比如先向量检索取Top-50,再用元数据(时间、类型、实体ID)过滤到Top-10,最后对涉及多跳的查询走图遍历。
混合结构的难点在路由:什么查询走哪条路。我的做法是加一个轻量分类器,判断查询类型(事实查询/关系查询/偏好查询),再分发到对应检索器。这个分类器不用很准,80%准确率就能显著提升整体效果,因为分错了顶多是走了次优路径,不会完全失效。
4. 评测记忆结构的可落地方法
4.1 先定义任务依赖模式,再选指标
评测前必须先回答:我的任务依赖模式是什么?是单跳事实(问什么答什么)、多跳链式(A→B→C)、时序演化(状态随时间变)、还是冲突消解(新信息覆盖旧信息)?不同模式对应不同指标。
| 依赖模式 | 核心指标 | 评测方法 |
|---|---|---|
| 单跳事实 | 召回率、精确率 | 构造QA对,测检索命中 |
| 多跳链式 | 路径完整率 | 构造多跳问题,测能否取全链路 |
| 时序演化 | 状态一致率 | 构造状态变更序列,测最终状态是否正确 |
| 冲突消解 | 覆盖准确率 | 构造矛盾信息,测是否取最新 |
我见过太多团队只测召回率,结果召回率95%但Agent表现很差,因为召回的内容里有冲突、有过期、有噪声。召回率是必要不充分指标。
4.2 构造“记忆压力测试集”的四个维度
评测集不能只用正常对话,要专门构造压力场景。我常用的四个维度:
- 干扰维度:在关键信息前后插入大量无关对话,测检索抗噪能力。
- 时间维度:同一事实在不同时间点有不同值,测时序感知。
- 冲突维度:显式构造矛盾陈述,测冲突消解。
- 长程维度:关键信息出现在很早的位置,中间隔大量对话,测长程保持。
每个维度构造20-50个case,人工标注期望输出,就能得到一个可复现的评测集。这个集子不用大,但要“毒”,能暴露结构缺陷。
4.3 一个最小可复现的评测脚本思路
不用复杂框架,用Python加一个向量库就能跑。核心逻辑是:构造对话序列→写入记忆系统→按查询检索→对比期望。
# 伪代码示意,展示评测流程骨架 test_cases = [ { "dialogue": ["用户:我要开发票", "用户:抬头改成公司全称", "用户:算了还是个人吧"], "query": "发票抬头是什么", "expected": "个人" }, # ... 更多case ] for case in test_cases: memory = MemorySystem() for turn in case["dialogue"]: memory.add(turn) result = memory.retrieve(case["query"]) score = evaluate(result, case["expected"])关键在MemorySystem的实现,换不同结构(扁平/分层/图)跑同一套case,对比分数,就能看出结构差异。这个脚本我跑过几十次,最大的价值不是分数本身,而是能定位到具体哪类case崩了。
4.4 别忽略“遗忘”的评测
记忆评测里最少被提的是遗忘。好的记忆系统要能主动遗忘:过期信息、被覆盖信息、低价值信息。评测遗忘,可以构造“信息过期”场景,看系统是否还会检索出过期内容。我实测发现,加了遗忘机制的Agent,在长周期任务上的准确率能提升15%以上,因为噪声少了。
5. 实测中暴露的结构性陷阱
5.1 检索窗口的“挤出效应”
前面提到的发票事故,本质是Top-K窗口被低价值内容挤占。K设小了漏关键信息,K设大了噪声多。我的解法是动态K:先按相似度取一个较大的候选集,再用元数据打分(时间新鲜度、信息类型权重、是否被引用过)重排,取重排后的Top-K。这样K可以设得小,但命中率不降。
5.2 摘要的“信息漂移”
分层摘要跑久了会出现信息漂移:第一层摘要还准,第二层摘要开始模糊,第三层摘要已经和原始对话对不上了。原因是每层摘要都在上一层基础上生成,误差累积。解法是摘要必须能回溯到原文,每层摘要存原文引用ID,检索命中摘要后强制下钻验证。宁可多一次检索,不要信摘要。
5.3 图结构的“脏数据传染”
图记忆最怕实体抽取错误。抽错一个实体,可能连出一串错误关系。我的做法是抽取结果先入待验证区,只有被后续对话确认过的实体关系才正式入图。比如用户说“我在用某产品”,这是弱信号;用户说“我买了某产品并续费了”,这是强信号。弱信号先挂着,强信号才建边。
5.4 混合路由的“误分类代价”
路由分类器分错查询类型,代价不对称。把关系查询误判成事实查询,顶多检索效果差;把事实查询误判成关系查询,可能触发图遍历,返回一堆不相关的关系路径。所以路由要保守:不确定时走通用路径(向量+元数据过滤),别激进走图。
6. 把记忆结构调优落到工程实践
6.1 从任务反推结构,而不是从结构反推任务
最常见的错误是先选技术(“我要用向量库”“我要上图”),再想怎么套任务。正确顺序是:列出任务的所有依赖模式→为每种模式设计最小可行结构→合并结构→评测→迭代。我现在的习惯是先用表格把任务依赖模式列全,再决定存储方案,能省掉大量返工。
6.2 记忆写入时的“预处理”决定上限
检索效果的上限,在写入时就决定了。写入时做三件事能显著提升后续检索:打标签(时间、类型、实体、置信度)、去重(相似内容合并)、建索引(为常用查询维度建倒排)。这三件事在写入时做,成本是检索时的十分之一。
6.3 监控记忆健康度的三个指标
上线后要持续监控:检索命中率(查询有结果的比例)、冲突率(检索结果内部矛盾的比例)、过期率(检索出已过期信息的比例)。这三个指标任何一个异常,都说明记忆结构需要调整。我一般设阈值:命中率低于80%、冲突率高于5%、过期率高于10%,就触发告警。
6.4 迭代节奏:小步快跑,用评测集卡关
记忆结构调优不要一次大改。每次只改一个维度(比如只调检索策略,不动存储结构),用评测集跑分,涨了保留,跌了回滚。我一般两周一个迭代周期,每个周期聚焦一个结构问题。这样虽然慢,但每一步都可解释、可复现,不会出现“改了一堆不知道哪个起作用”的情况。
7. 我个人在记忆结构上的一点体会
做了这么多Agent项目,我最大的体会是:记忆结构的复杂度要和任务的复杂度匹配,多一分都是负担。见过太多团队一上来就上图数据库、上多层摘要,结果维护成本压垮了迭代速度,最后还不如一个调好参数的向量库加元数据过滤。
另一个体会是,评测记忆结构,别只看最终答案对不对,要看检索链路对不对。答案对了可能是模型猜的,链路对了才是记忆真的起作用。我现在评测必看中间检索结果,这一步能暴露80%的结构问题。
最后分享一个实用技巧:如果你的Agent记忆效果不好,先别急着换结构,先把写入时的标签体系补全。很多时候不是检索算法不行,是写入时信息就丢了。标签补全后,用最简单的向量检索都能有明显提升。这个技巧帮我省过至少三次大重构。