简介
本文档系统讲解 AI Agent 中的 Memory(记忆)管理机制,是 Agent 开发中的核心知识点。
LLM 原生的限制
核心问题:原生 LLM(如 $y = f(x)$ 形式的纯函数)没有记忆能力。
LM 的参数(a、b、c)在训练结束时已固定,不会随输入 X 变化而改变。我们所谓的“记忆”实际上是通过将历史对话拼接到输入上下文(X1, X2, ..., Xn)中实现的——LLM 利用注意力机制“看到”历史对话,从而看似“有记忆”。
上下文窗口的限制
第一层补丁有两个主要缺陷:
1. 存储空间有限:对话轮次增加导致输入序列无限增长,加重资源负载,可能导致 LLM 无法执行。
2. 注意力扩散问题:上下文过长时,LLM 无法将注意力聚焦在正确内容上,导致幻觉或答非所问。
解决方案:限制上下文窗口(Context Window)。业界经验值为 200K tokens(约 20 万),综合考虑了不同 LLM 的支持能力和注意力扩散问题。(注:文中提到的 800K 可能是口误,业界主流仍是 200K)
滑动窗口的问题
当上下文填满 200K 窗口时,滑动窗口(Sliding Window)的处理方式是:丢弃旧内容,填入新内容。但这是一个糟糕的方案。
问题所在:以前的数据不代表不重要。虽然旧内容大概率没新内容重要,但存在大量反例——如用户自我介绍、使用偏好(如希望用中文回答)、项目背景等关键信息,通常在对话最早阶段就已说明,一旦滑动窗口划走,这些重要信息将全部丢失。
主流解决方案:两大流派
业界主要有两种做法:
流派一:数据库方案(RAG)
将历史对话视为文本,类比公司内部文件库,采用传统 RAG 方法处理。
核心思路:
存储:将历史消息切分为 chunks(可按段落或字数切分),添加原信息(topic、用户偏好等),存入数据库(如 Elasticsearch,同时支持 BM25 关键字搜索和向量搜索,支持混合搜索)
检索:两种取法——
- 直取(快且省 token):直接 embed 用户 query 为向量,或使用分词器 + BM25 评分机制进行搜索
- LM 取(更精准):让 Agent 采用 ReAct 模式,分析 query 生成更合适的搜索策略,可迭代 refine 搜索方案
加载:将检索到的 chunks 插入上下文窗口,再加入当前 query
优点:理论上支持无限大的记忆库,检索效率高
流派二:Markdown 方案(文件 + 压缩)
来自 Claude Code 的方法,被部分开发者奉为圭臬。(注:Claude Code 原名应为此方案,但文中称"Cloud Code",可能为口误)
核心思想:将记忆分层,对旧内容进行有损压缩(蒸馏/Distill),提取关键信息写入 Markdown 文件。
三层记忆架构
Claude Code 风格方案将记忆分为三层:
第一层:Memory(长期记忆)
永远加载,记录跨会话的持久信息:
- Profile:用户或项目的基本信息
- User Preference:用户偏好(如希望用代码回答、减少辅助信息)
- Constraint:全局约束(如删除文件时必须提示用户)
第二层:Topics(会话主题)
按需加载,记录当前会话相关的主题信息。每个 topic 文件名本身即为一个 key(如 database-deployment、front-end-design),包含该主题的约束、配置、版本等信息。
更新机制:当检测到主题变更(如 MongoDB 8.0 → 8.1),LM 会自动更新对应字段,并保留历史变更记录(如 8.0 to 8.1),确保记忆可回溯。
第三层:Transcript(原始数据)
作为保底的 raw message 存储。万一前两层总结内容无法命中所需信息,可回溯原始对话。此层检索效率不如 RAG,但作为兜底方案仍有价值。
记忆文件的维护与更新
文件格式:Markdown 本质是纯文本,其结构化符号(标题、列表等)仅用于美化展示。使用时通常将 Markdown 结构化为 JSON/Dict 存储,修改时以编程方式操作,完成后再渲染回 Markdown 格式。
更新流程:
- 加载时:将 Markdown 结构化为 JSON/Dict
- 压缩时:让 LLM 依照预设 key 生成结构化数据
- 合并时:按 key 逐项更新 JSON 内容
- 落盘时:渲染回 Markdown 格式输出
核心设计权衡:Predefined Key vs LLM 自定义 Key
这是记忆系统设计中最关键的 Trade-off:
| 方案 | 特点 | 风险 |
|---|---|---|
| Predefined Key(冰/最可控) | 预设 20 个固定 key,强制 LLM 按这些 key 归纳总结 | 过于僵硬,可能无法覆盖新场景 |
| LLM 自定义 Key(火/最动态) | 完全由 LLM 决定 key 命名(如 profile vs overview) | 高度不可控,出现新 key 时需额外决策逻辑 |
业界实践:采用中庸之道——预设足够广泛的 predefined key 覆盖常见场景,同时保留让 LLM 生成新 key 的灵活性。
冲突处理:当 LLM 生成近义词(如 profile vs overview)时,可丢回给 LLM 让其匹配到完全一致的 key,确保 merge 正常进行。
Topic 检索机制
Agent 如何知道当前需要加载哪些 topic?
方案一:直接用 glob 搜索所有 Markdown 文件,列出可用 topic
方案二:将支持的 topic 轻量化写入 Memory 的 known_topics 字段(必须轻量,因为 Memory 永远加载)
后者可减少一次 LLM 调用,但 Memory 会稍大;前者则相反。
面试题
Q1:LLM 原生为什么没有记忆?现有“记忆”是如何实现的?
答案:LLM 本质是纯函数 $y = f(x)$,参数在训练结束时固定,不随输入变化。所谓“记忆”实际上是将历史对话(X1, X2, ..., Xn)拼接到输入上下文中,利用 LLM 的注意力机制“看到”历史对话,从而看似有记忆能力。
Q2:上下文窗口的限制来自哪里?业界如何解决?
答案:限制来自两方面——
- 存储空间:对话无限增长导致资源过载
- 注意力扩散:上下文过长导致 LLM 无法聚焦正确内容
解决思路:限制上下文窗口在 200K tokens 附近。当填满时,放弃简单的滑动窗口(会丢失重要历史信息),改用 RAG 数据库方案或 Markdown 压缩方案进行有策略的记忆管理。
Q3:RAG 方案和 Markdown 方案的核心区别是什么?
答案:
| 维度 | RAG 方案 | Markdown 方案 |
|---|---|---|
| 存储介质 | 数据库(Elasticsearch 等) | Markdown 文件 |
| 检索方式 | 向量搜索 + BM25 评分 | 文件搜索或 glob 匹配 |
| 信息形式 | 保留原始 chunks | 有损压缩,提取结构化 key-value |
| 适用场景 | 需要精确回溯、检索效率要求高 | 追求低 token 消耗、记忆高度结构化 |
Q4:Claude Code 的三层记忆架构是怎样的?各层的作用?
答案:
- Memory(第一层):永久加载,记录跨会话的长期信息(用户偏好、全局约束、项目背景等)
- Topics(第二层):按需加载,记录当前会话相关主题的详细信息(如数据库类型、版本号、具体约束),发生变更时自动更新并保留历史
- Transcript(第三层):原始对话存档,作为保底方案,当前两层都无法命中时回溯使用
Q5:Predefined Key 和 LLM 自定义 Key 各有什么优缺点?如何取舍?
答案:
- Predefined Key:优点是稳定可控,LLM 严格按指定格式归纳;缺点是覆盖范围受限,灵活性差
- LLM 自定义 Key:优点是高度动态,可适应新场景;缺点是不可控,出现新 key 时需额外 merge 逻辑
业界实践:预设足够广泛的 key 覆盖常见场景(冰端),同时保留 LLM 生成新 key 的能力作为扩展(火端),取中庸之道。
Q6:Topic 检索时,Agent 如何知道应该加载哪些 topic?
答案:两种方案——
- 动态搜索:每次用 glob 遍历所有 Markdown 文件获取可用 topic 列表
- 预写入 Memory:将支持的 topic 轻量写入 Memory 的
known_topics字段,Memory 加载时即获得 topic 列表
前者多一次 LLM 调用,后者增大 Memory 体积,根据实际场景权衡选择。