第8章:记忆系统(Memory)
如果说工具是 Agent 的手,那记忆就是它的大脑容量。没有记忆,Agent 每次都是从零开始——拿到任务,执行,结束,全部遗忘。下次来同一个用户,它不记得上次说过什么;同一个 Agent 跑了几十步,却不知道自己三步前做了什么决定。
记忆系统解决的是这个问题:Agent 在什么时候能看到什么信息?哪些信息需要跨会话保留?怎么在有限的注意力预算里只放"真正有用"的那些东西?
这一章不打算给你列一张"记忆类型大全"的表格然后就结束了。更值得讲的是背后的工程权衡:不同记忆机制的存取延迟、精度损失、实现成本,以及什么场景下该选哪一种。
8.1 记忆的三层架构
核心直觉
Agent 的记忆可以分三层:当前任务窗口里的、任务执行中产生的临时状态、以及跨会话需要持久化的长期知识。层级不同,存取方式不同,用途也不同。
认知科学的类比,以及它的边界
Lilian Weng 在 2023 年的 Agent 综述中用人类记忆系统类比 LLM Agent 的记忆 [1]:
- 感知记忆(Sensory Memory)→ 原始输入的 Embedding 表示
- 短期记忆(Short-Term Memory)→ 上下文窗口内的信息(有限且临时)
- 长期记忆(Long-Term Memory)→ 外部向量存储,支持快速检索
这个类比有用,但不要当成工程设计的全部依据。人类记忆是几百万年演化的结果,LLM 的记忆是工程选择的结果。两者最大的不同:人类记忆会自动根据"重要性"选择性遗忘,LLM 的上下文窗口是一刀切的——放进去的信息权重相近,放不进去的信息直接丢失。
更实用的框架是按照存取时机和持久化范围来划分:
短期记忆:当前 LLM 推理时的上下文窗口。所有内容对模型都"可见"——系统提示、对话历史、工具返回结果、当前问题。优点是访问延迟为零,模型可以直接推理。缺点是容量有限,而且随着内容增多,准确率会下降(后面会详细讲)。
工作记忆(Scratchpad):Agent 在任务执行过程中维护的临时状态。不完全等于"上下文窗口"——Scratchpad 可以被持久化到上下文窗口之外,然后在需要时读回来。代码 Agent 里的 NOTES.md、Claude Code 里的任务状态文件,都是工作记忆的具体实现形式。
长期记忆:跨会话持久化的信息。向量数据库存的语义记忆(这个用户喜欢什么风格)、关系数据库存的事实记忆(这个用户的订单历史)、KV 存储存的快速查找记忆(用户的偏好设置)。不同类型的长期记忆对应不同的存储后端,不能混为一谈。
这三层之间的张力
工程上真正麻烦的不是"哪层存什么",而是三层之间的信息流动:什么时候从长期记忆里取回什么,取回之后怎么放进上下文窗口,上下文窗口满了之后怎么决定压缩还是丢弃。这个流动过程设计不好,Agent 的表现会很差——要么对话历史太长导致注意力稀释,要么关键上下文被丢弃导致任务失败。
8.2 上下文窗口管理策略
核心直觉
上下文窗口是 Agent 的"工作桌面",桌面有限,必须主动管理放什么、什么时候清理、什么时候换一张桌子继续工作。
Context Rot:更长不等于更好
Anthropic 的 Context Engineering 文章(2025 年 9 月)引用了一个关键研究 [2]:Chroma 的 context rot 实验发现,随着上下文窗口中 token 数量增加,模型准确回忆上下文中信息的能力会下降。这个现象在所有主流模型上都能观察到,有些模型退化更慢,但没有哪个模型完全免疫。
根本原因是 Transformer 的架构决定的——每个 token 和所有其他 token 之间的注意力计算是 $O(n^2)$ 的关系。上下文越长,注意力越被稀释。模型在训练时接触的长序列数据也相对较少,参数对长程依赖的捕获能力本来就弱于短程。
这意味着:把所有东西都堆进上下文窗口是一个错误的工程选择,哪怕你的模型支持 200K token 的上下文窗口。Anthropic 的建议是 [2]:
"Context must be treated as a finite resource with diminishing marginal returns."
上下文是一种有限资源,边际收益递减。好的 Agent 应该把上下文当成昂贵的"注意力预算"来管理,而不是无限堆砌。
三种窗口管理策略
策略一:滑动窗口(Sliding Window)
保留最近 N 轮对话,丢弃更早的内容。实现简单,但有一个根本问题:早期的重要上下文(比如用户在第 1 轮说的约束条件)会被丢弃,而 Agent 可能在第 50 轮才发现自己需要它。
适合场景:对话历史重要性随时间单调递减的任务,比如简单的客服 FAQ。不适合场景:有长期目标的任务,或者早期上下文包含关键约束的任务。
策略二:摘要压缩(Compaction)
接近上下文上限时,用另一次 LLM 调用把对话历史压缩成摘要,然后用摘要替换原始历史。
Anthropic 在 Claude Code 里实现了这个策略 [2]:把消息历史传给模型,让它总结并压缩关键细节——保留架构决策、未解决的 bug 和实现细节,丢弃冗余的工具输出和消息。压缩后 Agent 可以带着压缩上下文继续工作,外加最近访问的 5 个文件。
压缩的艺术在于取舍:过于激进的压缩会丢失那些"当时看起来不重要但后来很关键"的细节。Anthropic 的工程建议是:先最大化召回(确保所有相关信息都被保留),再迭代提升精度(删除真正多余的内容)[2]。
一个经验法则:工具调用的原始结果是最安全的压缩对象。一个工具调用返回了 500 行 JSON,但关键信息就两个字段——把这 500 行保存在上下文里毫无意义。Anthropic 已经把"工具结果清理"(Tool Result Clearing)作为一个功能发布到了开发者平台 [2]。
策略三:选择性保留(Selective Retention)
主动判断每条信息的重要性,只保留"高信噪比"的部分。这比滑动窗口更智能,但需要一个额外的评分机制来决定什么值得保留。
Generative Agents(Park et al., 2023)[3] 的实现提供了一个参考:他们的记忆检索模型综合考虑三个维度——近期性(越新的事件权重越高)、重要性(直接向 LLM 询问"这条记忆有多重要",1-10 打分)、相关性(和当前查询的语义相似度)。最终检索分数是三个维度的加权和。
# Generative Agents 的记忆检索评分(简化版)
def compute_retrieval_score(memory, query, current_time):
recency = decay_factor ** (current_time - memory.created_at).hours
importance = memory.importance_score # LLM 提前打好的分
relevance = cosine_similarity(memory.embedding, query_embedding)
return alpha * recency + beta * importance + gamma * relevance这个设计的工程意义:把"哪些记忆值得检索"的决策权从单一的相似度搜索分散到三个独立维度,让最近发生的、本质重要的、语义相关的信息都能被找到——而不只是"当前 query 语义最近的那些"。
实战:多长的上下文算"安全"?
没有通用答案,取决于具体模型和任务。但有一些经验参考(注意:这些来自开发者社区的观测,不是官方 benchmark 结论):
- 超过几万 token 时,信息检索准确率会有可观察的下降
- 复杂推理任务比简单 QA 对上下文长度更敏感
- 中间位置的信息比开头和结尾更容易被"遗忘"(Lost in the Middle 现象,Liu et al., 2023 [4])
实践建议:在你的真实任务上做针刺测试(needle-in-a-haystack)——把关键信息放在不同位置,看模型能否找到它。这比依赖 benchmark 数字更可靠。
8.3 向量记忆:语义搜索外的设计决策
核心直觉
向量数据库让 Agent 可以用"语义相似"而不是"关键词匹配"来检索历史记忆,但向量记忆不等于"把对话历史存进向量库然后 similarity search"——怎么存、存什么、什么时候取回,才是真正的工程问题。
Embedding + 向量数据库的工作原理
Embedding 模型把文本转成一个高维向量,语义相似的文本映射到向量空间中距离较近的位置。向量数据库在这些向量上建立索引,支持快速的近似最近邻(ANN)搜索。
技术细节上,现代向量数据库(Chroma、Milvus、Qdrant、Weaviate 等)主要使用 HNSW(Hierarchical Navigable Small World)算法做近似 ANN 搜索。HNSW 构建多层图结构,上层是"高速公路"(稀疏连接),下层是"本地道路"(密集连接),查询时从高层开始快速定位区域,再在底层精细搜索。这让搜索复杂度从暴力搜索的 $O(n)$ 降到 $O(\log n)$,在百万级别的向量库上仍能保持毫秒级响应 [5]。
向量记忆的四个设计决策
决策一:存什么单元?
最自然的想法是"把每轮对话存成一条记忆"。但这不是最好的选择。对话包含很多噪声(客套话、澄清提问、重复信息),而且一轮对话可能包含多个独立的信息点。
更好的实践是在存储前做信息提取:从对话中抽取出结构化的事实或偏好,而不是存储原始文本。比如:
- 原始:
"用户说:我是一个前端工程师,做了 5 年,最近在学 Rust,不太喜欢太理论的东西" - 提取后存储的多条记忆:
"职业背景:前端工程师,5 年经验""当前学习目标:Rust 语言""偏好:避免过度理论化的内容"
提取后的记忆更精确,检索时信噪比更高,也不容易被多余的上下文稀释语义。
决策二:何时触发存储?
不是每轮对话结束都需要存记忆。频繁写入有代价:增加延迟、引入噪声、可能产生重复或冲突的记忆条目。
实践上常用的触发策略:
- 会话结束时批量提取:一次性从整个会话中提取关键信息
- 事件触发:检测到明确的"偏好表达"、"用户纠正 Agent"、"任务完成"等事件时
- 定期检查:每 N 轮对话触发一次记忆整合
决策三:如何处理冲突和更新?
用户的偏好会变。上周说"偏好简洁",这周开始要求详细解释。向量数据库里的记忆不会自动更新,你需要设计一个机制来处理:
- 时间衰减:给记忆条目加上时间戳,检索时给近期记忆更高权重
- 显式覆盖:检测到新的偏好表达时,标记旧记忆为过时
- 版本追踪:保留记忆的历史版本,让 Agent 可以感知"用户的想法变了"
这块没有最优解,取决于你的应用场景对"记忆稳定性"和"记忆实时性"的权衡。
决策四:Embedding 模型的选择
向量记忆的检索质量很大程度上取决于 Embedding 模型的质量。截至 2026 年 4 月,MTEB 排行榜(Massive Text Embedding Benchmark)是评估 Embedding 模型的主要基准 [6]。主流的选择包括 OpenAI 的 text-embedding-3-large、Cohere 的 embed-v3、以及各种开源模型(bge-m3、e5-large 等)。
选择时要考虑:
- 语言支持:如果需要多语言支持,要选专门的多语言模型
- 维度大小:维度更高不一定更好,但太低会损失信息
- 领域适应:通用 Embedding 在特定领域(医疗、法律、代码)可能不如领域特化模型
向量记忆的典型失败模式
语义搜索找到的不是"需要的"。向量相似度找的是"语义最近的",不是"最相关的"。一个 query "用户喜欢什么编程语言" 可能检索到 "用户最近在写 Python 代码" 而不是 "用户说自己更喜欢强类型语言"——两条记忆语义上都有关,但第二条才是真正需要的偏好信息。
缓解方法:在存储时给记忆打结构化标签(type: preference、type: fact、type: event),查询时加入元数据过滤,不要只依赖纯向量相似度。
幻觉放大。如果 Agent 误信了用户说的错误信息并存入了记忆,后续每次检索到这条记忆都会强化这个错误。向量记忆没有自动的"事实核查"机制。
8.4 结构化笔记:Agent 外置的持久化记忆
核心直觉
让 Agent 主动把关键信息写入上下文窗口外的文件或数据库,需要时再读回来——这是最直接也最可靠的长期记忆实现方式。
为什么文件比向量库更适合某些场景
向量库适合"语义检索":从大量记忆中找到和当前情况最相关的那些。但有些记忆根本不需要语义检索——比如"当前任务的待办列表"、"本次会话的关键决策"、"需要在下一步确认的问题"。这类信息的特点是:你知道它在哪里,你总是需要它,你需要完整地读取它,而不是"找最相似的几条"。
Anthropic 的 Context Engineering 文章把这种模式叫做 Structured Note-Taking(结构化笔记)[2]:
"Structured note-taking, or agentic memory, is a technique where the agent regularly writes notes persisted to memory outside of the context window. These notes get pulled back into the context window at later times."
Claude Code 里 Agent 会维护一个 NOTES.md 文件来记录任务进度。Claude 玩宝可梦的演示 [2] 更直观:Agent 在上下文窗口之外维护地图记录、训练进度、已获得的道具——没有这些持久化笔记,每次上下文压缩后 Agent 都会"失忆",多小时的连续游戏就无从谈起。
结构化笔记的实现设计
一个基础的结构化笔记工具通常包含三个操作:
# 三个核心操作:读取、追加、更新
tools = [
{
"name": "read_notes",
"description": "读取当前任务的笔记文件,用于回顾之前记录的关键信息、决策和待办事项",
"input_schema": {
"type": "object",
"properties": {
"section": {
"type": "string",
"description": "要读取的笔记章节,如 'decisions'、'todos'、'context'。留空则读取全部"
}
}
}
},
{
"name": "write_note",
"description": "向笔记文件追加或更新一条记录。用于保存重要发现、决策原因、或任务状态",
"input_schema": {
"type": "object",
"properties": {
"section": {"type": "string", "description": "笔记章节:'decisions'、'todos'、'context'、'findings'"},
"content": {"type": "string", "description": "要记录的内容"},
"mode": {"type": "string", "enum": ["append", "replace"], "description": "追加还是覆盖此章节"}
},
"required": ["section", "content", "mode"]
}
}
]笔记文件的结构建议保持简单而有规律:
# 任务状态笔记
## 核心目标
帮用户重构 auth 模块,保持 API 兼容性
## 关键决策
- [2026-04-22] 决定保留旧接口,用 adapter 层兼容 → 原因:有 3 个依赖服务未准备迁移
- [2026-04-22] 选择 JWT 替代 session cookie → 原因:用户说后端是无状态服务
## 待确认
- auth 模块是否需要支持 OAuth2?用户还没说清楚
- 测试覆盖率要求?
## 已完成
- [x] 分析现有 auth 模块结构
- [x] 确认 API 接口签名有几个设计细节值得注意:
记录决策原因,不只是决策本身。"为什么选择 A 而不是 B"比"选择了 A"重要得多。上下文压缩后,Agent 可能会质疑自己之前的决策——有了原因,它就能判断这个决策是否依然成立。
区分"确定的事实"和"待确认的事情"。把未确认的假设放进"已知事实"是一个常见错误,会导致后续推理建立在错误前提上。
笔记要能被独立读懂。笔记会在上下文压缩后被重新读入,那时候之前的对话历史已经不可见了。每条记录都应该能在没有上下文的情况下被理解。
结构化笔记 vs 向量记忆
| 维度 | 结构化笔记 | 向量记忆 |
|---|---|---|
| 适用场景 | 有固定结构、总是需要读取的状态 | 大量历史记忆中的语义检索 |
| 读取方式 | 直接读文件/数据库 | ANN 相似度搜索 |
| 实现复杂度 | 低——给 Agent 一个文件读写工具 | 中——需要 Embedding + 向量库 |
| 检索精度 | 精确——你知道存什么、取什么 | 近似——找"语义最近的" |
| 冲突处理 | 手动设计 | 需要额外的去重/版本逻辑 |
| 适用容量 | 有限(适合当前任务状态) | 大规模(百万级记忆条目) |
实践上两者通常结合使用:结构化笔记管理"当前任务状态",向量记忆管理"跨任务的用户偏好和历史知识"。
8.5 Multi-turn 对话的记忆工程:什么该记、什么该忘、什么时候压缩
核心直觉
多轮对话的记忆管理本质上是一个信号处理问题:从不断增长的信息流中,实时筛选出真正有价值的信号,丢弃噪声,保持信噪比。
什么该记
不是所有信息都值得持久化。以下几类信息优先保留:
用户偏好和约束:用户说"我不喜欢过于简洁的代码注释",这应该记录。下次 Agent 写代码时需要考虑这个偏好,而不是每次都从头问。
关键决策和原因:Agent 做了某个技术选型,原因是什么,记录下来。后续如果情况变化,Agent 可以判断是否需要重新评估这个决策。
任务状态和检查点:当前任务执行到了哪一步,哪些子任务完成了,哪些还在进行。对于长任务(超过一个上下文窗口)尤其重要。
已发现的问题和阻塞点:哪些尝试失败了,失败的原因是什么。这能防止 Agent 在同一个坑里反复踩。
什么该忘
以下内容可以放心压缩或丢弃:
工具调用的原始输出:list_files 返回了 200 个文件名,Agent 已经处理完了,下次不需要看这 200 行原始输出,只需要记住"src/auth/ 目录下有 8 个文件,其中 3 个需要修改"。
中间验证的过程:Agent 确认了某个事实,验证过程本身不需要保留,结论才需要。
重复的澄清对话:用户解释了三遍同一件事(因为 Agent 没理解),最终那次理解正确后,前两次的内容可以丢弃。
已解决问题的详细上下文:某个 bug 已经修复,修复过程的详细 debug 记录在任务结束后意义不大(除非有特殊的"经验总结"场景)。
什么时候触发压缩
固定阈值触发:当上下文使用率超过 70-80% 时触发压缩。这是最简单的策略,Anthropic 在 Claude Code 的实现中采用了类似方法 [2]。
阶段性触发:完成一个可识别的子任务后压缩一次。比如代码重构任务分成"分析"、"设计"、"实现"、"验证"四个阶段,每个阶段结束时压缩前一阶段的详细上下文,只保留结论。
事件触发:检测到特定事件(比如用户说"好的,这部分搞定了,我们继续下一个问题")时触发压缩。
不建议的做法:在任务进行中频繁压缩(每隔几轮就压缩一次),这会导致 Agent 丢失太多即时上下文,推理连贯性下降。压缩应该发生在自然的断点,而不是机械地按轮次触发。
一个典型的长任务记忆设计
假设你在构建一个能持续几小时工作的代码 Agent,处理一个大规模重构任务:
关键设计点:NOTES.md 是贯穿整个任务生命周期的"持久记忆",每次上下文压缩后都会被完整读回来,保证任务的连贯性。而上下文窗口里的内容是"当前工作记忆",压缩时会被大幅精简。
这个模式有个有意思的地方:Agent 实际上在帮自己的"未来版本"留下足够的上下文。压缩之后继续工作的是同一个 Agent,但它会丢失很多之前的细节——NOTES.md 就是连接前后两个"自己"的桥梁。
常见误区
误区一:上下文越长,Agent 越聪明。不是。上下文越长,信号被噪声稀释的程度越高。一个精心管理的 20K token 上下文往往比一个堆砌了 100K token 的上下文效果更好。
误区二:向量记忆是万能的。向量相似度适合"从大量历史中找相关的",但不适合"精确管理当前任务状态"。把任务状态存进向量库然后每轮都搜索,不如直接维护一个结构化的状态文件。
误区三:压缩会丢失重要信息,所以不要压缩。不压缩的代价是上下文膨胀、注意力稀释、最终任务失败。设计好压缩策略(什么一定要保留、什么可以压缩)比避免压缩更重要。
误区四:记忆系统和 RAG 是一回事。RAG(检索增强生成)通常指的是从外部知识库检索信息来增强生成质量,面向的是"知识查询"场景。Agent 记忆系统面向的是"状态管理"场景——记录 Agent 的执行历史、用户偏好、任务状态。两者都用向量检索,但设计目标、数据结构和更新频率都不一样。后者会在第 10 章详细展开。
面试高频题
Q:如何设计一个既不爆上下文又能保留关键信息的记忆系统?
好的回答框架:分三层来讲——
第一,识别不同信息的生命周期:把"一次任务结束就没用了"、"当前任务内持续需要"、"跨任务需要持久化"的信息分开对待,用不同的存储机制。
第二,主动管理上下文质量而非总量:与其想着"怎么塞更多",不如想着"怎么每条信息的质量更高"。工具返回的原始输出、已完成步骤的详细过程,在任务推进后都可以清理或压缩。
第三,为压缩设计明确的取舍规则:决策 > 原因 > 结论 > 过程。决策必须保留,过程可以压缩。触发压缩的时机选在自然的任务断点,不要机械地按轮次触发。
加分点:提到 context rot 问题——上下文越长不一定越好,因为注意力会被稀释;提到结构化笔记(Structured Note-Taking)作为跨上下文压缩周期的状态桥梁;提到不同场景下向量记忆和结构化存储的适用边界。
参考资料
[1] Lilian Weng - LLM Powered Autonomous Agents (2023) - https://lilianweng.github.io/posts/2023-06-23-agent/
[2] Anthropic - Effective context engineering for AI agents (2025.09) - https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
[3] Park et al. - Generative Agents: Interactive Simulacra of Human Behavior (2023) - https://arxiv.org/abs/2304.03442
[4] Liu et al. - Lost in the Middle: How Language Models Use Long Contexts (2023) - https://arxiv.org/abs/2307.03172
[5] Malkov & Yashunin - Efficient and Robust Approximate Nearest Neighbor Search Using HNSW (2018) - https://arxiv.org/abs/1603.09320
[6] MTEB Leaderboard - Massive Text Embedding Benchmark - https://huggingface.co/spaces/mteb/leaderboard