Agent 记忆:真实项目中的分层、记录与消费

本文是本人基于实战的看法,由于项目的局限性,无法做到全而精准,大家可以批判的看看,有异议可以友好讨论。如有不认可,那肯定是本人学艺不精,你是对的。

全文约 3500 字, 预计阅读 9 分钟

读完可以:

  • 区分会话记忆、项目记忆、用户记忆的职责。
  • 用 4W1H 解释记忆的记录、存储与消费,并通过观测验证其效果。

问题问答

在我朋友的面试复盘样本中,约 1/3 面试官都会问到【记忆】,集中在记忆分层、记录时机、按需消费和跨项目同步。以下结合真实项目,说明记忆如何转变为后续任务可使用的上下文。

问题一:Agent 的记忆怎么分层?

面试官:你们的记忆是怎么区分的?

真实项目按作用范围划分三类:“会话记忆”承接当前任务的连续执行;“项目记忆”保存产品规格、约束和协作反馈;“用户记忆”保存跨项目复用的偏好、知识和经验。

其中会话记忆存储在当前 session 中,会受到【压缩】的影响;项目记忆和个人记忆分别维护,按任务需要进入模型上下文,不会受到【压缩】的影响;记忆和 skill 类似,都是渐进式披露的,写入到模型中的都是 catalog,模型自己判断需要对应记忆通过 tool 去 Read 即可。

问题二:Agent 如何保证历史信息能在当前 Run 被消费

如何使用用户之前关注的主题?

这里分两部分回答,一个是当前会话介绍过的主题,一个是在其他会话中提及的主题,两种记忆都是长期记忆,分别隔离存储;

整个 agent 编排中,我们都是优先让 agent 判断是否需要读取长期记忆,如果 agent 能在当前 context 信息中获取到对应信息,则不会去调记忆 catalog 去执行 Read(); 同理会话记忆的优先级会比个人记忆要高,这里主要考虑的是会话记忆在时序上会比个人记忆更加 fresh,在没有进行“同步”前,两者可能会存在差异;

如果用到了长期记忆,基本就是根据 intent, 然后确认是否有对应文件,然后读取对应文件获取【记忆】信息,然后重新写入到 context 中,然后 agent 就是知道用户之前有关注哪些主题了,至于这些主题是否会被消费,这个就看 agent 的使用,以及是否正确引导 agent 去使用这些主题了;

需要注意的是,这里是假设【关注的主题】都被认可写入到了【记忆】中,但是具体是否真实触发【记忆】写入,以及写入的内容是否准确,涉及到【记忆】的其他设计,不在本轮进行介绍。

问题三:不同会话的记忆如何进行交换使用

会话 A 更新记忆后,会话 B 怎么同步?

不同会话的记忆共享,主要存储到【用户记忆】中,这里涉及到了记忆的存储时机和介入时机;

对于任意一个会话,都会存储一份【用户记忆】的副本,而这个副本更新时机是每一轮真实对话后,模型执行前触发的,它会去调用接口查看记忆hash是否有更新,确认是否要更新记忆副本。因此,如果 A 更新记忆时,B 已经处于会话判断后,则 B 在本轮(Run)会话中,确认不会拿到最新的 A 更新后的记忆信息;而只会在下一轮会话时,才能拿到最新的更新信息。

4W1H 简单介绍一下我知道的记忆

一、What:记忆存什么,如何组织?

不同业务需要保存的信息不同。客服 Agent 关注服务记录和用户诉求,开发 Agent 关注产品规格与技术约束,学习 Agent 关注知识背景和表达偏好。结构上,可以按作用范围划分为会话记忆、项目记忆和用户长期记忆。

会话记忆:保存当前任务的过程。

比如用户说“把报名按钮改成确认报名”,随后 Agent 读取页面、修改代码、收到工具结果。这些消息构成连续执行所需的上下文。说人话,就是传给 Model 的 messages 信息。我们在一轮对话中,从 user 提供第一条信息(system 是第 0 条),然后会有 agent 回复的 assistant 信息, 工具调用返回的 tool 信息,共同组成了** 会话记忆 **

