AI Agent(智能体)和普通聊天机器人最大的区别之一,是它会持续使用工具、读取文件、修改代码、跑命令,并在多轮交互中维护任务状态。任务一长,上下文窗口很快就会被塞满。
Claude Code 的上下文管理不是简单删除旧消息,也不是每隔几轮粗暴做摘要,而是一套分层压缩机制。最重的一层叫 Auto-Compact:当上下文接近上限时,把历史对话重写成结构化摘要,再通过附件、缓存重载、系统提示重建等通道恢复关键状态。
理解这套机制,对设计自己的 Agent 很有帮助,因为它解决的不是“少花一点 token”这么简单的问题,而是如何让 Agent 在长任务中不丢目标、不丢进度、不重复踩坑。
上下文窗口是什么
大语言模型(Large Language Model,LLM)本身没有传统意义上的长期记忆。每次请求模型时,都需要把它当前要看的内容一起放进去,包括:
- system prompt;
- 工具列表和工具说明;
- 用户消息;
- assistant 历史回复;
- tool use;
- tool result;
- 当前输入;
- 项目记忆文件,例如
CLAUDE.md。
这些内容加起来的长度不能超过模型限制,这个限制就是上下文窗口,通常用 token 计量。
flowchart LR
A[system prompt] --> W[上下文窗口]
B[工具描述] --> W
C[历史对话] --> W
D[工具调用结果] --> W
E[当前用户输入] --> W
W --> M[大语言模型]
M --> R[生成回复]
可以把上下文窗口理解成模型的工作台。模型只能处理放在工作台上的材料,超过工作台大小的内容要么无法放入,要么必须被删除、压缩或改写。
为什么 Agent 更容易撑爆窗口
普通聊天通常是一问一答,每轮消息较短。Agent 不一样,它在执行任务时会不断产生额外上下文,尤其是工具调用结果。
固定开销很高
一个代码 Agent 启动后,还没真正开始干活,上下文里通常已经放入了不少内容:
| 内容 | 作用 | token 压力 |
|---|---|---|
| system prompt | 约束 Agent 行为 | 较高 |
| 工具描述 | 告诉模型有哪些工具、参数怎么传 | 较高 |
CLAUDE.md | 项目规范、长期记忆、偏好配置 | 视项目而定 |
| 权限配置 | 限制哪些命令能执行 | 较低到中等 |
| MCP server 信息 | 注入外部工具能力 | 视工具数量而定 |
这些是“固定成本”。Agent 还没读取任何代码,上下文就已经占掉一部分。
工具调用会双倍记账
Agent 读取一个文件时,对话里通常会出现两类消息:
tool_use: Read("src/auth.ts")
tool_result: <src/auth.ts 的完整内容>
tool_use 要进上下文,tool_result 也要进上下文。真正占空间的是工具返回值,尤其是源码文件、日志、搜索结果、测试输出。
sequenceDiagram
participant A as Agent
participant T as Read 工具
participant C as 上下文窗口
A->>T: tool_use: Read("a.py")
T-->>A: tool_result: 文件完整内容
A->>C: tool_use 写入上下文
A->>C: tool_result 写入上下文
一个中等大小的源码文件可能有几千 token,大文件可能超过一万 token。读几个文件、跑几次测试、grep 几轮结果,上下文很快就会膨胀。
长窗口不能彻底解决问题
模型厂商可以把窗口做大,从 200k token 扩到 1M、2M,确实能延缓问题,但不能从根上解决 Agent 的上下文管理。
主要原因有三点:
| 问题 | 说明 |
|---|---|
| 成本增加 | 输入 token 越多,请求成本越高 |
| 延迟增加 | 上下文越长,首 token 延迟通常越明显 |
| Lost in the Middle | 长上下文中间部分更容易被模型忽略 |
Lost in the Middle 指的是:模型对上下文开头和结尾的信息更敏感,对中间区域的信息关注较弱。即使窗口足够大,把所有历史一股脑塞进去,也不代表模型真的能稳定利用全部信息。
所以,Agent 需要主动管理信息结构,而不是单纯等待更大的上下文窗口。
常见上下文管理方案的问题
在 Claude Code 这种全量重写方案之前,很多 Agent 框架会使用三类办法:滑动窗口、定期摘要、向量召回。它们都有适用场景,但直接用于复杂 Agent 会遇到明显限制。
| 方案 | 做法 | 优点 | 主要问题 |
|---|---|---|---|
| 滑动窗口 | 删除最早的消息,只保留最近 N 轮 | 实现简单、无额外模型调用 | 可能删掉最初目标、约束和关键决策 |
| 定期摘要 | 每隔 N 轮或 N token 做一次摘要 | 比直接删除信息更安全 | 触发机械,重要细节容易被压没 |
| 向量召回 | 把历史切片入库,按相似度召回 | 适合文档问答类 RAG | 会破坏时序,tool use 和 tool result 可能被拆散 |
Agent 上下文有很强的时间顺序和状态依赖。比如“先改 A,再跑测试,然后根据报错改 B”,这不是一堆可独立检索的知识片段,而是一条执行链。
向量召回更适合 RAG(Retrieval-Augmented Generation,检索增强生成)场景:从文档库找相关资料,再放入上下文辅助回答。Agent 的历史消息不只是资料,它还记录了任务状态、尝试路径、错误修复和用户中途修改的要求。只靠相似度召回,很容易漏掉低相似但高价值的信息。
Claude Code 的五层上下文压缩
Claude Code 没有把所有压力都交给 Auto-Compact,而是设计了一套从轻到重的五层机制。越底层成本越低,越上层改动越大。
flowchart BT
L1[第 1 层:大工具结果落盘] --> L2[第 2 层:Snip 删除远古消息]
L2 --> L3[第 3 层:Micro-Compact 时间衰减]
L3 --> L4[第 4 层:Context Collapse 读时投影]
L4 --> L5[第 5 层:Auto-Compact 全量摘要]
| 层级 | 机制 | 核心动作 | 是否需要额外模型调用 |
|---|---|---|---|
| 第 1 层 | 大结果落盘 | 超大 tool result 写入磁盘,只留预览 | 否 |
| 第 2 层 | Snip | 删除较早消息,插入边界标记 | 否 |
| 第 3 层 | Micro-Compact | 清理可重新获取的旧工具结果 | 否 |
| 第 4 层 | Context Collapse | 调 API 时生成压缩视图,不改本地历史 | 否或较低 |
| 第 5 层 | Auto-Compact | 全量重写历史,并恢复关键附件 | 是 |
这套分层设计的关键在于:能用便宜手段解决,就不急着做重压缩。大文件先落盘,旧工具结果先清掉,读时可以投影压缩视图,只有这些办法仍然挡不住窗口膨胀时,才触发 Auto-Compact。
Auto-Compact 的核心思想
Auto-Compact 做三件事:
- 接近窗口上限时触发:不是按固定轮数,而是按 token 上限预留固定缓冲。
- 重写整段历史:不是只压旧消息,也不是保留最近 N 轮,而是把整段对话重新生成结构化摘要。
- 关键状态分通道恢复:文件、任务、计划、记忆、系统配置,不全靠摘要保留。
它的设计重点不是“把文字变短”,而是把不同类型的信息放到不同通道里管理。
flowchart LR
A[压缩前完整消息链] --> B[Micro-Compact 预处理]
B --> C[摘要器生成结构化摘要]
C --> D[清理缓存]
D --> E[生成附件]
E --> F[重组压缩后消息链]
F --> F1[边界标记]
F --> F2[摘要消息]
F --> F3[文件/任务/计划附件]
F --> F4[hook 结果]
语义信息适合摘要,例如用户目标、技术方案、已经完成的步骤、遇到的错误。状态信息不适合只靠摘要,例如最近读过的文件内容、异步子任务状态、当前计划文件。这些内容需要更精确的恢复方式。
什么时候触发 Auto-Compact
Claude Code 使用固定 token 缓冲,而不是百分比阈值。
相关实现可以抽象成这样:
export const AUTOCOMPACT_BUFFER_TOKENS = 13_000
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}
假设模型有效上下文窗口是 200k token,那么 Auto-Compact 的触发阈值大约是:
200000 - 13000 = 187000
也就是说,当当前上下文超过约 187k token,就会进入自动压缩流程。
固定缓冲比“80% 触发”更适合这种场景。摘要任务需要的输出预算并不会随着模型窗口线性增长。窗口从 200k 变成 1M,不代表摘要本身就需要多预留几十万 token。用固定缓冲能让触发行为更可预测。
手动压缩和自动压缩
Claude Code 也提供 /compact 手动入口。手动压缩和自动压缩底层会复用核心压缩能力,但行为不同。
| 模式 | 触发方式 | 是否支持自定义指令 | 是否禁止后续提问 |
|---|---|---|---|
手动 /compact | 用户主动执行 | 支持 | 不强制 |
| Auto-Compact | token 接近上限 | 不支持 | 开启 suppressFollowUpQuestions |
手动压缩时,用户可以告诉摘要器重点保留哪些信息。比如正在调一个认证 bug,可以要求压缩时重点记录认证流程、相关文件和已尝试的修复。
自动压缩通常发生在 Agent 正在执行任务的过程中。如果摘要里生成“请确认下一步做 A 还是 B”这类问题,就会打断任务流。所以自动压缩会开启 suppressFollowUpQuestions,避免摘要器额外抛出需要用户回答的问题。
熔断和递归保护
Auto-Compact 是一次额外模型调用。如果压缩连续失败还不断重试,可能导致费用异常上涨。因此实现里需要熔断机制:连续失败达到阈值后,停止继续自动压缩。
另一个必要保护是递归守卫。压缩过程本身也会调用模型生成摘要,如果这个摘要任务又触发 Auto-Compact,就可能进入循环。
简化后的判断类似这样:
if (querySource === 'session_memory' || querySource === 'compact') {
return false
}
当请求来源是 compact 或 session_memory 时,直接禁止再次触发压缩。这个判断很短,但能避免压缩任务递归压缩自己。
Auto-Compact 压什么、留什么、丢什么
Auto-Compact 最反直觉的地方是:它不保留最近 N 轮原始对话,而是把历史消息整体送入摘要器,生成新的压缩后消息链。
压缩后的结构可以抽象成:
export function buildPostCompactMessages(result: CompactionResult): Message[] {
return [
result.boundaryMarker,
...result.summaryMessages,
...result.attachments,
...result.hookResults,
]
}
四段内容分别承担不同职责:
| 压缩后内容 | 作用 |
|---|---|
| boundary marker | 记录压缩边界、压缩方式、压缩前状态 |
| summary messages | 存放结构化摘要 |
| attachments | 恢复文件、计划、技能、异步任务等精确状态 |
| hook results | 注入压缩过程中 hook 执行结果 |
原始历史消息会被替换掉。压缩后的对话不是“旧消息 + 摘要”,而是“边界 + 摘要 + 附件 + hook 结果”。
Micro-Compact 预处理
Auto-Compact 生成摘要前,会先让历史消息瘦身。工具调用结果往往最占空间,尤其是:
ReadBashGrepGlobWebFetchWebSearchEditWrite
这些工具的结果很多是可重新获取的。比如文件内容可以再读,grep 结果可以再查,命令输出在必要时可以重跑。Micro-Compact 会清理这类可恢复内容,只保留元数据或占位信息。
这样做有两个好处:
- 摘要器不用处理过多低密度工具输出;
- 压缩请求本身更不容易超过上下文限制。
需要注意,Micro-Compact 不会随便清掉不可重复状态。比如异步子任务的输出、任务运行状态等内容,如果丢了就无法恢复,必须走更谨慎的通道。
文件恢复策略:5 个文件、每个 5k、总预算 50k
工具结果被清理后,Agent 仍然需要知道最近正在处理哪些文件。Claude Code 会在压缩后恢复一部分最近活跃文件。
相关常量可以概括为:
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
export const POST_COMPACT_TOKEN_BUDGET = 50_000
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
含义如下:
| 参数 | 含义 |
|---|---|
POST_COMPACT_MAX_FILES_TO_RESTORE | 最多恢复 5 个文件 |
POST_COMPACT_MAX_TOKENS_PER_FILE | 每个文件最多注入 5k token |
POST_COMPACT_TOKEN_BUDGET | 压缩后附件总预算控制在 50k token 内 |
文件选择按最近活跃度排序,最近被 Read 过的文件优先恢复。这样可以把“当前正在改的代码”重新放回模型视野里,而不是完全依赖摘要器用自然语言描述文件内容。
CLAUDE.md 为什么不进摘要
CLAUDE.md 属于项目记忆或用户上下文,它通常记录编码规范、项目约定、长期偏好等信息。这类内容不是某一次对话的临时状态,而是每轮对话都应该自动加载的长期上下文。
Claude Code 不把 CLAUDE.md 塞进压缩后的摘要里,而是清空相关用户上下文缓存,例如 getUserContext。下一轮请求开始时,缓存为空,系统会重新从磁盘加载 CLAUDE.md。
flowchart LR
A[Auto-Compact] --> B[清空 getUserContext 缓存]
B --> C[下一轮请求开始]
C --> D[重新读取 CLAUDE.md]
D --> E[注入新的用户上下文]
这种做法把长期记忆和对话摘要分开管理。长期记忆不占摘要空间,也不会因为摘要器遗漏而失效。
system prompt 和异步任务怎么处理
system prompt 不参与压缩。压缩完成后,会重新构建有效 system prompt,把最新工具列表、权限设置、MCP(Model Context Protocol,模型上下文协议)服务等信息注入进去。
这样做可以避免一个问题:对话中途工具或权限发生变化,压缩后 Agent 仍然拿着旧操作手册工作。
异步任务状态则走附件通道。例如主 Agent 派出子 Agent 去跑测试、查文档或分析代码,压缩时必须保留这些任务的状态:
| 异步任务状态 | 压缩后处理 |
|---|---|
| 正在运行 | 作为附件恢复,主 Agent 继续等待或查询 |
| 已完成 | 注入结果摘要或结果位置 |
| 失败 | 注入错误状态,避免主 Agent 误判任务仍可用 |
这类信息不能只靠自然语言摘要,因为它关系到调度状态。
摘要 prompt 的结构
Auto-Compact 的摘要不是简单一句“请总结对话”。为了让压缩结果可接续,摘要 prompt 会对输出格式和内容清单做强约束。
禁止工具调用
摘要器的任务只是总结历史,不能继续执行工具。prompt 会明确要求只输出纯文本,不允许调用 Read、Bash、Grep、Glob、Edit、Write 等任何工具。
可以抽象成这样的约束:
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- Tool calls will be REJECTED and will waste your only turn.
- Your entire response must be plain text.
这是必要的防呆设计。因为模型在看到历史里出现文件、命令、错误时,可能会尝试继续操作;但摘要阶段一旦调用工具,就偏离了“压缩历史”的目标。
XML 输出结构
摘要输出大致分成两个块:
<analysis>
用于整理哪些信息重要的中间分析区
</analysis>
<summary>
真正进入压缩后消息链的结构化摘要
</summary>
analysis 块用于帮助模型组织信息,最终不会进入压缩后的对话。summary 块才是压缩后保留的内容。
9 个固定摘要章节
摘要必须覆盖 9 类信息:
| 序号 | 章节 | 作用 |
|---|---|---|
| 1 | Primary Request and Intent | 用户主要目标和真实意图 |
| 2 | Key Technical Concepts | 涉及的关键技术概念 |
| 3 | Files and Code Sections | 文件、函数、代码段和修改点 |
| 4 | Errors and fixes | 遇到的错误和对应修复 |
| 5 | Problem Solving | 已解决的问题和推理路径 |
| 6 | All user messages | 所有用户消息 |
| 7 | Pending Tasks | 尚未完成的任务 |
| 8 | Current Work | 当前正在做的具体工作 |
| 9 | Optional Next Step | 合理的下一步 |
其中第 6 项和第 8 项尤其关键。
All user messages 要求枚举所有非工具结果的用户消息。用户可能在对话中途改变需求、加入限制、废弃某个方案,这些信息一旦漏掉,Agent 后续行为就会偏离目标。
Current Work 要求描述当前工作到非常细的颗粒度。不是“正在调试登录功能”,而应该接近:
正在调试 auth.ts 中 refreshToken 函数的 token 续期逻辑;
已经确认 cookie 过期判断存在问题;
下一步准备修改 isExpired 判断并重新运行 auth.test.ts。
压缩后 Agent 接续工作的关键,就是能立刻知道“刚才做到哪一步”。
为什么摘要使用当前对话同一个模型
一种直觉做法是:摘要任务用更便宜的小模型完成。但 Claude Code 选择使用当前对话的同一个模型。
原因有两个:
| 原因 | 说明 |
|---|---|
| 摘要质量更重要 | 摘要丢信息会直接影响后续任务执行 |
| Prompt Cache 可复用 | 同模型更容易复用已有 system prompt、工具描述等缓存前缀 |
Prompt Cache 是大模型 API 常见能力:如果多次请求的前缀相同,服务端可以复用缓存,降低成本和延迟。摘要换成小模型未必真的省钱,因为压缩质量下降后,Agent 可能需要更多轮重新探索上下文。
压缩后的接续流程
Auto-Compact 完成后,需要把新消息链接回会话,让 Agent 不会误以为自己从零开始。
整体流程可以表示为:
flowchart TD
A[当前消息链接近窗口上限] --> B[触发 Auto-Compact]
B --> C[Micro-Compact 清理可恢复工具结果]
C --> D[调用摘要器生成 summary]
D --> E[清空文件状态和用户上下文缓存]
E --> F[并发生成附件]
F --> G[组装 boundary + summary + attachments + hook results]
G --> H[替换旧消息链]
H --> I[下一轮继续执行任务]
压缩后摘要通常会带有类似这样的语义包装:
本会话是从一次因上下文接近上限而压缩的历史会话延续而来。
以下内容概述了之前的目标、进度、文件状态、错误修复和下一步工作。
这句话告诉模型:当前不是新会话,而是接着之前的任务继续做。配合 Current Work 和 Pending Tasks,Agent 才能从压缩点继续向前推进。
旧消息链在会话里通常会被新消息链替换掉。如果启用了 transcript 备份机制,历史可能会写入本地记录文件,供需要时回查;但从当前上下文角度看,旧消息已经不再直接参与后续推理。
这套机制适合借鉴到哪些 Agent
Claude Code 的设计可以抽象成一组通用原则,不只适用于代码 Agent。
| 原则 | 适合解决的问题 |
|---|---|
| 大结果外置 | 日志、文件、搜索结果太大 |
| 可恢复结果清理 | 工具输出可重新读取或重新执行 |
| 语义信息摘要 | 用户目标、技术方案、错误教训需要保留 |
| 精确状态附件化 | 文件内容、任务状态、计划不能被摘要模糊化 |
| 长期记忆独立加载 | 项目规则、用户偏好不该混进对话摘要 |
| 自动流程加熔断 | 防止压缩失败后无限重试 |
| 自动压缩禁止提问 | 避免长任务被压缩流程打断 |
如果要给自己的 Agent 实现上下文管理,可以从一个简化版开始:
flowchart LR
A[统计 token] --> B{是否接近阈值}
B -- 否 --> C[正常调用模型]
B -- 是 --> D[清理可恢复工具结果]
D --> E[生成结构化摘要]
E --> F[恢复最近文件/任务状态]
F --> G[重组消息链]
G --> C
关键不是照搬参数,而是先定义清楚哪些信息必须保留:
| 信息类型 | 推荐处理方式 |
|---|---|
| 用户目标和约束 | 结构化摘要 |
| 中途需求变更 | 枚举保留 |
| 当前正在修改的文件 | 附件恢复 |
| 测试失败原因 | 摘要中单独记录 |
| 子任务状态 | 附件恢复 |
| 项目长期规则 | 独立记忆系统加载 |
| 大文件内容 | 落盘,需要时再读 |
用一句话概括 Claude Code 的上下文管理
Claude Code 的上下文压缩不是“删掉旧内容省 token”,而是把上下文拆成不同生命周期的信息:语义进摘要,状态走附件,长期记忆重新加载,系统能力重新构建,可恢复工具结果清理或落盘。
上下文窗口即使继续变大,这种主动管理仍然必要。窗口变大只能减少触发压缩的频率,不能消除 Lost in the Middle,也不能保证模型稳定利用所有历史细节。对复杂 Agent 来说,真正重要的是让模型始终看到结构清晰、当前有效、可继续执行的任务状态。