Agent 记忆架构:四层模型与冷热分离
我们第一版 Agent 记忆系统是怎么死的,值得先交代清楚,因为它的死法极具代表性。
设计思路很直觉:给 Agent 配一个向量库,会话里的重要内容都存进去,下次遇到相似问题检索出来注入上下文。「重要内容」的判断交给模型自己——它觉得值得记就记。上线三个月,攒了两千三百多条记忆。然后我抽查了一次检索质量,结果相当难看:随机拿五十个真实任务去查,返回的记忆里真正有用的不到一成。剩下的是什么?「用户正在修复 login 模块的 bug」(三个月前的任务状态)、「计划明天优化数据库索引」(早就做完了)、「用户似乎喜欢简洁的代码风格」(每个用户都「喜欢简洁」)……
更糟的是这些垃圾不是无害的。它们被注入上下文后,模型会当真。有一次 Agent 坚持要先「继续上次未完成的索引优化」——那个「上次」是两个月前的一条过期记忆。
第一版的教训一句话:把记忆当日记写,记忆系统就会变成垃圾场,而且是会反噬的垃圾场。
第二版:先想清楚「记忆」是几种东西
推倒重来时,我们先停下来分类。「记忆」这个词太笼统了——把四种性质完全不同的东西混在一个向量库里,是第一版所有问题的根源。借用认知科学的分法,Agent 的记忆其实是四层:
| 层 | 位置 | 存什么 | 例子 |
|---|---|---|---|
| Working | 上下文窗口 | 当前会话 | 正在讨论的这个 bug |
| Procedural | 技能包 | 怎么做事 | 「排查 OOM 的标准步骤」 |
| Episodic | 会话历史 | 发生过什么 | 上周三那次排查的完整记录 |
| Semantic | 一份小文件 | 什么是真的 | 「测试库只读,用 mock」 |
分层的意义在于每层的写入标准、存储介质、访问方式都不同,混在一起就全乱了。
工作记忆就是窗口,上下文工程那篇讲过,不重复。程序性记忆是「怎么做」——我们把高频操作流程(发布、排查、数据订正)写成技能包,按需加载,这类知识的特点是稳定、可复用、和具体会话无关。情景记忆是原始会话历史,我们现在直接存 JSONL 进 SQLite 加全文索引,原样存、不加工——它的价值恰恰在于原始,需要的时候全文搜。语义记忆是提炼过的事实和约定,一份人工可审的 Markdown 小文件,常驻上下文。
第一版的两千三百条记忆,按这个分类回头看:九成是本该阅后即焚的任务状态(哪层都不该进),一部分是该进技能包的流程知识,真正值得进语义记忆的,我们后来人工筛了一遍——不到二十条。
写入纪律:白名单制
第二版最重要的改动是把写入从「模型觉得重要就记」改成白名单制。语义记忆只收四类:
- 长期偏好——「提交信息用中文」「图表要交付 PNG 而不是 mermaid 源码」
- 环境事实——「CI 里跑不了 docker」「测试库是只读副本」
- 重复出现过的错误修正——同一个坑第二次踩,说明值得记
- 明确沉淀的约定——评审拍板的决策
黑名单同样写死:任务进度不记(进任务系统)、TODO 不记、会话流水账不记、单次出现的偏好不记(可能是噪声,重复出现才升级)。
执行层面有个关键设计:写入语义记忆需要经过「精选」环节——每次会话结束后,从当天的原始记录里提炼,而不是会话过程中随手写。这个延迟很重要:会话当时觉得重要的东西,隔天再看一半都不重要。OpenClaw 社区的实践也是这个路子,他们把这份精选文件视为整个系统最重要的长期资产,每次 session 之后从 daily notes 里提炼进去。我们抄了这个作业,效果很好。
现在这份语义记忆文件维持在一百行以内,我每两周人工审一遍,删掉过期的。是的,人工——这个规模下人工审查的成本几乎为零,而它换来的是「常驻上下文的每一行都值得信任」。
冷热分离
四层怎么落到存储上,一个模式就够了:冷热分离。
热的:语义记忆那份小文件,常驻 prompt。它必须小(不挤占指令预算)而且稳定(不能频繁变动)——稳定这点是有真金白银的理由的:它作为固定前缀能吃到推理服务的前缀缓存,这个话题下篇单独写。
冷的:会话历史和技能包,放外部存储按需检索。这里有个实用技巧是检索结果不直接入主窗口,先让一个便宜的小模型把命中内容压成摘要再返回——不然一次检索拉回五千 token 原文,窗口又爆了。
判断冷热就两个变量:使用频率 × 内容稳定性。高频且稳定的进热区,其他一律冷区。规则简单,难在抵抗诱惑——什么东西都想塞进常驻上下文「以防万一」,是每个维护者都要反复戒断的毛病。
情景记忆的实现细节
冷区里工作量最大的是情景记忆,把我们的实现摊开讲讲,都是朴素技术。
存储就是 JSONL 进 SQLite:一行一条消息,字段是会话 ID、时间戳、角色、内容、工具调用摘要。没有用任何专门的「记忆数据库」产品——评估过两个,功能都很炫,但我们的查询模式就两种(关键词找会话、按时间段捞记录),SQLite 加 FTS5 全文索引足够,还省掉一个运维依赖。数据量也吓不着它:一年的会话记录不到 2GB。
检索链路上有个花了点心思的环节:命中结果不直接返回主 Agent。一次全文检索命中三五个历史会话片段,原文可能上万 token,直接注入等于把主窗口炸了。我们的做法是过一道便宜的小模型:输入是命中片段加当前任务描述,输出是「与当前任务相关的要点摘要,不超过三百字,注明出处会话 ID」。主 Agent 觉得摘要不够用,可以拿会话 ID 去调原文——但实际统计下来,九成的场景摘要就够了。这一道中转的成本几乎可以忽略(小模型便宜),换来的是主窗口的清爽。
还有一条经验是检索触发时机。第一版是每个任务开始时自动检索一次「相关历史」,听起来很智能,实际效果差——多数任务根本不需要历史,注入的「相关记忆」反而是干扰。第二版改成工具化:给 Agent 一个 search_history 工具加一行描述,它自己判断什么时候查。调用频率立刻降到原来的两成,而真正调用的那些,几乎次次都是有效检索。又一次验证了那个老道理:把选择权交给模型,好过替它做选择。
给事实加失效日期
语义记忆运营半年后,我们补了一个字段:失效线索。
起因是一条记忆造成的误导:「CI 环境没有 docker,相关测试跳过」。这条记录写下时是对的,四个月后 CI 升级支持了 docker,但记忆还在——Agent 于是持续跳过那批测试,直到有人发现覆盖率不对劲。过期的记忆比没有记忆更糟,因为它带着「已确认事实」的权威性。
现在每条环境类事实写入时都要求附一个失效线索,格式很松,比如「若 CI 配置文件出现 docker 字样则此条作废」或者干脆一个复查日期。每两周的人工审查里,带失效线索的条目会被优先核对。不优雅,但把「记忆腐烂」从隐性问题变成了显性待办——工程上这就是巨大的进步。顺便说,这个字段的设计是那次让模型分析检索日志时它提出来的,见下文。
关于向量库和知识图谱
有人会问:第二版还用向量库吗?用,但只用在情景记忆的语义检索上,而且和全文检索并联——实际用下来,全文检索的命中率比向量检索高,因为查历史会话的场景大多带着明确关键词(报错信息、服务名、函数名)。向量检索负责兜底那些「我记得讨论过类似的事但想不起词」的模糊查询。
至于知识图谱,我们评估过,暂时没上。图能回答向量答不了的多跳问题(「这个模块和那次故障隔了几层依赖」),但代价是一整条抽取-建图-查询的管线,每一环都可能出错:抽取错误污染图、schema 设计不好图不可查、自然语言转图查询又是一层风险。我的判断是:先确认你真的有多跳关系查询的需求,再考虑图。多数团队的记忆需求,四层模型加冷热分离就覆盖了,图是给特定问题准备的重型武器,不是标配。
一个变化的记录
最后记一笔最近的观察。第二版记忆系统上线后做过一次调优,方式是把三个月的检索日志丢给模型,让它分析命中和落空的模式、提出改进方案。它给出的建议里有两条我们采纳了(调整精选环节的提示词、给环境事实加失效日期字段),效果不错。
罗福莉在一个访谈里说,所谓 Agent 的自学习,不是模型凭空变强,而是模型和 Agent 架构一起往前走——模型开始参与重写自己的记忆系统。我们这次小小的实践算是个注脚。系统参与自己的进化,这在传统软件工程里没有对应物,我还没完全想清楚它的边界在哪,先记下来。