我如何给 Agent 写记忆:文件、索引与失效治理
前言
我印象最深的一条 Agent 记忆,跟技术一点关系没有。有次让 Agent 打开一个 pptx,它用 Keynote 打的,弹了一串确认窗,把正在干别的的用户烦着了,甩来一句:以后用 WPS,别用 Keynote。就这么一句话,后来被写成了一条记忆文件。到现在几百次会话过去,这件事再没发生过第二次——不是因为我记性好,是因为它自己会看记忆。这篇讲我这套记忆是怎么组织的,文件怎么写、索引怎么维护、以及记忆过期了怎么处理。
一、先说没有记忆是什么体验
同一个项目开了几十次会话之后,你会在每个新会话的开头重复同样的话:这个项目的背景是这样的,我要的代码风格是这样的,上次那个接口有个坑你别踩。讲一遍两遍无所谓,讲到第十遍的时候我开始怀疑自己在跟失忆症患者合作。
更难受的是坑。有一次 Agent 给我改一个配置文件,改完服务起不来——这个坑两个月前踩过一模一样的,当时还排查了半小时。它不记得,我也不可能每次都记得提醒,于是原样又踩了一遍。背景重讲一遍只是浪费时间,坑重踩一遍是真返工。所以我后来给记忆排优先级:坑最优先,偏好其次,背景最后。
二、一个文件一个事实
我的记忆库就是一个目录,里面全是 markdown 文件。唯一的纪律:一个文件只写一件事。
拿开头那条 WPS 记忆举例,它长这样:
--- name: pptx-open-with-wps description: 打开 pptx 用 WPS 不用 Keynote,Keynote 弹窗多且脚本控制坑多 metadata: type: feedback --- 用户明确要求:pptx 用 WPS 打开,不要用 Keynote。 **Why:** Keynote 每次弹窗打扰用户;AppleScript 控制它 边遍历边关文档会报无效索引。 **How to apply:** 用 `open -a wpsoffice <路径>`;实在要用 Keynote 导出图片,导完立刻关掉文档。最开始我不是这么干的,图省事把一堆事写进一个大文件,「项目备忘.md」这种,啥都往里塞。后来发现这种文件就是垃圾场:想更新其中一条,得通读全篇怕改坏别的;想删掉过时的,又不敢确定别的条目还引用不引用。三个月之后它就变成一个没人敢动的文件,等于没有。
拆成一个文件一个事实之后,事情变简单了。要更新就整个文件重写,要废弃就直接删,去重也容易——文件名摆在那,写重了自己都能看出来。
还有个细节:这类「用户纠正过我」的记忆,我会把 Why 写上。只写「不要用 Keynote」,Agent 执行是执行了,但它不知道边界——哪天换了台没装 WPS 的机器,它可能就卡在那儿不会变通。把原因写清楚(弹窗烦人、脚本控制有坑),它自己能判断什么情况下这条规矩该怎么落地。
三、索引才是天天在干活的东西
记忆文件多了以后,问题变成了:新会话开始,Agent 怎么知道该翻哪条?
我维护了一个索引文件,每次会话自动加载进上下文。它长这样:
# Memory Index - [pptx 用 WPS 打开](pptx-open-with-wps.md) — 不用 Keynote,弹窗多脚本坑多 - [发布流程](release-flow.md) — 三条命令、验证端点、回滚位置注意索引里每条只有一行。这是被逼出来的规矩——索引每次会话都全量进上下文,它是固定成本,正文才是按需读取的。有阵子我偷懒把内容摘要直接写进索引,会话还没开始就先背了半页记忆,后来全删了,一行钩子足够,命中了再打开文件看。
写索引行(以及每条记忆的 description)有个门道,我摔过跤才明白:钩子要写「什么时候会用到它」,不是「它讲了什么」。早期我的 description 全是「本文记录了打开 pptx 的注意事项」这种——听起来没毛病,但搜「Keynote 弹窗」根本命中不了,因为场景词没在里面。后来改成「不用 Keynote,弹窗多脚本坑多」,把未来会用到的场景词都塞进去,命中率立刻不一样。
现在写每条记忆我都会问自己一句:三个月后我在什么情况下会需要翻出这条?把那个情况里的关键词写进 description。
四、什么该记,什么不该记
这条我走过弯路,一开始热情高涨什么都记,一个月记了小两百条,然后发现索引比正文还长,每次会话开头先读半本书,找个东西比不记的时候还费劲。烂记忆不比没记忆强,它还占地方。
现在我记的东西就这几类:用户纠正过我的做法(连原因)、踩坑的结论(坑长什么样、怎么绕)、项目的关键决策和理由、环境配置和资源位置。其他的一律不进。
特别说一下不记的里面最容易被犯的一条:从代码里能直接看到的东西,不要抄进记忆。函数签名、目录结构、配置项的当前值——这些代码里都有,你抄进记忆的那一刻就开始过期了,代码一改,记忆就成了毒数据,Agent 下次还拿着旧地图找路。记忆记「为什么」和「坑在哪」,「是什么」让代码自己说。
拿不准记不记的时候,就不记。我的经验是同一个问题第二次冒出来再沉淀也完全来得及,第一次可能是偶然,第二次才是真的会反复出现的事。
五、记忆是写入当天的快照,不是真理
这套体系跑了几个月,我对「记忆」这个词的理解变了:每条记忆都是写入那天的快照。里面的路径、命令、版本号,写的时候是对的,之后每天都在变成错的路上。
我实际遇到过的:记忆里记着某个脚本的位置,过了几周目录整理过一次,那个路径早就不存在了。Agent 召回这条记忆,理直气壮地按旧路径去执行,报错之后还试图在旧路径周围找补。从那以后我立了个规矩:记忆里出现的路径和命令,动手前先验证还在不在。召回出来的记忆是参考资料,不是指令——这一条对用记忆的 Agent 和写记忆的我,都成立。
除了过时,还有两种病。一是重复:同一个事实前后写了两版(口径变了但没删旧的),Agent 同时召回两条,不知道信哪条——发现就合并,别「留着以防万一」。二是膨胀:一个文件从二十行写到三百行,写的时候觉得都是有用的上下文,实际上没人(包括 Agent)读得完,长文件等于死文件,把已经完结的部分挪出去归档,主文件留结论。
清理这事我不用专门找时间做。哪次会话命中了一条记忆,顺手看一眼:还准吗?跟别的冲突吗?太长了吗?三十秒的事。
六、写记忆得长在流程里
最后这点可能比前面所有技巧都重要。我见过不少人的记忆体系是「靠自觉」的:想起来就记一条,忙起来就忘了。这种体系必然荒废,三个月后回头看,记的还是头两周的那些。
我的做法是把它绑死在几个固定时机上:
- 长会话被压缩之前,把结论和当前状态先写进文件——压缩是个不可逆的动作,细节丢了就真丢了;
- 任务中断的暂停点,交接文档先落盘:任务原话、已经确认的事实、已经证伪不要再试的路、当前做到哪了。下次开新会话,把这份文档丢给它,直接从八成的地方接上;
- 被用户纠正的那一刻,当场写记忆,别等「回头整理」——回头整理这件事从来不会发生。
前面说的索引自动加载是另一半:会话一开始,过去的东西自动在场,不用每次想起来去问一句「上次那个怎么弄的来着」。记下来、能召回、用之前验证一下,这个环闭上了,记忆才算真的在干活。
踩坑清单
- 拿召回的记忆当指令直接执行——里面记的路径早没了。记忆是参考资料,执行前验证现状;
- description 写成内容概括——「本文记录了……」这种是召回不到的,写触发场景里的关键词;
- 什么都记——索引比正文还长,找东西比不记还费劲。准入门槛宁高勿低,拿不准就不记;
- 只增不减——过时的、重复的、膨胀的都不处理,最后整个库没人敢动。命中时顺手体检,三十秒的事。
参考资料
- 本系列第五篇:《Agent 上下文管理详解:Prompt Caching、压缩与记忆》—— 记忆分层与压缩的原理篇
- 本系列番外五:《Agent 调试与止损》—— 会话交接文档的模板与用法
- 本系列第八篇:《Claude Code 效率倍增:Skills、Hooks 与定制化详解》—— 记忆与 skill 的分工:记忆存事实,skill 存流程