
给 Agent 加记忆以后,我遇到过一种很难第一眼察觉的错:它能找到与问题高度相关的记录,回答也有出处,偏偏出处是旧的。
例如记忆里先写“项目部署在 8080”,后来又写“端口改为 9090”。用户问当前端口时,词面检索很可能把两条都召回。若旧记录写得更完整,或在排序中占了先手,模型会认真地根据过期事实作答。
问题不在“没检索到”,恰恰是检索到了太多版本。只调大 top-k 往往会让冲突更明显。
为了把问题缩小,我做了一组确定性实验。数据集中有 24 条记忆,其中 12 条是仍然有效的新值,另外 12 条已被后续记录替代。每组新旧记录用 supersedes 明确关联。查询也是 24 条,覆盖端口、模型、分支、存储路径、超时等不同事实,并在实验前冻结。
基线不是 LLM,也没有向量模型。它只计算中文字符、汉字二元组与英文 token 的重叠分数,分数相同时让更早的记忆获胜。这个故意简单的词面代理适合观察版本冲突,但不能代表生产检索器的真实能力。
第二种策略只多一步:排序以前先收集所有 supersedes 指向的旧记录,把这些旧版本从候选集中移除,再运行完全相同的词面打分。

我最初做过一轮失败的预实验:旧记忆普遍写得比新记忆长,因而天然占到更多词面重叠。结果看起来很好看,却把“时效性”与“文本长度偏差”混在一起。那组数据没有进入最终结果,重新平衡长短后才冻结现在的集合。
在 24 条查询上,纯词面基线答对 7 条,答错 17 条。17 次错误全部选中了已被替代的旧记录,可以明确记为旧值误召回。
先消解新旧关系以后,答对 20 条,答错 4 条。由于旧记录已经退出候选集,旧值误召回为 0。剩下 4 次是一般检索错误:活跃记忆之间仍有词面相近的不同事实,简单重叠分数挑错了对象。

这个区别必须写清。若把 20/24 说成“时效性策略准确率 83.3%,其余仍是旧记忆问题”,就会掩盖后面真正要改的排序器。实验说明显式版本关系能消掉已知冲突,并不说明字符重叠已经足以承担记忆检索。
结果也不证明所有系统都该使用 supersedes。这里的关系是人工整理并在检索前已知,现实数据常常只会出现一条新的自然语言陈述,没有人替它标出覆盖了哪一条。怎样识别更新本身,仍是更困难的问题。
一个直觉做法是给最近记录更高权重。它确实能减少某些过期回答,也容易误伤长期事实。昨天写的临时偏好不一定比三个月前的账号归属更可靠;新的讨论可能只是在引用旧配置,并未宣布配置改变。
时间更适合当信号,不能单独充当裁判。我现在把记忆更新拆成几个字段:事实作用域、创建时间、有效时间、来源、置信度,以及可选的 supersedes。只有能确认“这条记录替换那条记录”时才建立覆盖关系。无法确认的冲突会同时保留,并在生成答案前要求二次判断或向用户澄清。
删除旧记录也未必合适。历史值对排障很有用:问“上周为什么连不上”时,8080 可能正是正确答案。更稳妥的做法是保留记录,但把“当前检索”和“历史追溯”分成不同视图。当前视图默认排除已失效项,历史查询可以按时间窗口重新纳入。
我逐条看了余下 4 次错误。它们不是随机波动,因为整个实验没有随机模型;错误都来自两个活跃事实共享太多词面,例如“模型请求超时”和“工具执行超时”,查询里又缺少足够限定。版本过滤对此无能为力。
下一步应该改查询与记忆的结构,而不是继续加大时间衰减。可选方向包括给作用域和事实类型单独加权、使用 embedding 做候选召回、再让重排器判断字段对应关系。无论换哪种方法,这 24 条冻结查询都可以留下当回归集,防止修好语义相似度后又把旧值放回来。
生产里我还会记录最终答案引用了哪条 memory id、该记录是否覆盖别人、是否已经被别人覆盖。没有这条链路,只看回答文本很难区分三种失败:根本没召回、召回后排错、生成阶段无视了正确记忆。
上线时还要给覆盖关系留审计入口。自动抽取若把两条并存偏好误判为新旧版本,旧记录会从当前视图里悄悄消失,表现得比普通排错更隐蔽。我倾向于让高置信度的结构化更新自动生效,低置信度关系只做候选,并把建立关系的时间、操作者和依据一起保存。这样回答异常时,至少能追到某条记忆为何退出候选集,也能单独撤销错误覆盖,而不必重建整套索引。
这次实验的源码、24 条记忆、24 条查询与逐题预测都放在 evidence/agent-memory-recency-experiment/。它是一组可复现的词面代理,不是 LLM 或 embedding 基准。能得到的结论也很窄:当系统已经知道明确的新旧关系时,应先消解关系,再比较相关性。
记忆系统最容易追求的是“记得更多”。真正让回答可信的,往往是它也知道哪些话已经不再代表现在。

文章问答
她会先读这篇文章,再安静地回答你的问题。可以继续追问。
加载中…
登录后参与讨论