原始记录可以持久保存,模型每次调用使用的是当前有效的消息视图;发生压缩后,这个视图会包含摘要和需要保留的近期消息。

项目记忆:保存当前项目的稳定状态与约束。

内容:“这是一个只面向移动端的活动报名工具”“报名无需登录”“已经确认的布局需要保留”。

在一个会话执行过程中,会有策略触发记录关键信息,这些信息属于某一个 session, 会在这一个 session 中长久保存起来,不会由于【压缩】而丢失;这一块记忆会作为 basic 信息,写入到用户的 context 中,用户在某些情况下需要进行查找时,可以读到并拉取完整的信息。

工作区/
├── memory/                         项目记忆,可读写
│   ├── MEMORY.md                   项目记忆索引
│   ├── feedback_visual_style.md    设计或协作反馈
│   └── reference_brand_assets.md   外部资料的用途与入口

当然, 如果没有过多的 memory 信息时,可能就只是单纯的一个文件信息即可,这里主要看 memory 写入策略。

用户长期记忆:保存跨项目可复用的信息。

比如用户偏好简洁话语、习惯先看流程图,或者持续关注某个领域以及已经存储的项目记忆目录。换一个项目后,这些信息仍可能帮助 Agent 理解需求。

工作区中的文件结构可以表达为:

工作区/
├── .memory/                        用户长期记忆的只读投影
    ├── hot.md                      简短概览与重要提示
    ├── index.md                    主题、标签与文件路径
    ├── log.md                      记忆整理记录
    └── wiki/
        ├── concepts/               概念与领域知识
        ├── entities/               对象及相关事实
        ├── references/             外部资料与引用
        ├── synthesis/              综合分析与结论
        └── journal/                有保留价值的阶段记录

