JavaGuide - Agent记忆系统
Agent 记忆系统
AI Agent 记忆系统:短期记忆、长期记忆与记忆演化机制
记忆系统的设计
Agent 的记忆一般分成短期和长期,二者需要从逻辑上、物理上进行隔离。不过,一般而言长期记忆从短期记忆的沉淀中得来。
短期记忆
短期记忆是内存的记忆,用于存放当前交互过程,包括用户提问、模型每轮回复、工具调用的中间结果。我们可以说:短期记忆就是Agent动态构建的Context。
短期记忆长度受限于模型的窗口,为了防止 Context 超过窗口,一般对短期记忆做如下操作:
压缩。
卸载:将上下文分索引,只将索引存到上下文中,内容卸载到本地持久化,按需查找再填入 Context。
隔离:一般是 Multiagent 时,master 给 slave 相对干净的上下文执行。
长期记忆
长期记忆是持久化的记忆,例如 coding agent 普遍用 jsonl 文件存储交互信息;又或者一些通用 Agent 中将一些真理知识存入向量数据库,这些信息很大程度来源于短期记忆。
那长期记忆和 RAG 有什么区别?
技术侧重点不一样;长期记忆只是单纯地将重要信息持久化,防止丢失;而 RAG 系统则在记忆系统的基础上,担负着赋予 Agent 更多通用知识的责任。
存储侧重点不一样;长期记忆由短期记忆沉淀得来,一般存储的也是项目和用户的非通用知识,例如项目采用的技术、方案,用户的个人信息、偏好等,也就是
语义信息;而 RAG 系统则一般存的是通用知识,例如各种博物学知识、各种生活常识、团体内部规章制度等。
长期记忆和 RAG 在大多数场景是可联合的,不是非此即彼。
记忆的高级演化
记忆光有存储和检索还不够,还需要搭载反思、固化、遗忘等机制,保证记忆足够权威、信息熵足够。
反思
一般的记忆系统都非简单的 append-only,而是要对已有的记忆进行总结,提炼出真正高价值、高现实依据的信息。这就是反思与融合。
一般而言,常见的反思有三类,分别发生在不同时间点。即一次交互完成后反思、一次交互内的每个子任务完成后反思、当连续执行高度相关的任务后反思。
反思坍缩
即便 llm 的执行效果非常完美,但是在用户的一句 “你应该反思” 下,llm 会陷入自我怀疑:
1 | 没有值得反思的地方啊?难道要反思我太完美了吗?不对不对,用户说了要反思,说明我肯定是错的,用户才是对的,我再好好看看...... |
之后,大概率会陷入与谄媚一样的问题:llm 会从犄角旮旯处翻出一点称不上错误的错误将其描述得非常严重,更有甚者会推翻自己原本正确的结论,来论证用户要求他们反思的这个决定是多么的正确。
这个问题就是反思坍缩(或者别的名称)。
事实大于反思
在早期时,ai 只能做做简单的任务,反思带来的效果确实很显著;但随着 agent 时代到来,ai 需要做更多更复杂的任务时,反思逐渐非但不带来提升反而会造成负面影响,不仅会降低性能、增加成本,更严重的是极大概率会降低输出效果。
基于反思坍缩这个事实的存在,如今业内已形成主流认知:反思本身没有意义,外部反馈才有意义。
具体而言,大家现在严禁大模型自己反思,要推翻结论必须有权威证据。也就是从反思然后发现问题,转变为证据表明出现了问题。
这个思想在记忆系统也已形成,以前的记忆系统,让 llm 自己反思主动评估记忆库中记忆的重要度,然后进行固化、遗忘或者修改,但事实是这种做法不可靠,更不安全;而现在的记忆系统,则是基于事实进行行动:记忆调用的频次、记忆直接的相关度和重合度等参数,直接接管记忆是该融合、该固化还是该遗忘。
所以归根结底,反思的问题是反思是否与事实一致?如果能够保证事实优先的话,反思似乎也有存在的价值,只不过不要识图从已有的事实中反思问题,而是反思有哪些问题是证据中没提到的。
所以在当下而言,我们可以说:单纯的反思一无所用。
合成、固化、遗忘
合成,就是将指向同一个事实的多个记忆,融合成单独的一个;固化就是提高一个记忆的重要度,让其在检索时得到更高的权重;遗忘就是降低一个记忆的重要度,最终使其不会被检索到。
遗忘机制的实现
两种实现方案:1.基于权重衰减;2.基于事实冲突。
第一种很简单,对每条记忆维护一个 score 例如 score = relevance × importance × decay(t) ,会时间衰减,最符合人类记忆特征。
第二种的话要求新旧记忆直接产生明显事实冲突,改为新的事实。
在实践中,两种方法可以共存。第一点可以随时保证记忆库噪声即时清理,第二点则可以保证紧要记忆随时保持正确。
优化记忆检索
混合检索
我们知道,向量数据库的向量检索(Dense Retrieval)是计算余弦距离,是纯粹的数学途径,那么有没有可能出现两条毫无关联的信息余弦相近呢?是有可能的,就和哈希冲突一样。
所以,我们在实践中不能完全相信向量检索,引入传统的关键词对比(BM25 / Sparse),动态调整二者检索结果的权重,做成混合检索机制可以提升召回率。
元数据过滤
在混合检索之前,增加一道元数据过滤(Hard Filters),基于 UserID、组织 ID、时间范围、业务标签等条件,滤除不满足的信息。
多重视检索链路
检索链路优化的 ROI 通常高于写入链路。
生产级记忆系统要点
| 维度 | 核心问题 | 解决方案 |
|---|---|---|
| 多维索引 | 召回精度 | Vector向量 + Graph图 + Keyword关键词 三种索引结合 |
| 隐私合规 | GDPR 等法规 | 写入前做 PII 脱敏 |
| 冷热分离 | 性能与成本 | 高频偏好缓存 + 低频背景 RAG |
GDPR:General Data Protection Regulation,欧洲最严格的数据保护法规之一,核心思想是用户有权知道你存了什么、有权要求删除自己的数据。
PPI:个人可识别信息
冷热分离:热点数据和冷门数据分开存储
高频偏好缓存:常用的有关用户偏好的信息,进行缓存,而非每次查询
Markdown
Md 文件也可以用来存储记忆,它的优势在于不需要繁琐的向量计算,低成本,阅读友好(各种意义上,ai read、人类打开就能直接读)。
不过,当 md 文件长度过长后,这些优势也就荡然无存了,所以如果要使用 md 存储记忆,必须将其长度控制在一定范围内,而且尽量存储高信息量的记忆。
基于 md 的以上特性,很多厂商都会维护 md 文档作为一部分记忆的存储方式。Manus 把文件系统视为结构化外部记忆;Claude Code 把 CLAUDE.md 和 Auto Memory 产品化;OpenClaw 等 Agent 项目和社区实践中,也能看到类似的文件化记忆思路。它们都说明,在不少 Agent 场景里,文件系统 + Markdown 已经是足够务实的长期记忆方案。
CLAUDE.md
CLAUDE.md 是 cc 记忆系统采用双轨制的一环,与自动积累的 Auto Memory 共同构成 cc 的记忆系统。
哪些该存,哪些不该存
首先,claude.md 的长度,官方建议控制在 200 行以内,如果 200 行以内写不完,可以把一些具体内容的文字拆分出去,给个引用链接,让 llm 自己去读就行。
claude.md 存放的一般是与项目相关的固定属性与规则,例如技术栈、各个组件的版本信息、为什么选择架构和组件等,由于渐进式披露的存在,llm 会根据这些信息编写更加符合版本的代码。
不适合存放的一般是行业默认行为,比如某种语言或框架的一般行为、大段文本等。
给出规范案例
claude.md 里面的规范,我们想要 agent 必须完全遵守,该怎么做?agent 确实可以每次都将 claude.md 存到 context,llm 能百分之百读到规则,却不一定百分之百按照规则执行,又或者是是不按我们所想的执行,这个时候必须考虑:是不是我们没写清楚?
事实上,我们最好给出案例,告诉 agent 该怎么做,例如:
1 | # 依赖注入 |
书写时按照项目惯例
标题尽量用常规名字,比如 Commands、Structure、Conventions、Testing。Claude 的训练数据里有大量标准 README 结构,它对这类标题下面通常写什么有稳定预期。
案例分析:PI Agent
PI 的 Context 有三大来源:system prompt、用户输入、session jsonl,三者共同占用上下文窗口。其中的 session json 就是 PI 的长期记忆来源。
Context
Context 由 agent 动态构建:当用户输入请求时,系统分别从三大来源搜集信息,将其拼接为一段文本,该文本的长度就是当前 Context 长度,按 token 数计算。当 token 数大于模型窗口,就必须考虑压缩上下文。然而,system prompt 和用户输入都是特别重要的信息,一般情况下不可减少,所以只能在 session jsonl 上进行压缩。
jsonl
jsonl 是 pi 的唯一长期记忆来源,里面包含了用户输入、模型回复、工具调用结果等。
拼接 Context 时,会将 jsonl 的文本全部提取,假如当前 jsonl 文件的长度是 200k,那就把 200k 的文本全部拼接到 Context 中,随后一次性发送给 LLM。
不过,以上做法仅限于未经过 compact 时的 jsonl。接下来介绍 jsonl 的压缩,这是 Context 压缩的主要手段。
jsonl 的压缩
jsonl 的压缩,实则就是发送给 LLM,让它帮忙对 jsonl 的内容进行总结。不过,我们当然不会直接对 jsonl 文件进行改动,依照 append-only 原则,我们要将总结得到的 summary 拼接到 jsonl 文件后面。
首先,哪些部分需要压缩?我们假设需要压缩时,jsonl 的长度是 800k:
1 | ------ |
pi 会保留最近的 20k 上下文作为 retainedTail,以保证近期任务的上下文不会压缩;而对前面的 780k 上下文进行总结,并将 10k (假设)的 summary 拼接到后文:
1 | ------ |
至此压缩完毕,随后继续 Context 的拼接。系统按顺序扫描 jsonl,发现存在 summary,于是将 summary 和前面的 retainedTail ,按照 [summary, retainedTail] 的顺序加入 Context,这样一来 jsonl 的占用就从原本的 800k 压缩为了 30k。
所以说,jsonl 的长度永续增加,由于 append-only,后面产生的上下文信息会继续在后文拼接下去,并且会持续地发生 compact,当多次 compact 时,Context 只会接受最后一个 [summary, retainedTail],前面所有的信息都会不再理会。
另外,之前的 summary 会被再次压缩吗?会压缩的,但是 pi 会保证之前的 summary 的重要信息会保留下来。但是其实也没必要担心,因为 jsonl 始终没有丢失,那么 Agent 就可以随时读回来。
