从上下文窗口确定到多级压缩策略的完整机制
一、整体架构
Claude Code 的上下文管理是分层策略驱动的,在每次调用大模型 API 之前,按固定顺序执行一系列压缩/裁剪操作:
css
queryLoop 每次迭代的上下文处理管线(query.ts:360-550):
1. applyToolResultBudget() ← 单条消息内工具结果总大小预算
2. snipCompactIfNeeded() ← HISTORY_SNIP:从中间删除消息
3. microcompactMessages() ← 缓存编辑 / 基于时间的工具结果清除
4. applyCollapsesIfNeeded() ← CONTEXT_COLLAPSE:提交日志投影折叠
5. autoCompactIfNeeded() ← 全对话摘要压缩(或会话内存压缩)
6. 阻塞限制检查 ← 禁用自动压缩时阻止请求
│
▼
buildSystemPromptBlocks() ← 分块系统提示 + cache_control
addCacheBreakpoints() ← 消息级缓存断点 + cache_edits
│
▼
调用 API(queryModel)
│
▼
API 返回 prompt_too_long? ← REACTIVE_COMPACT:反应式压缩后重试
二、上下文窗口大小确定
2.1 基础默认值
文件 : src/utils/context.ts
| 常量 | 值 | 用途 |
|---|---|---|
MODEL_CONTEXT_WINDOW_DEFAULT |
200,000 | 全局默认上下文窗口 |
COMPACT_MAX_OUTPUT_TOKENS |
20,000 | 压缩操作的最大输出 token |
MAX_OUTPUT_TOKENS_DEFAULT |
32,000 | 默认最大输出 token |
MAX_OUTPUT_TOKENS_UPPER_LIMIT |
64,000 | 输出 token 上限 |
CAPPED_DEFAULT_MAX_TOKENS |
8,000 | 预留优化的默认上限 |
ESCALATED_MAX_TOKENS |
64,000 | token 上限提升后的值 |
2.2 窗口大小解析逻辑
getContextWindowForModel(model, betas) 按优先级确定窗口大小:
ts
// 1. 环境变量覆盖(仅 ant)
CLAUDE_CODE_MAX_CONTEXT_TOKENS → 直接返回
// 2. [1m] 后缀显式 opt-in
model === 'claude-sonnet-4-20250514[1m]' → 1_000_000
// 3. 模型能力表
getModelCapability(model).max_input_tokens ≥ 100K → 取 cap
// 4. 默认
→ MODEL_CONTEXT_WINDOW_DEFAULT (200K)
1M 上下文支持受 CLAUDE_CODE_DISABLE_1M_CONTEXT 环境变量控制(HIPAA 合规),当前 Sonnet 4 和 Opus 4.6 支持。
2.3 有效上下文窗口
getEffectiveContextWindowSize(model) 从上下文窗口中扣除压缩摘要的输出预留:
ts
// 从上下文窗口减去最多 20K 的摘要输出 token
有效窗口 = 上下文窗口 - min(模型最大输出, 20_000)
同时 CLAUDE_CODE_AUTO_COMPACT_WINDOW 环境变量可进一步限制自动压缩感知的窗口大小。
三、消息汇编与缓存管理
3.1 系统提示构建
文件 : src/services/api/claude.ts:buildSystemPromptBlocks
系统提示被分割为多个 block,每个 block 可附加 cache_control 标记。关键点:
- 全局缓存范围 :通过
splitSysPromptPrefix支持可选的全局缓存前缀(依赖split_system_promptbeta) - 分块策略:将 system prompt 拆分为多个 block,使缓存前缀尽可能长
- 系统提示包含:核心指令 + 用户上下文(CLAUDE.md) + 系统上下文(git 状态)
3.2 查询上下文获取
文件 : src/utils/queryContext.ts
ts
fetchSystemPromptParts() → 并行获取三个部分:
1. 系统提示部分 ← 核心 prompt 模板
2. 用户上下文部分 ← CLAUDE.md 文件内容 + 当前日期
3. 系统上下文部分 ← git 分支、状态、最近提交
这三个部分在会话期间被记忆化 (src/context.ts),构成 API 缓存键前缀的基础。只要这三部分不变,后续请求可以复用缓存前缀。
3.3 消息标准化
在构建 API 请求前,对消息数组进行一系列修复:
scss
normalizeMessagesForAPI()
├─ ensureToolResultPairing() ← 修复备用/远程会话中不匹配的 tool_use/tool_result 对
├─ stripAdvisorBlocks() ← 移除不需要的 advisor 内容
└─ stripExcessMediaItems() ← 超过 100 个媒体项时移除最旧的
3.4 缓存断点
文件 : src/services/api/claude.ts:3063-3200
addCacheBreakpoints() 在消息数组上精确放置一个 cache_control 标记:
ts
// 最关键的设计:每条请求只有一个消息级 cache_control 标记
const markerIndex = skipCacheWrite ? messages.length - 2 : messages.length - 1
为什么只有一个标记? Mycro(API 的 KV 缓存管理器)的逐出策略只保护最后标记位置的 KV 页。如果有两个标记,第二个到最后一个位置也会被保护,浪费缓存空间。
Cache_edits 支持(配合缓存的微压缩):
当使用缓存的微压缩(CACHED_MICROCOMPACT)时,在消息中插入 cache_edits 块,这些块告诉 API 服务器在缓存中删除特定 tool_use_id 的内容,而无需发送完整消息体。同时:
cache_reference被添加到缓存前缀内的tool_result块(避免 API 重新编码这些块的令牌)- 已固定的 edits 在后续请求中保持在同一位置(
pinnedEdits) - 新增 edits 只会插入到最后一个用户消息中
3.5 思考配置
支持两种思考模式:
- 自适应思考 (新模型)--- 由
thinking_config控制 - 预算驱动思考 (旧模型)---
getMaxThinkingTokensForModel提供上限
3.6 Task Budget(任务预算)
如果用户指定了 token 预算,通过 configureTaskBudgetParams 注入 output_config.task_budget 字段,使模型能够自我调节输出长度。
四、多级压缩策略
这是上下文管理的核心。各策略按优先级从低到高(实际执行顺序从前往后)排列。
4.0 执行顺序
在 query.ts:360-550 的 queryLoop 中,每次迭代按以下顺序执行:
序号 策略 触发条件 效果
─── ───────────────────── ────────────────────────── ─────────────────
1 工具结果预算 单条用户消息内结果总大小超限 将最大结果持久化到磁盘,替换为预览
2 HISTORY_SNIP feature flag + 需要时 从消息数组中间删除消息
3 微压缩(时间/缓存) 每轮运行 清除旧工具结果 / 通过 API cache_edits 删除
4 上下文折叠 feature flag + 需要时 提交日志投影,分段折叠
5 自动压缩 / 会话内存压缩 token 超过阈值 全对话摘要(或会话内存替换)
6 反应式压缩 API 返回 prompt_too_long 打薄上下文后重试
4.1 工具结果预算(Tool Result Budget)
文件 : src/utils/toolResultStorage.ts
由 tengu_hawthorn_steeple GrowthBook 标志控制。
每个工具结果的阈值(第 55-78 行):
ts
getPersistenceThreshold(toolName, maxResultSizeChars)
// 1. 如果工具声明了 Infinity(如 Read 工具)→ 硬 opt-out,不持久化
// 2. GrowthBook 覆盖(tengu_satin_quoll)→ 按工具名独立配置
// 3. 默认取 min(声明的上限, DEFAULT_MAX_RESULT_SIZE_CHARS)
当单个工具结果超过阈值时:
- 内容被写入
<sessionDir>/tool-results/<toolUseId>.{json,txt} - 原始内容被替换为
<persisted-output>预览标签 - 预览大小固定为
PREVIEW_SIZE_BYTES = 2000
每条消息的聚合预算(第 369+ 行):
ts
enforceToolResultBudget(messages, state, skipToolNames)
// 如果单条用户消息中所有工具结果的总大小超过 MAX_TOOL_RESULTS_PER_MESSAGE_CHARS
// 最大的结果被持久化到磁盘,用预览替换
通过 ContentReplacementState 跨轮次跟踪,确保决策稳定(提示缓存稳定性):
seenIds--- 已经做过预算检查的结果replacements--- 确切预览字符串的映射
与微压缩的关系 :工具结果预算在微压缩之前 运行(第 369 行注释)。由于缓存的微压缩仅按 tool_use_id 操作(从不检查内容),内容替换对它不可见,两者可以干净地组合。
4.2 HISTORY_SNIP
文件 : src/query.ts:400-410
由 HISTORY_SNIP feature flag 控制。在微压缩之前运行,从消息数组中间删除 消息(不是摘要,就是直接删除)。snipTokensFreed 传递给自动压缩,使其阈值计算反映已删除的内容。
ts
if (feature('HISTORY_SNIP')) {
const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
snipTokensFreed = snipResult.tokensFreed
}
4.3 微压缩(MicroCompact)
文件 : src/services/compact/microCompact.ts
每轮运行,包含两条路径:
4.3.1 基于时间的微压缩
受 GrowthBook 的 tengu_slate_heron(TimeBasedMCConfig)控制:
ts
type TimeBasedMCConfig = {
enabled: boolean
gapThresholdMinutes: number // 默认 60 分钟
keepRecent: number // 保留最近几条工具结果不清除
}
当自上次助手消息以来的时间间隔超过阈值时,服务器缓存几乎肯定会过期,因此清除旧工具结果以减小需要重写的 payload 大小。
清除的内容替换为:'[Old tool result content cleared]'
4.3.2 缓存编辑微压缩(Cached MicroCompact)
由 CACHED_MICROCOMPACT feature flag 控制。使用 API 的 cache_edits 功能,在不使缓存前缀失效的情况下移除旧工具结果。这是 ant-only 的高级功能。
工作流程:
- 在消息数组中标记需要删除的
tool_use_id - 构建
cache_edits块(包含{ type: 'delete', cache_reference: tool_use_id }) - 在 API 请求中插入这些 edits,服务器端从缓存中删除对应内容
- 维护模块级
CachedMCState,跨轮次跟踪已应用的 edits
ts
// 可压缩的工具白名单
const COMPACTABLE_TOOLS = new Set([
FILE_READ_TOOL_NAME, SHELL_TOOL_NAMES, GREP_TOOL_NAME,
GLOB_TOOL_NAME, WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME,
FILE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME,
])
每种消息类型的估算用 estimateMessageTokens(messages) 做客户端快速检查(使用 4/3 填充因子确保保守)。
如果想详细了解以上两种微压缩的细节和区别,可参考章节:Microcompact(微压缩)机制分析
4.4 上下文折叠(Context Collapse)
文件 : src/query.ts:440-447
由 CONTEXT_COLLAPSE feature flag 控制。在自动压缩检查之前运行,提供比完整压缩更细粒度的上下文管理。
核心设计:
- 不替换消息 ,而是维护一个"提交日志",对对话各部分做只读投影
- 摘要消息存在于折叠存储中,而不是 REPL 的消息数组
- 每次进入查询时,
projectView()重放提交日志,在读取时构建当前投影 - 跨轮次持久化(提交日志不变),下一轮的
projectView()自动反映最新状态
ini
没有上下文折叠的情况:
消息: [A][B][C][D][E][F][G][H] ← 整个数组进入 API
有上下文折叠的情况:
提交日志: [collapse(D, E)→S1][collapse(F, G)→S2]
投影视图: [A][B][C][S1][S2][H] ← 只在读取时投影,不修改原始数组
从溢出中恢复 (recoverFromOverflow):在反应式压缩接管之前,从暂存的折叠中排空。
Context Collapse 上下文折叠更细节的介绍,请参考章节:Context Collapse(消息折叠)机制分析
4.5 自动压缩(AutoCompact)
文件 : src/services/compact/autoCompact.ts
当上下文 token 超过阈值时触发。这是主要的"大锤"压缩机制。
触发阈值
ts
const AUTOCOMPACT_BUFFER_TOKENS = 13_000
getAutoCompactThreshold(model) {
effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - 13_000 // 预留 13K 缓冲区
}
同时支持环境变量 CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 覆盖为百分比。
Token 警告状态
ts
const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000
const MANUAL_COMPACT_BUFFER_TOKENS = 3_000
calculateTokenWarningState(tokenUsage, model) → {
percentLeft, // 剩余百分比
isAboveWarningThreshold, // token >= 阈值 - 20K
isAboveErrorThreshold, // token >= 阈值 - 20K
isAboveAutoCompactThreshold, // token >= 自动压缩阈值
isAtBlockingLimit, // 禁用自动压缩时的阻塞限制
}
断路器模式
ts
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
// 连续 3 次失败后停止重试,避免浪费 API 调用
尝试顺序
autoCompactIfNeeded() 的尝试顺序:
scss
1. trySessionMemoryCompaction() ← 实验性:用会话内存代替摘要
├─ 检查 feature flag (tengu_session_memory + tengu_sm_compact)
├─ 等待正在进行的记忆提取完成
└─ 如果可用 → 用结构化记忆替换对话,跳过完整压缩
2. compactConversation() ← 传统全对话摘要
├─ 执行预处理 hooks
├─ 发送整个对话给模型做摘要(9 部分结构化分析+摘要)
└─ 执行后处理清理
后压缩清理
compact.ts:buildPostCompactMessages 之后运行 runPostCompactCleanup:
- 文件附件恢复:最多恢复 5 个文件,总额度 50K token,每个文件最多 5K token
- 技能内容附加:每个技能最多 5K token,总额度 25K token
- 为计划模式、技能、代理、MCP 工具增量创建附件
- 调用
resetContextCollapse()重置上下文折叠状态
部分压缩
partialCompactConversation(allMessages, pivotIndex) 支持"从/向上到"方向的精确压缩:
- 保留选择的对话前缀或后缀
- 同时摘要性地总结另一部分
- 用于子代理分叉时的上下文构建
如需了解更详细的自动压缩机制,可阅读:Autocompact(自动压缩)机制分析
4.6 反应式压缩(Reactive Compact)
文件 : src/query.ts:1062-1182
当 API 调用返回 prompt_too_long 或媒体大小错误时,在流循环之后 触发。由 REACTIVE_COMPACT feature flag 控制,开源的代码中并没有相关的代码,故此出不做具体的逻辑分析。
五、Token 预算管理
5.1 用户指定的 token 预算
文件 : src/utils/tokenBudget.ts
用户可以通过输入指定 token 预算:
+500k--- 缩写语法行尾 <text> +500k--- 行尾语法use/spend X tokens--- 详细语法
5.2 自动继续逻辑
文件 : src/query/tokenBudget.ts
checkTokenBudget(tracker, agentId, budget, globalTurnTokens) 将跟踪器与预算阈值(默认 90%)比较:
ts
// 当已使用超过 90% 的预算时触发
if (currentTokenUsage / budget.total >= 0.9) {
// 收益递减检测:自上次检查以来 delta < 500 且已使用 ≥3 次继续
// 如果收益递减 → 停止自动继续
// 否则 → 生成继续提示消息
}
getBudgetContinuationMessage(pct, turnTokens, budget) 生成继续提示,告诉模型已使用预算的百分比,要求它尽快收尾。
5.3 Token 计数与估算
文件 : src/utils/tokens.ts
| 函数 | 用途 |
|---|---|
getTokenUsage(message) |
提取消息的真实 usage(排除合成消息) |
getTokenCountFromUsage(usage) |
计算总上下文 token(input + cache + output) |
tokenCountFromLastAPIResponse(messages) |
从最后一条 API 响应获取 token 数 |
finalContextTokensFromLastResponse(messages) |
服务器端 iterations[-1] 的 token 数(用于 task_budget 跨压缩边界的剩余计算) |
doesMostRecentAssistantMessageExceed200k(messages) |
检查最近助手消息是否超过 200K(用于计划模式模型切换) |
文件 : src/services/tokenEstimation.ts
| 函数 | 用途 |
|---|---|
countTokensWithAPI(content) |
实际 API 调用计数 |
roughTokenCountEstimation(content, bytesPerToken=4) |
简单字符/4 估算 |
roughTokenCountEstimationForMessages(messages) |
跨消息数组估算 |
countTokensViaHaikuFallback(messages, tools) |
回退到 Haiku API 计数 |
bytesPerTokenForFileType(extension) |
JSON 文件用 2 而非 4 |
5.4 轮次级 Token 跟踪
文件 : src/bootstrap/state.ts:724-743
ts
snapshotOutputTokensForTurn(budget) // 轮次开始时快照
getTurnOutputTokens() // 当前轮次已生成的 token 总数
getCurrentTurnTokenBudget() // 当前轮次的用户指定预算
incrementBudgetContinuationCount() // 继续次数 +1
六、API 级上下文管理(服务器端)
文件 : src/services/compact/apiMicrocompact.ts
ant-only 功能,通过 context_management 参数让 API 服务器在服务器端透明地管理上下文。
两种策略:
-
clear_thinking_20251015--- 清除旧思考块- 如设置了
clearAllThinking,至少保留 1 个思考轮次 - 否则保留所有
- 如设置了
-
clear_tool_uses_20250919--- 清除旧工具结果- 当超过
DEFAULT_MAX_INPUT_TOKENS(180K)阈值时触发 - 目标是将输入降到
DEFAULT_TARGET_INPUT_TOKENS(40K)
- 当超过
这些通过 API 请求的 context_management.edits 数组传递。
七、主查询循环编排
7.1 完整流程时序
文件 : src/query.ts:360-1200
css
queryLoop 每次迭代:
┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: 工具结果预算 │
│ applyToolResultBudget(messagesForQuery) │
│ → 持久化超大的工具结果,用预览替换 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ STEP 2: HISTORY_SNIP │
│ snipCompactIfNeeded(messagesForQuery) │
│ → 从消息数组中间删除消息 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ STEP 3: 微压缩 │
│ microcompactMessages(messagesForQuery) │
│ → 基于时间或缓存编辑清除旧工具结果 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ STEP 4: 上下文折叠 │
│ applyCollapsesIfNeeded(messagesForQuery) │
│ → 提交日志投影,分段折叠中间消息 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ STEP 5: 自动压缩 │
│ autoCompactIfNeeded(messagesForQuery) │
│ → token > 阈值时,对旧消息做摘要压缩 │
│ ├─ 先尝试会话内存压缩 │
│ └─ 失败则回退到传统全对话压缩 │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ STEP 6: 构建系统提示 + 缓存断点 │
│ buildSystemPromptBlocks() + addCacheBreakpoints() │
│ → 分块系统提示、放置 cache_control 标记、注入 cache_edits │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ STEP 7: 调用 API │
│ queryModel() → SSE 流式响应 │
│ → 如果返回 prompt_too_long → REACTIVE_COMPACT │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ STEP 8: 工具执行 + 继续判定 │
│ collectToolResults() → runTools() │
│ → 有 tool_use 且 budget 未耗尽 → 回到 STEP 1 │
│ → 否则 return │
└─────────────────────────────────────────────────────────────────┘
7.2 压缩与计数的交互
关键的跨压缩边界 token 计算:
ts
// task_budget 支持:在 messagesForQuery 被替换为 postCompactMessages 之前
// 捕获压缩前的最终上下文窗口
if (params.taskBudget) {
const preCompactContext = finalContextTokensFromLastResponse(messagesForQuery)
taskBudgetRemaining = Math.max(
0, (taskBudgetRemaining ?? params.taskBudget.total) - preCompactContext,
)
}
每次压缩后重置轮次计数器:
ts
tracking = {
compacted: true,
turnId: deps.uuid(), // 新 ID
turnCounter: 0, // 归零
consecutiveFailures: 0, // 清零断路器
}
八、对比总结
8.1 各压缩策略对比
| 策略 | 粒度 | 是否改原始消息 | 是否调 API | 用户可见 | 主要场景 |
|---|---|---|---|---|---|
| 工具结果预算 | 每个结果 | 否(持久化) | 否 | 是(预览标签) | 超大工具输出 |
| HISTORY_SNIP | 整条消息 | 是(删除) | 否 | 是(边界消息) | 中间冗余消息 |
| 微压缩(时间) | 工具结果 | 是(清空内容) | 否 | 是(占位文本) | 长时间间隔 |
| 微压缩(缓存) | 工具结果 | 否 | 否(cache_edits) | 否 | 每轮自动 |
| 上下文折叠 | 消息段 | 否(只读投影) | 否 | 否 | 细粒度管理 |
| 自动压缩 | 全对话 | 是(替换为摘要) | 是 | 是(压缩边界) | token 超阈值 |
| 会话内存压缩 | 全对话 | 是(记忆替换) | 否 | 是 | 替代摘要压缩 |
| 反应式压缩 | 全对话 | 是(打薄) | 是 | 是(恢复) | PTL 错误恢复 |
8.2 设计原则
-
从轻到重:先尝试无 API 调用的轻量级策略(工具结果预算、微压缩),最后才使用需要 API 调用的完整压缩
-
渐进式施压:每个策略只做"足够"的事情------微压缩清除旧工具结果就够了就不触发完整压缩
-
缓存优先:尽可能让压缩不影响提示缓存(缓存编辑微压缩、工具结果预算、上下文折叠都不破坏缓存前缀)
-
断路器保护:自动压缩有连续失败 3 次的断路器,防止不可恢复状态下浪费 API 调用
-
跨轮次稳定性 :
ContentReplacementState、CachedMCState、RecompactionInfo等状态对象确保压缩决策不来回抖动,维持缓存稳定
九、相关文件清单
| 文件 | 角色 |
|---|---|
src/query.ts |
主查询循环,编排所有上下文管理步骤 |
src/utils/context.ts |
上下文窗口大小解析,max tokens 常量 |
src/utils/tokens.ts |
Token 计数、提取、跨压缩边界的预算跟踪 |
src/context.ts |
用户/系统上下文生成(记忆化,会话级缓存) |
src/utils/queryContext.ts |
缓存安全参数装配 |
src/utils/tokenBudget.ts |
用户指定 token 预算解析 |
src/query/tokenBudget.ts |
Token 预算继续决策逻辑 |
src/utils/toolResultStorage.ts |
工具结果持久化与聚合预算 |
src/services/compact/autoCompact.ts |
自动压缩触发器逻辑 |
src/services/compact/compact.ts |
全对话摘要压缩 |
src/services/compact/microCompact.ts |
每轮轻量级微压缩 |
src/services/compact/apiMicrocompact.ts |
API 级服务器端上下文管理 |
src/services/compact/sessionMemoryCompact.ts |
基于会话记忆的压缩 |
src/services/compact/cachedMicrocompact.ts |
缓存编辑微压缩(ant-only) |
src/services/compact/grouping.ts |
API 轮分组 |
src/services/compact/prompt.ts |
压缩提示模板 |
src/services/compact/postCompactCleanup.ts |
压缩后状态清理 |
src/services/tokenEstimation.ts |
Token 估算工具 |
src/services/api/claude.ts |
API 消息装配、缓存断点、思考配置 |
src/services/api/usage.ts |
API 调用级使用量跟踪 |
src/services/api/errors.ts |
PTL 错误提取 |
src/bootstrap/state.ts |
轮次级 token 预算跟踪 |
src/screens/REPL.tsx |
用户侧 token 预算 UI 和输入解析 |