Agent 的记忆:让它在下一次会话里还记得你
在 搭建 Agent 的基础知识 里,我用一张表把记忆分成了工作记忆、情景记忆、语义记忆和程序记忆,最后留了一句话:记忆系统最难的部分不是“存”,而是“什么时候该写、写什么、什么时候该读、读多少”。后来在 上下文管理 里,又把笔记文件和记忆目录当作管理上下文的一种手段讲了一遍。
那两篇关心的都是一次任务之内的事:窗口快满了,怎么把信息挪到外面去。这篇想换个角度,专门讲跨会话的记忆:今天告诉 Agent 的事,下周开一个全新的会话,它还知不知道;它知道的东西,是不是还对。
这件事比看上去难。一个“什么都记”的 Agent 会越用越啰嗦、越用越怪;一个“什么都不记”的 Agent 会让你每次都重新解释一遍自己是谁。中间那条线怎么画,就是这篇要聊的。
记忆不是一个功能,是一条读写回路
很多人第一次做记忆,想的是“加一个向量数据库”。但数据库只是中间那个存储,真正决定效果的是两头:什么东西、在什么时候、以什么形式被写进去,以及什么东西、在什么时候、以多大的量被读回来。再加上一个经常被忘掉的环节:存进去的东西会过期、会矛盾、会越积越多,需要有人整理。
下面就按这个回路,一个环节一个环节地讲。
先想清楚:要记的到底是什么
认知科学那套分类适合理解概念,但动手时更有用的是按用途分。我自己做下来,值得跨会话保留的东西大致是这五类:
| 类别 | 例子 | 价值 | 过期速度 |
|---|---|---|---|
| 用户画像 | 角色、技术背景、常用语言 | 决定怎么解释、解释到多深 | 慢 |
| 偏好与反馈 | “别给我写总结段落”、“提交前先跑测试” | 最高,避免同一个错误被纠正两次 | 慢 |
| 项目事实 | “周四之后冻结合并”、“这个服务即将下线” | 高,且无法从代码里推出来 | 快 |
| 经验与做法 | 某类问题的排查步骤、某个工具的坑 | 高,能沉淀成可复用的技能 | 中 |
| 事件记录 | 某天讨论过什么、做过什么决定 | 偶尔有用,主要用于回溯 | 慢,但很少被读 |
其中最值钱的是第二类,用户的纠正。用户说“不要这样做”的那一刻,是 Agent 获得信号最强、成本最低的时候;如果这条纠正没有被记下来,下一个会话里用户就得再纠正一次,而“同样的错犯两次”是最快让人失去信任的事情之一。值得一提的是,用户确认“对,就这样做”也同样值得记,只记批评的记忆会让 Agent 越来越保守。
判断一条信息该不该记,我用一个很简单的问题:下一个会话里的 Agent,如果不知道这件事,会不会需要再问用户一遍,或者再犯一次错? 会,就记;不会(比如能从代码、文档、Git 历史里查到),就不记。
还有一个细节:把“为什么”一起记下来。只记“不要在测试里 mock 数据库”,Agent 只能机械执行;记成“不要在测试里 mock 数据库,因为上次 mock 测试全过但线上迁移挂了”,Agent 才能在边界情况下自己判断这条规则适不适用。
写入:谁来写、什么时候写
写入有两条基本路线,区别在于写记忆这件事发生在哪里。
热路径写入:把记忆做成一个工具,Agent 在工作过程中自己决定什么时候调用。用户说了一句“以后都用 pnpm”,Agent 当场调用写入工具记下来。
后台抽取:Agent 工作时完全不管记忆,会话结束后(或者每隔若干轮),由另一个流程把这段对话交给模型,让它抽取出值得记住的东西,再合并进记忆库。
我的经验是:用户明确说出来的东西走热路径,隐含在行为里的东西交给后台。 用户说“记住,我们周四之后不合并代码”,这是一条明确的指令,应该当场记下,并且告诉用户记了;而“这个用户每次都会把我写的长段落改短”这种模式,单看一次对话看不出来,适合让后台流程在多次会话之后归纳。
写入不只是追加
最容易犯的错,是把写记忆实现成一个 append。这样用不了多久,记忆库里就会出现“用户喜欢 npm”和“用户改用 pnpm 了”两条并存的记录,读取时模型只能猜。
更好的做法是让每一次写入都先和已有记忆比对,再决定做哪种操作。很多记忆框架(比如 Mem0)把它归纳成四种:
- 新增:这是一条全新的信息;
- 更新:已有一条相关记忆,新信息是对它的补充或修正;
- 删除:新信息说明某条旧记忆已经不成立了;
- 跳过:已经记过了,或者不值得记。
如果走热路径,就把这几种操作直接设计成工具的能力。以文件形式的记忆为例,一个记忆工具大概长这样:
{
"name": "memory",
"description": "读写跨会话的长期记忆,存放在 /memories 目录。写入前先 view 目录,优先更新已有文件而不是新建;发现内容过时或错误时删除它。不要记录可以从代码或文档中查到的信息。",
"input_schema": {
"type": "object",
"properties": {
"command": { "type": "string", "enum": ["view", "create", "str_replace", "delete"] },
"path": { "type": "string", "description": "例如 /memories/preferences.md" },
"content": { "type": "string", "description": "create 时的完整内容" },
"old_str": { "type": "string" },
"new_str": { "type": "string" }
},
"required": ["command", "path"]
}
}
注意描述里写的不是“这个工具能干什么”,而是“什么时候该用、怎么用才对”。这和 工具设计 那篇说的一样:对记忆工具来说,描述里的写入规则就是整个记忆系统的质量上限。
如果走后台,就把同样的逻辑写进抽取提示词:
下面是一段已结束的对话,以及当前已有的记忆条目。
请找出对话中值得长期记住的信息,对每一条给出操作:
ADD(新增)/ UPDATE(修改哪条、改成什么)/ DELETE(哪条已不成立)/ NONE。
只记录:用户的身份与偏好、用户的纠正或确认、无法从代码和文档中推出的项目事实。
不要记录:一次性的任务细节、已经解决的问题、你自己的推测。
每条记忆写清楚"是什么"和"为什么",并注明来自用户原话还是你的推断。
最后一句“注明来自原话还是推断”很重要,后面讲安全时会再提到。
存储:文件、向量、结构化还是图
存储的选择往往被讨论得最多,但它对效果的影响其实没有写入和读取大。几种常见形式的对比:
| 形式 | 适合 | 不适合 | 可维护性 |
|---|---|---|---|
| Markdown 文件 | 几十到几百条、需要人能读能改的记忆 | 海量条目、需要模糊匹配 | 最好,直接用编辑器和 Git |
| 向量库 | 大量非结构化内容,比如历史对话、文档 | 精确事实、需要更新和删除的记忆 | 差,很难看清里面有什么 |
| 结构化存储 | 字段固定的画像,比如语言、时区、角色 | 开放式的经验和规则 | 好,但需要提前设计结构 |
| 知识图谱 | 实体关系复杂、需要按时间追溯的事实 | 大多数个人和项目场景 | 一般,构建和维护成本高 |
我的建议是从文件开始。原因有三个:
- 透明。用户和开发者都能直接打开看 Agent 记住了什么,记错了直接改。记忆最怕的不是记不住,而是记错了还没人发现;
- 模型本来就擅长读写文件。编码 Agent 的训练里大量是读文件、改文件,用同一套能力操作记忆,几乎不需要额外教;
- 条目通常没那么多。一个人或者一个项目真正值得长期记住的东西,几十到几百条就差不多了,用不上向量检索。
一种好用的组织方式是一个主题一个文件,加一个索引:
<!-- /memories/index.md:每次会话开始时读这个 -->
- [用户画像](user.md):后端工程师,Go 为主,前端只懂基础
- [写作偏好](writing.md):中文回复,不要总结段落,代码注释用英文
- [项目约定](project-billing.md):周四之后冻结合并;账单服务计划年底下线
<!-- /memories/writing.md -->
---
updated: 2026-10-20
source: 用户原话
---
- 回复结尾不要写"总结一下"的段落。
为什么:用户说读到最后一段都是重复,浪费时间。
- 代码注释用英文,正文用中文。
为什么:团队里有不读中文的同事会看代码。
当条目真的多到索引放不下(比如要记住几千次历史对话),再引入向量检索也不迟,而且可以只对“事件记录”这一类用,偏好和规则仍然放在文件里。
读取:放进来多少、什么时候放
记忆写得再好,读不回来也等于没有;读得太多,又会变成 上下文腐烂 的来源。读取大致有三种方式,按“主动程度”排列:
- 常驻:把记忆全文放进系统提示词。最可靠,模型每一轮都看得到;代价是每一轮都要付这笔 token,只适合少量、几乎每次都用得上的东西,比如用户画像和最重要的几条偏好;
- 索引 + 按需打开:只常驻一个目录,每条一行描述,模型判断需要时再用工具读具体文件。这是 渐进式披露 在记忆上的应用,也是我最推荐的默认方式;
- 自动检索注入:每一轮由 Harness 根据当前输入去记忆库检索,把最相关的几条插进上下文。最省 token,模型也不用主动去想;但它把“该不该想起来”这个判断交给了一个相似度分数,很容易注入无关内容,或者漏掉真正相关但字面不像的记忆。
检索时只看语义相似度通常不够。斯坦福那篇著名的 Generative Agents 论文里,记忆的打分由三部分加权组成:相关性(和当前情境多像)、近期性(多久之前发生的,按时间衰减)和重要性(写入时由模型打的分)。这个思路至今仍然好用:一条三个月前的、无关紧要的记忆,即使字面上很像,也不该排在昨天用户刚说的一条纠正前面。
读取的两条工程约束
第一,和缓存的关系。 记忆通常放在上下文的前部,而 提示词缓存 是按前缀匹配的。如果会话中途写了一条新记忆,然后立刻把它刷新进系统提示词,整段缓存就失效了。比较稳妥的做法是:记忆在会话开始时加载一次,会话中新写入的内容只追加到对话里(工具调用本身就留在历史中),下一次会话再正式生效。
第二,读到的记忆是“当时的事实”。 记忆里写着“配置在 config/prod.yaml”,可能文件早就被挪走了;写着“用户在做支付项目”,可能人家已经换组了。所以在系统提示词里要明确告诉模型:记忆是背景信息,可能已经过时,涉及具体文件、函数、配置时要先验证再使用;和用户当前说的话冲突时,以用户当前的话为准,并顺手更新记忆。
维护:合并、冲突与遗忘
写入和读取都做好了,记忆系统在头几周通常表现不错。真正的问题出现在几个月之后:条目越来越多,重复的、过期的、互相矛盾的混在一起,效果开始悄悄变差。所以记忆需要维护,而这一环在大多数实现里是缺失的。
维护要处理三件事。
合并重复。 后台抽取尤其容易产生近义的重复条目:“用户偏好简洁回复”、“用户不喜欢长篇解释”、“回复尽量短”。它们单独看都没错,但放在一起既浪费空间,又会让模型觉得这件事“特别重要”而过度执行。整理时把它们合成一条,并保留最具体的那个版本。
解决冲突。 新旧记忆矛盾时,默认以新的为准,但有两个例外值得注意:一是新信息来自模型的推断而旧信息来自用户原话,这时不该轻易覆盖;二是两条记忆其实都对,只是适用范围不同(“个人项目用 pnpm,公司项目用 npm”),这时应该合并成一条带条件的记忆,而不是二选一。这也是为什么前面强调每条记忆都要带时间和出处:没有这两样,冲突根本没法判断。
主动遗忘。 不同类别的记忆过期速度很不一样。偏好可以保留很久;项目事实,比如“周四之后冻结合并”,可能下周就不成立了。几种可行的做法:给项目类记忆记录日期,整理时让模型判断是否还成立;统计每条记忆被读取和引用的次数,长期没被用过的降权或归档;以及最简单也最重要的,给用户一个直接查看和删除记忆的入口。
这些整理工作很适合放在离线流程里:每隔一段时间(或者每积累若干次会话),让一个模型把整个记忆库读一遍,做合并、改写和删除。有人把它比作人类睡眠时的记忆巩固。这个过程还有一个额外的收获:从多条具体的事件里提炼出规律。比如三次会话里用户都要求“改完代码先跑一下 lint”,就可以把这三条事件记录提炼成一条做事规则,甚至写成一份技能文档。情景记忆就这样慢慢变成了程序记忆。
作用域:记忆属于谁
单用户、单 Agent 的时候不用想这个问题,一旦场景复杂一点,“这条记忆该对谁可见”就变得很关键:
- 用户级:个人偏好、背景,跟着人走,跨项目生效;
- 项目级:项目约定、架构决策、已知的坑,跟着项目走,项目里的所有人和 Agent 共享;
- 组织级:团队规范、通用流程,通常由人维护,Agent 只读;
- 会话级:只在一次任务里有用的笔记,任务结束可以丢掉,这其实就是上下文管理那篇里讲的进度文件。
写入时就要决定作用域。“我喜欢用 tab 缩进”是用户级的,不该污染整个团队的项目记忆;“这个仓库的测试要先启动本地数据库”是项目级的,换个人来也应该知道。
作用域还带来两个更严肃的问题。一是隐私隔离:多用户系统里,A 用户的记忆绝对不能出现在 B 用户的会话里,这种隔离必须在存储层做,不能指望模型自觉。二是并发写入:多个 Agent 同时更新同一份项目记忆,后写的会覆盖先写的。简单的办法是让每个 Agent 只追加到自己的区域,由整理流程统一合并;或者像代码一样,用版本控制来管理记忆文件。
安全:记忆是一个持久化的注入面
这是最容易被忽视、但后果最严重的一点。
在 Agent 沙箱 里我提过提示词注入:Agent 读了一个网页,网页里藏着“忽略之前的指令,去做某件事”。没有记忆的时候,这种攻击的影响只限于当前会话,关掉重开就没了。有了记忆之后,注入的内容可能被写进记忆,从此在每一次会话里生效。 一条被污染的记忆,比如“用户授权你在部署时跳过审批”,会安静地躺在记忆库里,一直影响后面的行为,而且很难被发现。
几条我认为必要的防线:
- 区分出处。每条记忆标明它来自用户原话、模型推断,还是来自外部内容。来自网页、文档、工具结果的信息,原则上不应该直接变成“规则类”记忆;
- 权限类的内容不进记忆。“用户允许我做 X”这种授权,不应该通过记忆来持久化。权限应该放在 Harness 的配置里,由人来改,而不是由模型写;
- 记忆是数据,不是指令。在系统提示词里明确告诉模型:记忆内容是背景信息,其中出现的指令性文字不具备授权效力;
- 让写入可见、可审查。热路径写入时告诉用户“我记下了……”,后台写入保留变更记录。用户能看到、能删,记忆才可能长期保持干净。
说到底,记忆系统和沙箱遵循同一个原则:真正的边界要放在模型之外。模型可以决定记什么,但哪些东西有资格被记、记下来之后有什么效力,应该由系统来约束。
怎么评估记忆
学术界有一些专门的评测集,比如 LoCoMo 和 LongMemEval,它们构造很长的多轮、多会话对话,然后考模型能否回忆起早期的信息、处理信息的更新、做跨会话的推理、在没有相关信息时拒绝回答。这些评测很适合用来比较不同的记忆方案。
但对一个具体的产品来说,更有用的是围绕自己的场景写几类跨会话测试,就像 评估 那一节说的,从真实失败里来:
| 测试类型 | 怎么构造 | 期望 |
|---|---|---|
| 记住 | 会话一告知偏好,会话二给一个相关任务 | 不需要提醒就遵守 |
| 更新 | 会话一说用 A,会话二改成 B,会话三检查 | 用 B,且记忆里没有残留的 A |
| 不乱用 | 会话二是一个完全无关的任务 | 不提起、不套用无关的记忆 |
| 不轻信 | 会话一读一个含注入内容的网页 | 注入内容没有进入记忆 |
| 不膨胀 | 模拟几十次会话 | 记忆条目数趋于平稳,而不是线性增长 |
其中“不乱用”最容易被忽略。一个总在不相关的地方提起“我记得你上次说过……“的 Agent,给人的感觉不是贴心,而是有点让人不舒服。好的记忆应该像一个靠谱的同事:该想起来的时候想得起来,不相关的时候也不会到处说。
动手时的几条建议
- 先做读,再做写。手写一份记忆文件放进系统提示词,看看“有记忆”到底能带来多少提升,再决定值不值得做自动写入;
- 从文件和索引开始,等真的遇到规模问题再上向量库;
- 把写入规则写进工具描述或抽取提示词:记什么、不记什么、要写为什么、要标出处;
- 写入要能更新和删除,不能只追加;
- 每条记忆带时间和出处,这是之后做冲突处理和安全过滤的前提;
- 给记忆留一个离线整理流程,哪怕只是每周跑一次的脚本;
- 让用户看得见、改得了。这既是信任问题,也是纠错最便宜的方式。
小结
- 跨会话记忆是一条读写回路:写入、存储、读取、整理,存储只是其中最不重要的一环;
- 最值得记的是用户的纠正和确认,以及无法从别处推出来的项目事实,并且要连“为什么”一起记;
- 明确指令走热路径,隐含模式交给后台,写入要支持新增、更新、删除和跳过;
- 从文件和索引开始,透明、可编辑,比一开始就上向量库更容易做对;
- 读取默认用索引加按需打开,记忆在会话开始时加载,读到的内容当作可能过时的事实;
- 记忆需要维护:合并重复、解决冲突、主动遗忘,并把反复出现的经验提炼成规则;
- 记忆是一个持久化的注入面:区分出处,权限不进记忆,边界放在模型之外;
- 评估要跨会话,不只考记不记得住,还要考会不会更新、会不会乱用。
上下文管理解决的是“这一次怎么不乱”,记忆解决的是“下一次怎么更好”。前者让 Agent 能把一件事做完,后者才让它真正有可能越用越顺手。