专栏 Agent 工程

记忆机制:从上下文到持久化

上周跟它说过,这个项目用 Kotlin,不要写 Java。

记忆机制:从上下文到持久化

上周跟它说过,这个项目用 Kotlin,不要写 Java。

今天新开一个会话,它又建议我用 Java。

不是它没记住,是那次对话的上下文已经不在了。

一、上下文不是记忆

前面讲过,所谓”它记得”,是每次把历史重新拼一遍发过去。

这个机制有个硬边界:会话结束,历史就不再被发送。下次开新会话,上下文从零开始,昨天聊过的所有东西都不在里面。

所以严格说,Agent 没有记忆,只有”这一轮发了什么”。要跨会话记住一件事,必须把它从上下文里搬到一个不随会话消失的地方。

那个地方是文件。

二、两层记忆

  工作记忆 长期记忆
载体 上下文数组 磁盘上的 Markdown
生命周期 一次会话 永久,直到被删
写入方式 自动(每轮追加) 主动(模型调用工具)
读取方式 全量重发 注入 + 检索
成本 每轮都付 token 只在注入或检索时付

这两层不是替代关系,是配合关系:长期记忆负责”跨会话不失忆”,工作记忆负责”这一轮想得起来”。

麻烦在于,从长期到工作记忆的那一步,没有自动通道。你得决定什么时候把它捞上来。

三、写入:由模型判断什么值得记

长期记忆的写入口是一个工具:

memory_write(content: "...")

它把内容追加到当天的日志文件里,每条前面带时间戳。文件按日期分,一天一个。

这个设计有两个含义。第一,记忆是按时间组织的,不是按主题。第二,写入是追加的,不覆盖,所以旧记录不会因为新记录而消失。

关键问题是谁来判断”什么值得记”。答案是模型自己。工具描述里给了标准:

  • 用户偏好(用什么语言、什么风格、什么工具)
  • 反复出现的模式
  • 关键事实(项目名、路径、约束)
  • 项目约定
  • 可复用的知识

同时明确了几条不该记的:密码、API Key、令牌,除非用户在被警告后明确同意。

把判断权交给模型,是有代价的。它可能记太多,日志变成流水账;也可能记太少,真正重要的东西没留下。而且它的判断标准来自工具描述,描述写得含糊,记下来的东西就含糊。

四、读取:注入和检索

记忆有两种被用上的方式。

注入:新会话开始时,把全局记忆和最近几天的日志直接拼进系统提示。模型一开口就已经”知道”了这些事,不需要主动去查。

检索:模型觉得需要更多背景时,调用工具按关键词搜索:

memory_get(scope: "all", keywords: "kotlin 项目约定")

搜索是模糊匹配,空格分隔的多个关键词必须同时命中某一行或它的上下文。

这就是一个最小化的检索系统:注入解决”高频常识”,检索解决”低频细节”。

两者的成本结构完全不同。注入的代价是每轮固定支付——写进系统提示的东西,每一次请求都要重发。检索的代价是按需支付——不查就不花钱,查了多一轮工具调用。

所以判断标准很直接:经常用到的才值得注入,偶尔用到的应该去检索。

五、记忆也要算预算

沿着上面的逻辑,记忆不能无限增长。

如果全局记忆写到一万字,每个新会话的系统提示就多一万字。十轮对话下来,这是十万 token 的额外开销,而且每一轮都在为”可能用不上”的信息付费。

所以记忆系统必须有容量意识,只是这个意识通常不写在代码里,而写在设计里:

  • 全局记忆保持短,只放最稳定的偏好和约定;
  • 日常日志按天分文件,注入时只取最近几天;
  • 更早的内容靠检索,不靠注入。

这和前面的上下文预算是同一个问题,只是时间尺度更长:上下文管的是”这一轮装什么”,记忆管的是”哪些东西值得跨会话保留”。

六、记忆会怎么坏

比上下文污染更麻烦的是,记忆的错误是持久的。

失效 表现 后果
记错 把”A 方案”记成”B 方案” 之后每次都按错的来
记旧 用户换了技术栈,旧偏好还在 建议过时,且理直气壮
记漏 关键约束没写进去 下次重犯同一个错
冲突 两条记录互相矛盾 模型随机选一条执行

上下文污染至少会随会话结束消失,记忆污染会一直跟着你。

所以记忆系统比上下文系统更需要可编辑性:用户能看见它记了什么,能改,能删。看不见的记忆比没有记忆更危险,因为你不知道它凭什么做出某个判断。

七、记忆属于谁

最后一点,也是最容易被忽略的:记忆的所有权。

理想的模型是:记忆是用户的资产,不是模型的内部状态。它存在用户能访问的文件里,用普通文本,能备份、能迁移、能删除。工具只能追加,不能悄悄改写历史。

这也解释了为什么全局记忆通常不允许模型直接写——那是用户自己维护的部分,模型只能读。模型写日志,人写原则。


下一篇讲这些工具到底跑在什么地方:一个没有 root、没有命名空间的 Android 手机,怎么装下一个 Linux。