06-上下文管理
第 6 篇:上下文管理 — 无限对话的秘密
本篇是《深入 Claude Code 源码》系列的第 6 篇。我们将深入分析 Claude Code 如何在有限的 context window 内支撑无限长度的对话——从 token 预算管理、多级压缩策略,到文件状态缓存与 compact 后恢复的设计。
范围说明:上下文的”构建”(System Prompt 组装、CLAUDE.md 注入、git status 获取)已在第 4 篇中覆盖,缓存策略的横切视角将在第 7 篇中展开。本篇聚焦于上下文构建完成后的压缩、清理与恢复——即当 context window 不够用时,Claude Code 如何在不中断对话的情况下释放空间并重建必要的工作上下文。
为什么上下文管理如此重要?
LLM 的 context window 是有限的。Claude 的模型通常有 200K token 的窗口(部分模型支持 1M),看起来很大,但在实际使用中消耗极快:
- 一次 System Prompt 可能占 5K-15K token
- 用户附带的 CLAUDE.md 文件可能占 3K-10K token
- 每次工具调用的结果(如读取一个 500 行文件)可能占 2K-5K token
- 一次完整的 agentic loop(读文件 → 分析 → 编辑 → 运行测试)可能消耗 30K-50K token
在一个真实的编程会话中,用户可能连续工作数小时,产生上百个工具调用。如果不做任何上下文管理,context window 很快就会耗尽,对话将被迫中断。
Claude Code 的解决方案是一套多层次的上下文压缩与恢复体系——从最轻量的 Microcompact(清理工具结果)到最重量级的 Full Compact(用模型总结整个对话),形成了一个完整的上下文压力梯度响应系统。
一、Token 预算管理:三个关键函数
上下文管理的基础是精确的 token 预算计算。三个核心函数定义了整个系统的运行边界。
1 | graph LR |
1.1 getEffectiveContextWindowSize() — 实际可用空间
文件:services/compact/autoCompact.ts:33-49
1 | const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 |
这个函数计算的是实际可用于输入的 token 空间。关键逻辑:
- 从模型的 context window(如 200K)中减去输出预留(
MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000) - 20K 的预留值来源于 p99.99 统计:compact 总结输出的最大值为 17,387 token
以一个 200K context window 的模型为例:effectiveContextWindow = 200,000 - 20,000 = 180,000
1.2 getAutoCompactThreshold() — 自动压缩触发线
文件:services/compact/autoCompact.ts:72-91
1 | export const AUTOCOMPACT_BUFFER_TOKENS = 13_000 |
Auto-compact 触发线 = 有效窗口 - 13K 缓冲。以 200K 模型为例:167,000 = 180,000 - 13,000。
这个 13K 缓冲区是刻意留出的——它确保在检测到需要 compact 后,仍有足够空间完成当前 turn 的工具调用和模型响应。
1.3 calculateTokenWarningState() — 四级告警体系
文件:services/compact/autoCompact.ts:93-145
1 | export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 |
以 200K 模型为例的四级告警(auto-compact 启用时):
| 级别 | 阈值 | Token 值 | 含义 |
|---|---|---|---|
| Warning | threshold - 20K | ~147K | UI 显示黄色警告 |
| Error | threshold - 20K | ~147K | UI 显示红色警告 |
| AutoCompact | threshold | ~167K | 触发自动压缩 |
| Blocking | effective - 3K | ~177K | 禁止新查询,必须手动 /compact |
值得注意的是,Warning 和 Error 阈值在当前源码中完全相同(都是 20K 缓冲),这意味着两者总是同时触发。在 TokenWarning.tsx 中,UI 使用 isAboveErrorThreshold ? "error" : "warning" 判断颜色,但由于两个阈值相等,实际上总是直接显示红色(error)。分开定义这两个常量是为未来独立调整留下了扩展空间。
二、Microcompact — 最轻量的上下文清理
在 auto-compact 触发之前,系统会先尝试更轻量的 Microcompact。Microcompact 不会调用模型来总结对话,而是直接清理旧的工具调用结果来释放空间。
2.1 设计思路
工具调用结果(如文件内容、命令输出、搜索结果)在刚返回时对模型理解上下文至关重要,但随着对话推进,它们的价值递减——模型已经”消化”了这些信息并做出了决策。Microcompact 的策略就是保留最近的 N 个工具结果,清理更早的。
2.2 可清理的工具类型
文件:services/compact/microCompact.ts:41-50
1 | const COMPACTABLE_TOOLS = new Set<string>([ |
这些是允许清理其历史结果的高占用工具。列表中包含读取类(FileRead、Grep、Glob)、Shell 命令、网络请求,也包含写入类(FileEdit、FileWrite)——它们的 tool_result 往往包含大量操作反馈文本,在后续 turn 中信息价值递减。注意 AgentTool 等工具的结果不在此列——它们的输出通常是高度浓缩的总结,清理后信息损失大。
2.3 本地 Microcompact 的两条路径
microcompactMessages() 函数(services/compact/microCompact.ts:253-293)内部有两条本地路径,按优先级短路选择:
路径 1:Time-based Microcompact
文件:services/compact/microCompact.ts:446-530
当用户离开一段时间后回来(gap > 阈值),服务端的 prompt cache 已经过期,此时无需保护缓存,直接构造新的消息对象替换旧的工具结果内容:
1 | function maybeTimeBasedMicrocompact( |
这种方式通过 { ...block, content: ... } 和 { ...message, message: { ...message.message, content: newContent } } 构造新的 Message / content 对象返回,而非原地修改(mutate)原始消息。因为 cache 已经冷了,没有需要保护的前缀,所以可以放心地替换内容。
路径 2:Cached Microcompact(cache_edits API)
文件:services/compact/microCompact.ts:305-399
当 prompt cache 仍然温热时,不能替换消息内容(那会改变 prompt 前缀,破坏缓存命中),而是通过 Anthropic API 的 cache_edits 机制,在 API 层面删除指定工具的 tool_result:
1 | async function cachedMicrocompactPath( |
关键区别:这种路径不修改本地消息。删除操作通过 API 的 cache_edits 参数传递,由服务端在缓存层执行。这保留了 prompt cache 的命中率。
2.4 API-level Context Management — 独立的并行机制
文件:services/compact/apiMicrocompact.ts
除了上述两条本地 microcompact 路径外,还有一套独立的 API 层上下文管理机制。它不在 microcompactMessages() 的选择链中,而是通过 services/api/claude.ts 在构建 API 请求时注入 context_management 配置参数,将上下文清理策略声明式地委托给 Anthropic API 服务端执行:
1 | // services/api/claude.ts:1633 — 在 API 请求构建时调用 |
getAPIContextManagement() 返回一组声明式策略:
1 | export function getAPIContextManagement(options?: { |
与本地 microcompact 的关系:这两套机制并行存在,互不排斥。本地 microcompact 在客户端发送请求前清理或标记消息;API-level context management 则是在请求参数中声明清理策略,由服务端在处理请求时执行。它们可以同时生效——客户端做一轮粗清理,服务端再做一轮精细清理。
三、Full Compact — 用模型总结对话
当 Microcompact 不够用,token 用量达到 auto-compact 阈值时,系统触发 Full Compact——用模型自己来总结之前的对话。
3.1 触发流程
文件:services/compact/autoCompact.ts:241-351
1 | export async function autoCompactIfNeeded( |
几个重要的工程细节:
熔断器(Circuit Breaker):连续失败 3 次后停止尝试。注释揭示了这个设计的背景:
BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272) in a single session, wasting ~250K API calls/day globally.
没有熔断器时,context 不可恢复地超限的 session 会在每个 turn 都发起注定失败的 compact 请求,全球每天浪费约 25 万次 API 调用。
递归保护:shouldAutoCompact() 会检查 querySource,防止 compact 自身('compact')或 session memory('session_memory')触发新的 compact,避免无限递归。
3.2 Session Memory Compact — 免调用的压缩
文件:services/compact/sessionMemoryCompact.ts
这是 compact 的一个创新路径:不调用模型生成总结,而是直接使用 Session Memory 系统已有的对话记忆作为压缩后的总结。
Session Memory 是一个独立的后台系统(将在第 23 篇详述),它在对话过程中持续异步提取关键信息到磁盘文件。当 compact 触发时,如果 Session Memory 已经有内容,就直接用它作为总结,跳过昂贵的 API 调用:
1 | export async function trySessionMemoryCompaction( |
calculateMessagesToKeepIndex 使用一组可配置的阈值决定保留多少最近消息:
- 最少保留 10K token 或 5 条有文本的消息(取较大者)
- 最多保留 40K token(硬上限)
- 还会确保不拆分
tool_use/tool_result配对
3.3 传统 Full Compact — compactConversation()
文件:services/compact/compact.ts:387-586
当 Session Memory Compact 不可用时,执行传统的 Full Compact:
- 执行 PreCompact hooks——允许用户自定义的 hook 脚本在 compact 前运行
- 构建总结请求——将对话历史和总结 prompt 发给模型
- 流式获取总结——模型生成对话总结
- 处理 prompt_too_long——如果连 compact 请求本身都超限,会截断最旧的消息重试
- 重建上下文——清理文件缓存,重新注入关键附件
3.4 Compact Prompt — 模型如何被指导生成总结
文件:services/compact/prompt.ts
Compact prompt 的设计非常讲究。它要求模型按 9 个维度进行结构化总结:
1 | 1. Primary Request and Intent — 用户的请求和意图 |
一个精妙的设计是 <analysis> 标签——模型被要求先在 <analysis> 中整理思路,然后在 <summary> 中给出正式总结。之后,formatCompactSummary() 函数会剥离 <analysis> 部分,只保留 <summary> 注入到后续上下文中:
1 | // services/compact/prompt.ts:311-335 |
这实质上是一种 chain-of-thought 然后剥离 的技巧:让模型在生成最终总结前先深入思考,但不把思考过程注入后续上下文(节省 token)。
另一个值得注意的设计:prompt 开头有一段强力的 NO_TOOLS_PREAMBLE,反复强调模型不要调用工具:
1 | CRITICAL: Respond with TEXT ONLY. Do NOT call any tools. |
注释解释了原因:compact 使用 maxTurns: 1 的 forked agent 执行,模型如果尝试调用工具会被拒绝,导致白白浪费 API 调用。在 Sonnet 4.6 上这个问题的发生率从 0.01% 上升到了 2.79%,所以增加了这个前置强调。
3.5 Compact 后的上下文重建
Compact 不仅仅是压缩——压缩完后需要重建模型继续工作所需的上下文:
1 | // compact.ts:531-585 (简化) |
文件恢复有严格的 token 预算控制:POST_COMPACT_TOKEN_BUDGET = 50_000,POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000,POST_COMPACT_MAX_FILES_TO_RESTORE = 5。这确保 compact 后的上下文不会因为恢复附件而再次膨胀。
四、FileStateCache — 文件读写安全状态追踪
文件:utils/fileStateCache.ts
FileStateCache 的核心职责不是”避免重复读取”,而是追踪文件的读取状态与可编辑性,并在 compact 后作为文件恢复的索引依据。它回答的核心问题是:模型当前对哪些文件有什么程度的认知?是否可以安全地编辑它们?
1 | export type FileState = { |
1 | export class FileStateCache { |
几个设计要点:
读写安全守卫:
isPartialView标记是这个缓存最关键的语义。当文件内容是被截断注入的(如 CLAUDE.md 去掉 HTML 注释、截断的 MEMORY.md),这个标记告诉 FileEdit/FileWrite 工具必须先做一次完整的 Read,不能基于缓存的部分内容进行编辑。源码注释明确说明:content字段存储的是原始磁盘内容(用于getChangedFiles差异比对),不是模型看到的内容。Compact 后文件恢复索引:Full Compact 后,
readFileState.clear()清除所有缓存,然后createPostCompactFileAttachments()(services/compact/compact.ts:1415-1464)从 compact 前的preCompactReadFileState中挑选最近的文件重新注入。FileStateCache 在这里充当了”恢复索引”——它知道哪些文件是模型最近读取过的,按时间戳排序后选择最重要的恢复。双重限制:
max限制条目数(默认 100),maxSize限制总大小(默认 25MB)。这防止大量大文件导致内存膨胀。路径标准化:所有 key 在存取时都经过
normalize(),确保/foo/../bar和/bar命中同一条缓存。Agent 隔离:
createSubagentContext()会cloneFileStateCache(),确保子 Agent 的文件读取不污染父级的缓存状态。
五、compactWarningState — 用 Store 模式管理告警状态
文件:services/compact/compactWarningState.ts
这是一个有趣的小模块——它复用了第 3 篇介绍的极简 Store 来管理 compact 告警的抑制状态:
1 | import { createStore } from '../../state/store.js' |
为什么需要抑制?因为 compact 成功后,我们不再有准确的 token 计数(要等下次 API 响应才知道),如果继续显示告警会造成误导。所以 compact 成功后设置抑制,直到下次 microcompact 开始时清除。
这个模块同时展示了 Store 模式的复用性——同一个 35 行 Store 实现,既用于管理全局 AppState,也用于管理局部的 UI 状态。
六、postCompactCleanup — 压缩后的缓存清理
文件:services/compact/postCompactCleanup.ts
Compact 后需要清理大量缓存和追踪状态。这个清理函数揭示了 Claude Code 中有多少模块级缓存:
1 | export function runPostCompactCleanup(querySource?: QuerySource): void { |
isMainThreadCompact 的判断尤其重要——子 Agent 和主线程运行在同一个进程中,共享模块级状态。如果子 Agent compact 时清理了主线程的缓存(如 getUserContext 缓存),会导致主线程状态损坏。注释中记录了这个教训。
七、可迁移的设计模式
模式 1:多级压力梯度响应
不要等到资源耗尽才采取行动。设计一组从轻量到重量级的干预措施,在不同的压力等级触发不同的响应:
- 低压:Time-based 清理(几乎零成本)
- 中压:Cached Microcompact(利用 API 原生能力)
- 高压:Full Compact(用模型总结,昂贵但有效)
- 极限:Blocking(禁止新请求,强制用户手动干预)
适用场景:任何有资源限制的系统——内存管理、数据库连接池、磁盘空间、API 配额。
模式 2:熔断器 + 连续失败计数
当某个操作连续失败超过阈值时,停止重试。这避免了”在已知无法成功的操作上浪费资源”的陷阱。
1 | 失败 → 计数器 +1 → 超过阈值 → 熔断(停止尝试) |
Claude Code 的实践:3 次连续 compact 失败后熔断,因为统计显示有些 session 会连续失败数千次,每天浪费 25 万次 API 调用。
适用场景:任何可能连续失败的重试逻辑——API 调用、数据库重连、任务队列。
模式 3:Chain-of-Thought 然后剥离
让模型在生成最终输出前先在指定标签(如 <analysis>)中进行详细推理,然后在后处理中剥离推理过程,只保留最终结果(<summary>)。这兼顾了输出质量和 token 经济性。
适用场景:任何需要模型生成高质量摘要/总结的场景,特别是当总结会被注入后续 prompt 时。
下一篇预告
第 7 篇:Prompt Cache — 跨模块的缓存策略如何降低 API 成本
我们将从横切视角审视 Prompt Cache 如何贯穿 System Prompt、对话循环和上下文管理三个模块。你会看到 CacheSafeParams 如何让 fork agent 与 parent 共享缓存、saveCacheSafeParams() 如何实现跨 turn 复用,以及 fork subagent 的 byte-exact prompt threading 极致优化。
全部内容请关注 https://github.com/luyao618/Claude-Code-Source-Study (求一颗免费的小星星)