上下文压缩
Claude Code 如何管理跨长时间会话的有限上下文窗口 - 从即时微型紧凑到完整的 LLM 总结。
每个法学硕士都有一个有限的上下文窗口。 对于 Claude Code 运行长时间的编码会话(读取文件、运行 shell 命令、迭代错误),该窗口填满的速度比您预期的要快。 上下文压缩 是 Claude Code 用来在令牌压力增大时保持对话活跃且有用的一组策略。
services/compact/microCompact.ts ·
services/compact/compact.ts ·
services/compact/autoCompact.ts ·
services/compact/sessionMemoryCompact.ts ·
services/compact/prompt.ts ·
services/compact/postCompactCleanup.ts ·
services/compact/timeBasedMCConfig.ts ·
services/compact/apiMicrocompact.ts ·
commands/compact/compact.ts ·
commands/context/context.tsx
有 四大策略,每个都有不同的成本/保真度权衡:
Microcompact
从内存消息数组中静默清除旧的工具结果内容。 零 API 调用,即时。
会话内存紧凑
用预先构建的会话内存文件替换旧消息。 零汇总API调用。
完整的法学硕士紧凑型
分叉一个子代理来编写 9 部分的结构化摘要。 一次额外的 API 调用,最高保真度。
反应紧凑型
由 413 提示太长的 API 错误触发。 从尾部剥落 API 圆,直到符合要求为止。
压缩是阈值控制的。 在每次 API 调用之前, autoCompact.ts 计算一个 有效上下文窗口 通过从原始上下文大小中减去输出余量,然后得出四个不同的警报级别。
// autoCompact.ts — effective window calculation
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 // p99.99 of compact output
export function getEffectiveContextWindowSize(model: string): number {
const reservedTokensForSummary = Math.min(
getMaxOutputTokensForModel(model),
MAX_OUTPUT_TOKENS_FOR_SUMMARY,
)
const contextWindow = getContextWindowForModel(model, getSdkBetas())
return contextWindow - reservedTokensForSummary
}
| State | 缓冲区低于有效窗口 | Effect | Constant |
|---|---|---|---|
| Normal | > 剩余 20 000 个代币 | 无动作 | — |
| Warning | 剩余 ≤ 20 000 个代币 | UI 显示黄色指示器 | WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 |
| Error | 剩余 ≤ 20 000 个代币(相同级别) | UI 显示红色指示器 | ERROR_THRESHOLD_BUFFER_TOKENS = 20_000 |
| Auto-Compact | 剩余 ≤ 13 000 个代币 | 触发自动压缩 | AUTOCOMPACT_BUFFER_TOKENS = 13_000 |
| Blocking | 剩余 ≤ 3 000 个代币 | 阻止新用户输入 | MANUAL_COMPACT_BUFFER_TOKENS = 3_000 |
深入探讨:断路器
自动压缩可能会失败(网络超时、压缩请求本身提示太长)。 如果没有防护装置,随后的每个回合都会重试压实,从而以注定失败的尝试来打击 API。 代码跟踪 consecutiveFailures 并在 3 后停止:
// autoCompact.ts
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 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.
if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
return { wasCompacted: false }
}
这个评论异常坦率:在这个断路器存在之前,一种故障模式是全球每天消耗 25 万个 API 调用。 修复方法是三行。
Microcompact 是一个 API 调用前通道,可直接清除内存消息数组中旧工具结果的内容。 确实如此 not 打电话给法学硕士并做 not 将任何内容写入磁盘。 目标:在发送之前缩小提示,无需支付任何费用。
哪些工具符合条件?
// microCompact.ts — only results from these tools can be cleared
const COMPACTABLE_TOOLS = new Set<string>([
FILE_READ_TOOL_NAME,
...SHELL_TOOL_NAMES, // Bash, etc.
GREP_TOOL_NAME,
GLOB_TOOL_NAME,
WEB_SEARCH_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
FILE_EDIT_TOOL_NAME,
FILE_WRITE_TOOL_NAME,
])
只有读取/搜索/shell 工具结果才符合资格。 这些工具的结果很大,通常是陈旧的,并且在几次轮换后不太可能逐字需要。 来自自定义 MCP 工具、代理生成或用户可见操作的工具结果将被保留。
三个微型紧凑路径
路径 1:基于时间的微型紧凑型
如果自上一条助理消息以来的间隔超过阈值(默认值:60 分钟),则服务器端提示缓存几乎肯定已过期。 重写提示是不可避免的 - 因此内容清除旧工具的结果 before 该请求缩小了重写的内容。 该逻辑纯粹是客户端的,并且会就地改变消息。
// timeBasedMCConfig.ts — GrowthBook-controlled config
const TIME_BASED_MC_CONFIG_DEFAULTS: TimeBasedMCConfig = {
enabled: false,
gapThresholdMinutes: 60, // server 1h cache TTL
keepRecent: 5, // always keep the last 5 tool results
}
// microCompact.ts — content-clearing loop
const keepSet = new Set(compactableIds.slice(-keepRecent))
const clearSet = new Set(compactableIds.filter(id => !keepSet.has(id)))
// Replace each cleared block's content with a sentinel string
return { ...block, content: TIME_BASED_MC_CLEARED_MESSAGE }
// TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]'
路径 2:缓存的微型紧凑型(实验性)
常规的基于时间的路径会改变消息内容,这会破坏服务器端提示缓存(前缀已更改)。 缓存 MC 以不同的方式解决了这个问题:它不是重写消息内容,而是将消息排队 cache_edits 块为 API层 应用服务器端,保持缓存的前缀不变。
// microCompact.ts — cached MC result shape
return {
messages, // UNCHANGED — messages are not mutated
compactionInfo: {
pendingCacheEdits: {
trigger: 'auto',
deletedToolIds: toolsToDelete,
baselineCacheDeletedTokens: baseline,
},
},
}
深入探讨:微型决策的代币估计
Microcompact 需要估计工具结果包含多少个标记,以便决定要清除哪些内容。 它使用粗略的基于字符的启发式,用 4/3 填充:
// microCompact.ts
export function estimateMessageTokens(messages: Message[]): number {
// ... walk all blocks ...
// Pad estimate by 4/3 to be conservative since we're approximating
return Math.ceil(totalTokens * (4 / 3))
}
无论格式如何,图像和文档始终计为 2,000 个标记(IMAGE_MAX_TOKEN_SIZE = 2000)。 4/3 填充补偿了高于 1:1 的字符与标记的比率。
会话内存压缩是一种实验性路径,可以完全避免完整 LLM 汇总调用的成本。 它没有要求克劳德总结对话,而是使用不断更新的 会话内存文件 在后台编写,作为压缩会话的上下文。
什么时候激活?
Both autoCompactIfNeeded 和 /compact 命令首先尝试会话内存压缩,然后再回退到完整的 LLM 压缩:
// autoCompact.ts — session memory is always tried first
const sessionMemoryResult = await trySessionMemoryCompaction(
messages,
toolUseContext.agentId,
recompactionInfo.autoCompactThreshold,
)
if (sessionMemoryResult) {
setLastSummarizedMessageId(undefined)
runPostCompactCleanup(querySource)
return { wasCompacted: true, compactionResult: sessionMemoryResult }
}
保留哪些消息?
关键功能 calculateMessagesToKeepIndex 找到已汇总到会话内存中的内容与足以保留逐字记录的最新内容之间的界限。 它从最后汇总的消息向后扩展,直到满足可配置的最小值:
// sessionMemoryCompact.ts — default config (can be overridden by GrowthBook)
export const DEFAULT_SM_COMPACT_CONFIG: SessionMemoryCompactConfig = {
minTokens: 10_000, // keep at least 10k tokens of recent context
minTextBlockMessages: 5, // keep at least 5 messages with text content
maxTokens: 40_000, // hard cap: don't keep more than 40k tokens
}
工具对不变量
一个微妙的正确性要求:API 拒绝以下对话: tool_result
块引用a tool_use 之前未出现在消息列表中的块。 当我们进行切片以仅保留最近的消息时,我们可能会意外地包含一条用户消息
tool_result 阻止但排除前面具有相应内容的辅助消息 tool_use。 功能 adjustIndexToPreserveAPIInvariants
向后行走以查找并包含任何孤立的工具使用对。
深入探讨:处理两种压缩场景
场景 1 — 正常情况: lastSummarizedMessageId 已设置。 会话内存提取至少运行一次,我们确切地知道它覆盖了哪些消息。 我们只保留该 ID 之后的消息(扩展到满足最小值)。
场景 2 — 恢复会议: 会话内存有内容但是
lastSummarizedMessageId 未设置(例如,会话从之前的记录中恢复)。 我们将此视为“一切都已概括”并设定 lastSummarizedIndex
to messages.length - 1。 无论如何,扩展循环可能会保留一些最近的消息以满足最小值。
// sessionMemoryCompact.ts
if (!lastSummarizedMessageId) {
// Resumed session: session memory has content but we don't know the boundary
lastSummarizedIndex = messages.length - 1
logEvent('tengu_sm_compact_resumed_session', {})
}
当 microcompact 和会话内存压缩都不可用时,Claude Code 分叉一个子代理并要求它编写整个对话的结构化摘要。 这是最昂贵的路径 - 一个额外的 API 调用 - 但会产生最忠实的摘要。
压实流量
9 部分摘要提示
压缩提示指示模型生成包含这九个部分的结构化摘要。 这种结构是有意为之的:它确保每个后续会话即使在没有其他上下文的情况下也能理解正在发生的事情。
- 主要请求和意图 用户的所有明确请求和意图的详细信息。
- 关键技术概念 讨论了重要的技术、框架和设计模式。
- 文件和代码部分 检查/修改/创建特定文件,并在适用的情况下提供完整的代码片段。
- 错误和修复 遇到的每个错误以及如何解决。 用户反馈被突出显示。
- 解决问题 已解决的问题和持续的故障排除工作。
- 所有用户留言 每条非工具结果用户消息均逐字列出。 对于跟踪意图漂移至关重要。
- 待处理任务 明确分配但尚未完成的任务。
- 目前的工作 正是在压缩之前发生的事情,包括文件名和片段。
- 可选的下一步 仅当直接符合最近的用户请求时。 必须包含逐字引用。
分析暂存器模式
提示要求模型将其推理包含在 <analysis> 生产前的标签 <summary>。 分析部分是一个起草草稿本——它在摘要到达上下文之前被删除:
// prompt.ts — formatCompactSummary strips analysis block
export function formatCompactSummary(summary: string): string {
let formatted = summary
// Strip analysis section — drafting scratchpad, no informational value
formatted = formatted.replace(
/<analysis>[\s\S]*?<\/analysis>/,
'',
)
// Extract and format <summary> section
const match = formatted.match(/<summary>([\s\S]*?)<\/summary>/)
if (match) {
formatted = formatted.replace(
/<summary>[\s\S]*?<\/summary>/,
`Summary:\n${match[1]!.trim()}`,
)
}
return formatted.trim()
}
无工具序言
由于紧凑请求分叉了主对话的工具集(用于缓存键匹配),因此模型可能会尝试调用工具,尽管被要求进行总结。 工具调用浪费了唯一的回合并且不产生摘要。 提示以咄咄逼人的序言开头:
// prompt.ts
const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.
`
tengu_compact_ptl_retry events.
深入探讨:部分紧凑提示
实际上有三种紧凑的提示变体,而不是一种:
- BASE_COMPACT_PROMPT — 完整对话摘要,第 1 课-9 部分,包括“当前工作”
- PARTIAL_COMPACT_PROMPT (
direction: 'from')——仅总结 recent 部分; 较早的消息保持不变 - PARTIAL_COMPACT_UP_TO_PROMPT (
direction: 'up_to') — 摘要放置在 start 连续会议; 第 9 课 节变为“继续工作的背景”而不是“可选的下一步”
反应式紧凑路径使用部分提示来仅总结需要丢弃的对话部分。
主动自动紧凑型火灾 before 上下文窗口已满。 但是,如果会话启动时窗口已超出限制(例如,具有较大转录本的恢复会话或禁用自动压缩的会话),该怎么办? 反应式紧凑路径可以处理这种情况。
反应式紧凑型何时起火?
反应式紧凑型以两种模式激活:
- 仅反应模式 (
tengu_cobalt_raccoon功能标志):完全抑制主动自动压缩; 来自 API 的 413 错误是唯一的触发因素。 - 紧急后备:API 返回 413
prompt_too_long正常请求时出错。 该代码从最旧的一端剥离 API 轮组并重试。
// autoCompact.ts — reactive-only mode short-circuit
if (feature('REACTIVE_COMPACT')) {
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) {
return false // suppress proactive autocompact
}
}
上下文崩溃
还有一个单独的 上下文崩溃 特征 (CONTEXT_COLLAPSE)在启用时完全抑制自动压缩。 上下文崩溃是它自己的上下文管理系统,在 90%(提交)和 95%(阻塞)阈值下运行,比压缩更细粒度。 自动紧凑坐姿约为 93% 会与之竞争:
// autoCompact.ts — suppress autocompact when context collapse is active
if (feature('CONTEXT_COLLAPSE')) {
if (isContextCollapseEnabled()) {
return false // let collapse manage the headroom problem
}
}
querySource === 'session_memory' or
querySource === 'compact' ——这些是分叉的代理,如果它们试图压缩自己,就会陷入僵局。 在任何压缩逻辑运行之前都会检查防护。
紧凑请求提示过长重试和反应式紧凑路径都需要将消息丢弃在安全单元中。 该单位是一个 API轮:来自一个完整的请求-响应对的一组消息。
// grouping.ts — group at assistant message.id boundaries
export function groupMessagesByApiRound(messages: Message[]): Message[][] {
const groups: Message[][] = []
let current: Message[] = []
let lastAssistantId: string | undefined
for (const msg of messages) {
if (
msg.type === 'assistant' &&
msg.message.id !== lastAssistantId &&
current.length > 0
) {
groups.push(current)
current = [msg]
} else {
current.push(msg)
}
if (msg.type === 'assistant') lastAssistantId = msg.message.id
}
if (current.length > 0) groups.push(current)
return groups
}
边界信号是 助理消息ID 改变。 流媒体发送一 AssistantMessage 每个内容块(思考、工具使用、文本)都共享相同的内容 message.id。 新的 ID 意味着真正的新 API 往返。 这使得代码可以在轮次边界处安全地分割,而不会破坏属于同一轮次的 tool_use/tool_result 对。
在任何成功的压缩(微型压缩、会话内存或完整 LLM)之后,都会运行一个清理函数以使缓存无效,并指出新上下文窗口中可能出现的错误。
// postCompactCleanup.ts — called by ALL compaction paths
export function runPostCompactCleanup(querySource?: QuerySource): void {
const isMainThread =
querySource === undefined ||
querySource.startsWith('repl_main_thread') ||
querySource === 'sdk'
resetMicrocompactState() // always
if (feature('CONTEXT_COLLAPSE') && isMainThread) {
resetContextCollapse() // main thread only
}
if (isMainThread) {
getUserContext.cache.clear?.() // re-read CLAUDE.md on next turn
resetGetMemoryFilesCache() // arm InstructionsLoaded hook
}
clearSystemPromptSections()
clearClassifierApprovals()
clearSpeculativeChecks()
clearBetaTracingState()
clearSessionMessagesCache()
}
getUserContext,它会破坏主线程的内存文件缓存。 这 isMainThread 警卫阻止了这一点。 守卫使用 startsWith('repl_main_thread') 因为输出样式变体产生像这样的源 'repl_main_thread:outputStyle:custom'.
故意不清除的内容
清理是故意的 not reset sentSkillNames。 在每个紧凑型上重新注入完整的技能列表(~4k 令牌)将是纯粹的缓存失效,带来的好处最小——模型仍然具有 SkillTool 架构,并且 invoked_skills 附件保留使用的技能内容。 这是评论中记录的故意的性能权衡。
后压缩文件附件
在完整的 LLM 紧凑之后,系统重新注入模型之前读取的文件,因此不需要在新会话中重新读取它们:
// compact.ts — constants for post-compact file restoration
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
export const POST_COMPACT_TOKEN_BUDGET = 50_000
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000
export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000
技能是按技能设置上限的,而不是完全删除:每个技能文件可以是 18-20 KB,之前它们是无限制地重新注入的,每个契约花费 5-10k 代币。 按技能截断保留最重要的指令(位于文件顶部),同时限制总成本。
The /context 命令向用户显示其上下文窗口的完整程度。 重要的设计细节:它应用了与之前相同的 API 转换 query.ts
适用,因此用户看到的内容反映了模型实际接收到的内容,而不是原始的 REPL 回滚历史记录。
// context.tsx — mirrors the query.ts pre-API transform pipeline
function toApiView(messages: Message[]): Message[] {
// 1. Slice to only messages after the last compact boundary
let view = getMessagesAfterCompactBoundary(messages)
// 2. Apply context-collapse projection if enabled
if (feature('CONTEXT_COLLAPSE')) {
view = projectView(view)
}
return view
}
// Then apply microcompact to get accurate token count
const { messages: compacted } = await microcompactMessages(apiView)
如果没有这个管道,无论上下文崩溃保存了多少,令牌计数都会超算——当 API 只看到 120k 时,用户会看到“180k,3 个跨度崩溃”。 应用与真实 API 调用路径相同的转换可以使显示准确。
要点
- 压缩是一个成本阶梯:微型压缩是免费的,会话内存几乎是免费的,完整的LLM压缩需要一次额外的API调用,反应式压缩是逃生舱口。
- 有效上下文窗口是原始窗口减去为紧凑摘要输出本身保留的 20k 标记——一个基于 p99.99 的常量。
- 自动压缩在有效窗口限制之前的 13k 个令牌处触发(
AUTOCOMPACT_BUFFER_TOKENS); 在 3k 处阻止触发器。 - Microcompact 从不调用 API - 它会清除本地消息数组中旧工具结果的内容,或者(使用缓存的 MC)对不会破坏提示缓存的服务器端 cache_edit 进行排队。
- 会话内存压缩通过使用持续更新的内存文件完全避免了汇总 API 调用。 它由两个功能标志控制,并具有可配置的最小/最大令牌预算,用于保存逐字记录的最近消息数。
- 9 部分的摘要提示是一个经过深思熟虑的结构:第 6 课 部分(所有用户消息)捕获了仅使用工具使用历史记录会错过的意图转变。
- The
<analysis>紧凑输出中的块是一个暂存器——在摘要进入上下文窗口之前它总是被删除。 - 压缩后清理集中在
runPostCompactCleanup并保护子代理,子代理与主线程共享模块级状态。 - The
/context命令应用与查询循环相同的预 API 转换来显示准确的标记计数,而不是原始 REPL 历史记录。
知识检查
adjustIndexToPreserveAPIInvariants 函数向后扩展会话内存紧凑“保持”边界。 它保护哪两个不变量?runPostCompactCleanup 只清楚 getUserContext and getMemoryFilesCache 对于主线程压缩,而不是子代理压缩?/context 命令在显示令牌使用情况之前应用 microcomp。 为什么?完成所有问题即可查看您的分数。