【AI Agent面试系列】AI Agent记忆机制知识点2
上一篇文章主要讲了如何设计Agent记忆系统,本文主要介绍如何定位记忆系统效果不好的问题原因、如何评测记忆系统等。
1、记忆系统效果不好,你怎么定位是哪个环节的问题?
记忆系统是一条流水线,任何一段坏了,最终表现都是"答得不对"。但不同环节坏掉,症状不同。
做法:先按症状分类,再用对照实验和自检索测试快速排除,最后才逐段深挖。优先怀疑抽取和覆盖,最后才怀疑向量检索——因为前者出错概率高得多,而后者一旦配好就很少出问题。
对照实验
关掉改写,只做原始 query 检索。
- 如果关掉改写后效果变好,说明改写有害,直接下线。
- 如果效果不变,说明改写无效,白付成本。
- 如果效果变差,改写才有价值,再去看它具体哪里改错了。
定位顺序(按成本从低到高)
1. 关改写对照实验 —— 5 分钟,排除一整类问题 2. 自检索测试 —— 10 分钟,验证索引是否可用 3. 抽取离线评测 —— 需要标注集,但能定位漏抽/错抽 4. 覆盖状态检查 —— 查库,看是否有双活跃记录 5. 注入排除法 —— 喂完美结果,看是否还错具体说明如下:
1.1、先看症状属于哪一类
按照写入 -> 检索 -> 注入流程顺序,主要症状表现如下:
| 症状 | 出问题环节 | 可能原因 |
|---|---|---|
| 该记的没记住 | 写入 | 被门控过滤、抽取问题 |
| 记住了旧值,新值没生效 | 写入 | 覆盖问题 |
| 记住了但检索不到 | 检索 | Query改写有问题 |
| 答得对但很慢/很贵 | 检索 | 检索(top_k 过大、无过滤) |
| 时好时坏,不稳定 | 检索 | 改写(需要上下文) |
| 检索到了但答错 | 注入 | 注入问题,比如位置靠中间等 |
需要先排除非记忆系统问题,比如回答慢、检索到了但答错等也可能是模型问题。
1.2、逐段隔离测试
1.2.1、抽取
测法:拿一批已知答案的对话,只看抽取输出,不看检索结果。
# 输入:对话文本 # 期望输出:应该抽出的记忆列表(人工标注) # 实际输出:系统抽出的记忆列表 # 指标:召回率(漏抽多少)、准确率(错抽多少)关键:这一步必须脱离检索单独测。如果抽取漏了,后面检索再好也检不到,但你会误以为是检索问题。
常见故障:
- 漏抽:显式声明(“记住…”)没被识别 → 规则没覆盖
- 错抽:把临时状态(“这次先用 A”)当决策 → 缺排除规则
- 槽位错:preference.tone 和 preference.length 混用 → 词表没约束
1.2.2、写入
测法:给定抽取结果,检查写入后的库状态。
# 输入:抽取出的记忆 # 检查:库里是否多了一条?旧记录 superseded_by 是否设了? # 指标:写入成功率、覆盖正确率常见故障:
- 唯一约束误伤:(key, scope) 冲突导致新记录被拒
- 覆盖没生效:superseded_by 没设,新旧两条都活跃 → 检索返回旧值
- 置信度阈值过高:inferred 类全被过滤
1.2.3、索引
测法:绕过检索,直接算向量相似度。
# 给定一条已知记忆,用它的"标准问法"去检索 # 看它自己能不能排进 top_1 这是自检索测试(self-retrieval):用记忆本身生成 query,看能否检回自己。 如果连自己都检不回,索引一定有问题。常见故障:
- embedding 文本构造错:只 embed 了 value(“Go”),语义太短
- key 的点没换空格:project.tech_stack 被切碎
- payload 缺 superseded_by:失效记录占满 top_k
1.2.4、改写
测法:对比"改写前检索"和"改写后检索"的结果差异。
# 指标: # - 改写前后结果相同 → 改写无效(白做) # - 改写后结果变差 → 改写有害 # - 改写后空结果率上升 → 改写过度收窄关键:改写是唯一一个可能让效果变差的环节。其他环节坏了是"没帮上忙",改写坏了是"帮倒忙"。
常见故障:
- 补全指代补错:把"那个方案"补成 A,实际是 B
- 推断式改写:替用户提问,检索方向被悄悄改掉
- 过度收窄:加了太多 filter,把正确结果过滤掉
1.2.5、注入
测法:给定正确的检索结果,看最终回答对不对。
# 输入:人工构造的"完美检索结果" # 输出:模型回答 # 如果这样还答错 → 注入或生成的问题,与检索无关 这一步是排除法:如果喂了正确答案还答错,说明问题不在检索链路,在上下文组织或生成。常见故障:
- 注入位置错:记忆放在长上下文中间,被忽略
- 格式混乱:多条记忆无结构拼接,模型分不清哪条是当前值
- 缺时间信息:模型不知道哪条更新
2、你怎么评估记忆系统好不好?
评估记忆系统要分层归因——L1 抽取看召回和误收,L2 存储看双活跃率,L3 检索看自检索和改写增益,L4 端到端必须做对照实验。不要用"回答看起来对不对"当主指标,那评估的是模型,不是记忆系统。
记忆系统的评估必须分层、可归因——每个指标要能指向具体环节。
评估的四个层次:
L1 抽取质量 —— 该记的记住了吗?不该记的记了吗? L2 存储正确性 —— 存进去的东西结构对吗?覆盖对吗? L3 检索质量 —— 需要的时候找得到吗?找得准吗? L4 端到端效果 —— 最终回答变好了吗?必须按照L1->L4的顺序评估。如果 L1 就漏了,L4 再差也说明不了检索有问题。反过来,L4 好也不代表系统好——可能只是这次问题简单,记忆没起作用也答对了。
L1 抽取质量
需要一个人工标注集:给定对话,标出"应该抽出哪些记忆"。指标 定义 关注点 召回率 抽出的 / 应抽出的 漏抽是最致命的,漏了后面全白搭 准确率 正确的 / 抽出的 错抽会污染库,且难以清理 槽位准确率 key 归对的 / 抽出的 槽位错会导致覆盖判断失效 误收率 临时信息被收进库的比例 “这次先用 A” 这类不该进 误收率要单独看。它和准确率不是一回事:准确率看"抽出来的对不对",误收率看"压根不该抽的抽了没"。后者错了代价更大,因为临时信息进库后,靠 expires_at 补救并不可靠。
L2 存储正确性
这一层不需要人工标注,查库就能算:指标 检查方式 双活跃率 (key, scope) 下 superseded_by=null 的记录数 > 1 的比例,应为 0 覆盖及时率 新值写入后,旧记录 superseded_by 被设置的比例 版本链完整率 superseded_by != null 时 superseded_at 也非空的比例 置信度合规率 source_type=explicit 时 confidence=1.0 的比例 孤儿向量率 向量索引里有、主库没有的 id 比例 双活跃率是最重要的一个。它一旦 > 0,检索就会返回旧值,而且症状看起来像"排序错",极难定位。这个指标应该做成监控告警,不是事后评估。
L3 检索质量
- 自检索测试(最便宜,先做这个)
用记忆本身生成 query,看能否检回自己。
for rec in sample(memories, 200): query = generate_query_from(rec) # 用 quote 或 key+value 生成问法 results = retrieve(query, top_k=10) hit = rec["id"] in [r["id"] for r in results]指标 含义 健康值 Recall@1 自己排第一的比例 > 70% Recall@10 自己进前十的比例 > 95% Recall@10 < 95%说明索引构造有问题(embedding 文本太短、key 没换空格、payload 过滤错),不用往下测了。
- 端到端检索评测
需要标注"这个问题应该检索到哪几条记忆"。
指标 含义 Recall@k 应检索到的记忆里,实际返回的比例 Precision@k 返回结果里,相关的比例 MRR 正确结果排名的倒数均值 空结果率 返回空的比例(高 = 改写过度收窄或索引缺失) 假阳性率 返回了不相关记忆的比例(高 = 改写过度扩展) 空结果率和假阳性率要一起看。只看 Recall 会掩盖问题:把 top_k 调到 100,Recall 一定高,但假阳性也高,注入上下文后反而干扰模型。
- 改写增益(必测)
指标 = 检索质量(改写后) - 检索质量(改写前) 增益 ≈ 0 → 改写无效,白付成本,应下线 增益 < 0 → 改写有害,立即下线 增益 > 0 → 改写有价值,再看具体哪类 query 受益这个指标最容易被跳过,但它是唯一能证明改写该不该存在的指标。
- 自检索测试(最便宜,先做这个)
L4 端到端效果
- 对照实验(唯一可信的方法)
A 组:有记忆系统 B 组:无记忆系统(或记忆置空) 同一批问题,比较回答质量必须做对照。只测"有记忆时答对率 85%"没有意义——可能无记忆时也有 83%,记忆只贡献了 2 个点,不值得这套复杂度。
- 分场景看,不要只看总分
场景 记忆应有的贡献 跨会话偏好(“还是用简洁格式”) 高,无记忆必失败 项目配置(“部署在哪台机器”) 高,无记忆必失败 单轮事实问答 零,记忆不该有影响 需要推理的问题 低,记忆只提供素材 如果"单轮事实问答"场景下记忆系统让效果变差了,说明注入的记忆在干扰模型——这是很常见的隐性损失,总分看不出来。
- 成本也要算进去
指标 说明 每次请求额外 token 注入的记忆占多少上下文 额外延迟 检索 + 改写耗时 存储增长速率 每天新增多少条,其中多少是垃圾 记忆系统的隐性成本是"库越来越脏"。如果误收率高,半年后检索质量会自然下降,而单次评测看不出来。所以要监控库的构成:活跃记录数、失效记录数、从未被检索到的记录比例。
一个反直觉但重要的指标:从未被检索到的记忆占比
如果库里 60% 的记录从来没被任何 query 命中过,说明:
- 要么抽取过度(记了太多没用的)
- 要么检索覆盖不到(索引或改写有问题)
- 要么这些记忆本来就该在会话内解决,不该进跨会话库
这个指标直接反映记忆系统的信噪比。库越大、这个比例越高,系统越接近"存了一堆用不上的东西"。
评估的最小可行方案
- 双活跃率监控(查库,零成本,防最严重的静默错误)
- 自检索 Recall@10(200 条样本,10 分钟,验证索引可用)
- 改写开关对照实验(改个配置,验证改写是否值得存在)
这三项覆盖了最容易出问题、且最难通过"看回答"发现的三类故障。端到端人工评测放最后做,因为它最贵、最不可归因,而且容易被"模型本身很强"掩盖。
3、记忆和 RAG 的边界在哪?
技术实现上记忆是 RAG 的特例,但语义上它们是两套不同的系统——RAG 管"外部知识怎么找",记忆管"用户状态怎么维护"。边界不在向量库,在谁产生数据、谁决定写入、失效意味着什么。混用基础设施可以,混用语义模型不行。
3.1、表面看它们几乎一样
| 环节 | RAG | 记忆 |
|---|---|---|
| 切分 | 文档切 chunk | 对话切记忆条目 |
| 索引 | embedding + 向量库 | embedding + 向量库 |
| 检索 | query → top_k | query → top_k |
| 注入 | 拼进 prompt | 拼进 prompt |
所以很多人认为"记忆就是 RAG 的一个特例"。这个看法在实现层成立,但在语义层会误导设计——因为两者的关键差异恰恰在那些"看起来一样"的环节里。
3.2、三个真正的边界
边界 1:数据来源——谁产生的
- RAG:外部语料。文档、网页、知识库。用户不产生它,只是查询它。
- 记忆:用户自己说的话。用户既是生产者也是消费者。
这条差异的后果很实际:RAG 的语料是只读的,记忆是可写的。可写意味着要处理覆盖、冲突、版本、失效——这些 RAG 完全不需要考虑。
边界 2:写入时机——谁决定存什么
- RAG:离线批量入库。存什么由语料本身决定,系统不做筛选。
- 记忆:在线增量写入。存什么由抽取策略决定,系统必须主动判断"这条值不值得记"。
这条差异导致两者的错误模式完全不同:
| 错误模式 | RAG | 记忆 |
|---|---|---|
| 主要风险 | 检索不到(召回不足 | 记错了(污染) |
| 错误可逆性 | 高(补文档即可) | 低(错误记忆会持续影响后续所有对话) |
| 需要人工介入 | 少 | 多(要清理、要纠正) |
记忆系统的核心难题是"写什么",RAG 的核心难题是"怎么找"。 这是两者最本质的分工差异。
边界 3:失效语义——什么时候该消失
- RAG:文档过期了,重新入库覆盖。失效是内容问题。
- 记忆:信息被新信息取代,旧值要标记 superseded_by。失效是状态问题。
这条差异最容易被忽略,但它是记忆系统复杂度的主要来源。
RAG 里"同一份文档的两个版本"是运维问题;记忆里"同一事实的两个值"是语义问题——必须判断哪个是当前值、哪个是历史、变更发生在什么时候。
RAG和记忆对照表:
| 维度 | RAG | 记忆 |
|---|---|---|
| 数据来源 | 外部文档 | 用户对话 |
| 写入方式 | 离线批量 | 在线增量 |
| 写入决策 | 语料决定 | 抽取策略决定 |
| 可写性 | 只读 | 可写 |
| 核心难题 | 召回 | 写什么 + 覆盖 |
| 失效语义 | 内容过期 | 状态被取代 |
| 主要风险 | 检索不到 | 记错污染 |
| 错误可逆性 | 高 | 低 |
| 需要版本化 | 文档版本 | 事实版本(superseded_by) |
| 评估重点 | Recall / Precision | 抽取召回 + 双活跃率 |