【用户记忆】写入 llm 和我们正常阅读 wiki 是一样的,主要是查看 index.md 和 hot.md, 需要更详细内容时,再去读取对应的 wiki/* 信息;index 的内容大概长下面👇的样子

二、When / Where:什么时候加载,又在哪里进入模型?

记忆的准备发生在用户触发一轮对话之后、Agent 调用模型之前。这样,模型第一次判断任务时,就有机会结合已有约束和用户背景。

例如,用户只说“之前在别的项目中,做过需求澄清的分析,这一次也按照之前的方式来进行需求澄清”。如果项目记忆记录到了[需求澄清] 这个记忆,那么 agent 有可能调用 Read(xxx) 读取这个记忆信息,然后加载到当前 context 中,然后进行后续的操作。

这里需要区分两个动作:

  • 同步文件:每轮请求开始时,运行时取得用户长期记忆,并更新项目中的只读投影。
  • 注入消息:运行时将需要的记忆导览组织成独立的 user 消息,放在本轮真实用户输入之前。

实际代码并不是每轮都重复追加完整导览。项目记忆与用户 Wiki 的静态导览主要在首次进入、上下文压缩后注入;其余轮次继续消费上下文中保留的信息,必要时再读取文件。

这一设计让已有知识能够参与最初的任务判断,也减少了重复注入的 token 开销。代价是同步和读取需要时间,导览会占用上下文,而且不同会话之间可能存在版本延迟。

以同一用户的会话 A 和会话 B为例:

  1. B 开始执行时,读取到的偏好是“使用浅色界面”。
  2. A 随后更新偏好,并完成长期记忆整理:“今后优先使用深色界面”。
  3. B 已经在执行,本次输入不会被中途替换,仍可能沿用旧偏好。
  4. B 下一次请求可以同步新版文件,但上下文中已有的旧概览,还需要通过重新读取或再次注入才能更新。

因此,文件已经同步到最新版本,不等于模型正在使用的记忆也立即更新了。

对于偏好和历史背景等变化较慢的信息,短暂延迟通常可以接受;需要立即生效的当前要求,应通过本轮明确输入或有效业务状态传递。

三、Why:为什么需要记忆?

记忆让已经确认的信息在后续任务中继续有效。

同一个项目经过多轮修改,用户不应该反复解释目标人群、页面范围和设计约束;用户开启另一个项目时,也不应该重新说明所有稳定偏好。项目记忆维护前一种连续性,用户长期记忆支持后一种复用。

压缩进一步体现了这种分工。对话摘要用于承接当前任务,但不适合承担全部长期知识。完整规格、决策依据和主题知识保存在独立文件中,压缩后重新提供入口,Agent 就能按需取回细节。

例如,历史聊天已经压缩,但 MEMORY.md 仍保留整个 session 的核心信息。Agent 需要获取 session 的某些核心信息,可以重新读取 MEMORY.md,查看摘要和 catalog;如果需要详情,可以读取到对应的文档,而不至于丢失核心模块的全部信息。

四、How 1:会话中的信息怎样成为长期记忆?

这条链路经历两次筛选:从对话中提取项目事实,再从项目事实中整理跨项目知识。

第一步发生在正常对话中。消息和工具结果形成会话记录;当用户确认需求、纠正做法或明确要求记住某件事时,Agent 判断哪些信息值得持久保存,再通过文件工具更新 memory/。

例如,用户说:

后续所有学习专题会话都要简洁专业,不要有情绪价值的信息输出。

适合保存的不只是这句话,还包括适用范围和使用方式:

事实:用户偏好简洁专业
适用范围:后续项目的信息输出
使用方式:在学习专题对话中,输出前确认一下信息输出是否过度使用情绪化信息

正文写入独立主题文件,MEMORY.md 增加一条简短入口。若内容已经存在,就更新原主题,避免反复生成同义记录。

第二步由独立的记忆整理任务完成。真实项目中的一种触发策略是:项目会话静默一段时间后,再处理发生变化的记忆文件。

这么做可以减少对尚在反复讨论的信息进行频繁整理,但也意味着个人长期记忆不会在每次对话后立即更新。

整理任务读取项目记忆快照,判断哪些内容应新增、哪些应合并、哪些与旧结论冲突,再更新 Wiki 页面、索引和概览,并保留来源信息。

因此,个人长期记忆的直接原料是经过筛选的项目记忆与用户提供的资料,而非完整聊天记录的无差别复制。

五、How 2:进入模型的是索引,还是另外生成的 Catalog?

源码中的做法是:读取已有的索引和概览内容,再加上固定使用引导,组成记忆导览消息。 Catalog 是这组导航信息的统称,并没有另外生成一份独立的语义目录。

具体包含三部分。

第一部分是项目记忆索引:读取 memory/MEMORY.md,将索引内容放入消息,并限制长度。模型据此知道当前项目有哪些记忆文件。

第二部分是用户记忆概览与索引:读取 .memory/hot.md 和 .memory/index.md。hot.md 提供简短背景,index.md 提供主题到正文路径的映射。

目录较小时,索引页表会一起进入上下文;目录较大时,只注入索引前言和文件入口,引导模型先读取完整 index.md,再定位正文。索引位于 .memory/index.md,wiki/* 存放主题页面。

第三部分是固定使用引导:说明这些信息来自跨项目记忆、当前目录只读、内容可能与本轮任务无关,以及需要时如何寻找对应页面。

记忆相关输入可以抽象为:

user:运行时记忆导览
  ├── memory/MEMORY.md 的项目索引
  ├── .memory/hot.md 的简短概览
  ├── .memory/index.md 的页表,或索引入口
  └── 路径、相关性、只读边界等使用说明

user:本轮真实用户问题

这条路径不依赖额外的向量检索服务。知识在写入端已经按主题组织,读取端由模型结合当前问题选择页面。它降低了默认注入量,但增加了按需读取的工具开销,也要求索引足够准确、及时。

观测时应分别检查:模型是否看到了目录、是否选择了正确页面、读取是否成功,以及最终回答或行动是否正确使用了正文。“目录已注入”只证明记忆可被发现,不能单独证明记忆已经生效。

小结

当对话、工具结果和已展开的记忆正文不断增长,接下来的问题就是:如何通过【压缩】控制上下文规模,同时保留任务状态,并恢复这些记忆的消费入口。这一块我们下一次再讲