模块七:记忆与上下文 | 前置依赖:第 20 课 | 预计学习时间:70 分钟
学习目标
完成本课后,你将能够:
- 解释三层压缩策略(microCompact / autoCompact / 手动 compact)的触发条件与协作关系
- 描述
compactConversation的核心流程:分组、剥离图片、生成摘要、恢复文件与技能 - 理解 token 预算体系(50K 上下文、5K/文件、5K/技能、25K 技能总额)
- 说明
calculateTokenWarningState的多级阈值设计 - 区分 REACTIVE_COMPACT 和 CONTEXT_COLLAPSE 两个实验性功能的作用
22.1 系统概览
上下文压缩是 Claude Code 中最关键的基础设施之一。LLM 的上下文窗口有限(通常 128K-200K tokens),而一个复杂的编码会话可能轻松超过这个限制。压缩系统确保对话可以无限延续。
三层压缩架构
上下文使用量增长 →
┌─────────────────────────────────────────────────────┐
│ │
│ microCompact │
│ (每次 API 调用前) │
│ 清理旧工具结果 │
│ │
│ ┌──────────────────────────────────────────┐│
│ │ ││
│ │ autoCompact ││
│ │ (token 阈值触发) ││
│ │ 完整摘要生成 ││
│ │ ││
│ │ ┌────────────────────────────────┐││
│ │ │ │││
│ │ │ 手动 /compact │││
│ │ │ (用户主动触发) │││
│ │ │ 支持自定义指令 │││
│ │ │ │││
│ │ └────────────────────────────────┘││
│ └──────────────────────────────────────────┘│
└─────────────────────────────────────────────────────┘
轻量 ←──────────────────────────────────────→ 重量
高频 ←──────────────────────────────────────→ 低频
22.2 microCompact — 轻量级压缩
触发时机
microCompact 在每次 API 调用前运行,成本极低。它不生成摘要,而是清理已经被处理过的工具结果。
三种 microCompact 路径
export async function microcompactMessages(
messages: Message[],
toolUseContext?: ToolUseContext,
querySource?: QuerySource,
): Promise<MicrocompactResult> {
// 路径 1: 时间基础微压缩(优先级最高)
const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
if (timeBasedResult) return timeBasedResult
// 路径 2: 缓存编辑微压缩(ant-only feature)
if (feature('CACHED_MICROCOMPACT')) {
// 使用 cache_edits API 删除工具结果
// 不修改本地消息内容,通过 API 层操作
}
// 路径 3: 无操作(外部构建回退到 autoCompact)
return { messages }
}
时间基础微压缩
当用户离开一段时间后回来,服务端的 prompt cache 已经过期(通常 5 分钟 TTL)。此时清理旧工具结果不会破坏缓存(因为缓存已经不存在了),反而能减少重写量:
export function evaluateTimeBasedTrigger(
messages: Message[],
querySource: QuerySource | undefined,
): { gapMinutes: number; config: TimeBasedMCConfig } | null {
// 计算距离上次 assistant 消息的时间间隔
const gapMinutes =
(Date.now() - new Date(lastAssistant.timestamp).getTime()) / 60_000
if (gapMinutes < config.gapThresholdMinutes) return null
return { gapMinutes, config }
}
清理策略:保留最近 N 个工具结果,将其余替换为占位符:
// 保留最后 keepRecent 个,至少保留 1 个
const keepRecent = Math.max(1, config.keepRecent)
const keepSet = new Set(compactableIds.slice(-keepRecent))
const clearSet = new Set(compactableIds.filter(id => !keepSet.has(id)))
// 将清除目标的内容替换为占位符
return { ...block, content: '[Old tool result content cleared]' }
可清理的工具类型
const COMPACTABLE_TOOLS = new Set<string>([
FILE_READ_TOOL_NAME, // 文件读取结果
...SHELL_TOOL_NAMES, // Bash 执行结果
GREP_TOOL_NAME, // 搜索结果
GLOB_TOOL_NAME, // 文件匹配结果
WEB_SEARCH_TOOL_NAME, // 网页搜索结果
WEB_FETCH_TOOL_NAME, // 网页获取结果
FILE_EDIT_TOOL_NAME, // 编辑确认
FILE_WRITE_TOOL_NAME, // 写入确认
])
只有这些工具的结果会被清理。MCP 工具、Agent 工具等不在列表中。
22.3 autoCompact — 自动摘要压缩
token 阈值计算
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model)
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS // 13K 缓冲
}
export function getEffectiveContextWindowSize(model: string): number {
const reservedTokensForSummary = Math.min(
getMaxOutputTokensForModel(model),
MAX_OUTPUT_TOKENS_FOR_SUMMARY, // 20K
)
return contextWindow - reservedTokensForSummary
}
假设上下文窗口为 200K tokens:
总上下文窗口: 200,000 tokens
- 摘要输出预留: - 20,000 tokens
= 有效上下文: 180,000 tokens
- autoCompact 缓冲: - 13,000 tokens
= autoCompact 阈值: 167,000 tokens
多级警告状态
calculateTokenWarningState 计算四个阈值状态:
export function calculateTokenWarningState(tokenUsage: number, model: string) {
const threshold = isAutoCompactEnabled()
? autoCompactThreshold
: getEffectiveContextWindowSize(model)
return {
percentLeft: Math.max(0, Math.round(((threshold - tokenUsage) / threshold) * 100)),
isAboveWarningThreshold: tokenUsage >= threshold - 20_000,
isAboveErrorThreshold: tokenUsage >= threshold - 20_000,
isAboveAutoCompactThreshold: isAutoCompactEnabled() && tokenUsage >= autoCompactThreshold,
isAtBlockingLimit: tokenUsage >= actualContextWindow - 3_000,
}
}
视觉化这些阈值:
0% 100%
│═══════════════════════════════════════════════════════════════│
│ │
│ ▲ Warning ▲ AutoCompact ▲ Blocking │
│ │ (-20K) │ (-13K) │ (-3K) │
│ │ │ │ │
│ 状态栏变黄 自动压缩触发 阻止用户输入 │
熔断器
const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
return { wasCompacted: false } // 停止重试
}
如果 autoCompact 连续失败 3 次,停止尝试。这是一个重要的保护机制 — 没有它,上下文不可恢复地超限的会话每轮都会发出无效的压缩 API 调用,全局每天浪费约 250K 次调用。
Session Memory 优先
export async function autoCompactIfNeeded(...) {
// 首先尝试 Session Memory 压缩(更便宜)
const sessionMemoryResult = await trySessionMemoryCompaction(
messages, toolUseContext.agentId, recompactionInfo.autoCompactThreshold,
)
if (sessionMemoryResult) {
// 成功 → 不需要完整摘要
return { wasCompacted: true, compactionResult: sessionMemoryResult }
}
// Session Memory 不可用/不足 → 回退到完整摘要
const compactionResult = await compactConversation(...)
}
Session Memory 压缩(第 20 课的 MEMORY.md)作为更轻量的替代方案优先尝试。它直接使用已有的记忆文件作为摘要,不需要额外的 API 调用来生成摘要。
22.4 compactConversation — 核心压缩流程
完整流程
compactConversation(messages, context, ...)
│
├── 1. 执行 PreCompact hooks
│
├── 2. 按 API 轮次分组消息
│ groupMessagesByApiRound()
│
├── 3. 剥离图片和文档
│ stripImagesFromMessages()
│
├── 4. 生成摘要(Fork 或 Stream)
│ streamCompactSummary()
│ ├── 共享缓存路径(runForkedAgent)
│ └── 独立流式路径(queryModelWithStreaming)
│
├── 5. 处理 prompt_too_long 重试
│ truncateHeadForPTLRetry()
│
├── 6. 清空文件缓存
│ context.readFileState.clear()
│
├── 7. 恢复关键文件
│ createPostCompactFileAttachments()
│
├── 8. 恢复技能内容
│ createSkillAttachmentIfNeeded()
│
├── 9. 重新注入工具/Agent/MCP 信息
│ getDeferredToolsDeltaAttachment()
│ getAgentListingDeltaAttachment()
│ getMcpInstructionsDeltaAttachment()
│
├── 10. 执行 SessionStart hooks
│
└── 11. 构建最终消息数组
boundaryMarker + summary + attachments + hookResults
消息分组
groupMessagesByApiRound() 按 assistant 消息的 message.id 分组 — 来自同一个 API 响应的消息(包括流式分块)共享同一个 id,被归入同一组。这确保了 tool_use 和 tool_result 配对不被拆散。
图片剥离
stripImagesFromMessages() 将 image/document 块替换为 [image]/[document] 文本标记。图片不参与摘要生成(压缩 API 本身可能因图片导致 prompt_too_long),但标记让摘要知道这里有图片。也处理 tool_result 中嵌套的图片。
摘要提示
摘要提示要求模型生成包含 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 — 可选下一步
特别的是,提示中包含一个 <analysis> 草稿区:
const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
...
Your entire response must be plain text: an <analysis> block followed by a <summary> block.`
<analysis> 块是模型的思考过程,在格式化时被移除:
export function formatCompactSummary(summary: string): string {
// 移除 analysis 草稿区
formattedSummary = formattedSummary.replace(/<analysis>[\s\S]*?<\/analysis>/, '')
// 提取 summary 内容
const summaryMatch = formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/)
// ...
}
22.5 Token 预算体系
压缩后需要恢复关键的上下文信息,但恢复量必须有上限,否则压缩效果会被稀释。
预算常量
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总恢复预算 50K
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每个文件 5K
export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 // 每个技能 5K
export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 // 技能总预算 25K
预算分配图
POST_COMPACT_TOKEN_BUDGET (50K)
├── 文件恢复 (最多 5 个 x 5K = 25K)
│ ├── 最近读取的文件(按 readFileState)
│ ├── 去重(file_unchanged 的不重复读取)
│ └── 超长文件截断到 5K tokens
│
├── 技能恢复 (最多 25K)
│ ├── 当前会话中调用过的技能
│ ├── 每个技能截断到 5K tokens
│ └── "Instructions at the top are usually critical"
│
├── 计划恢复 (如有)
│ └── Plan mode 的当前计划文件
│
├── 工具信息重注入
│ ├── Deferred tools delta
│ ├── Agent listing delta
│ └── MCP instructions delta
│
└── SessionStart hooks 结果
└── CLAUDE.md 重加载等
技能截断的设计考量
// Skills can be large (verify=18.7KB, claude-api=20.1KB). Previously re-injected
// unbounded on every compact → 5-10K tok/compact. Per-skill truncation beats
// dropping — instructions at the top of a skill file are usually the critical part.
export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000
技能文件可以很大(最大超过 20KB)。之前不限制地重注入导致每次压缩消耗 5-10K tokens。截断策略优于直接丢弃 — 技能文件顶部的指令通常是最关键的部分。
22.6 prompt_too_long 重试
当压缩请求本身因为上下文太长而失败时,truncateHeadForPTLRetry() 自动截断最老的消息组并重试。策略:如果 API 返回了精确的 token 缺口,按需丢弃覆盖缺口的组数;否则回退到丢弃 20% 的组。至少保留 1 组,最多重试 3 次。这是"最后手段" — 丢弃最老的上下文有损,但解除了用户被卡住的困境。
22.7 Feature-Gated 实验
REACTIVE_COMPACT
if (feature('REACTIVE_COMPACT')) {
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) {
return false // 抑制主动 autoCompact
}
}
Reactive Compact 是一种替代策略:不主动压缩,而是等到 API 返回 prompt_too_long 错误时再被动压缩。这减少了不必要的压缩操作(很多会话永远不会达到上限),但增加了到达上限时的延迟。
CONTEXT_COLLAPSE
if (feature('CONTEXT_COLLAPSE')) {
const { isContextCollapseEnabled } =
require('../contextCollapse/index.js')
if (isContextCollapseEnabled()) {
return false // Context Collapse 取代 autoCompact
}
}
Context Collapse 是一个更精细的上下文管理系统。当它启用时,autoCompact 被完全禁用,因为两者会竞争:
autoCompact 触发点: ~93% 有效上下文
Context Collapse 提交: ~90% 有效上下文
Context Collapse 阻塞: ~95% 有效上下文
如果两者同时启用,autoCompact 会在 93% 时触发,销毁 Context Collapse 正在维护的细粒度上下文。所以必须互斥。
两个 feature flag 都用 feature() 包裹,确保在外部构建中被死代码消除,flag 字符串不会出现在发布的代码中。
22.8 Post-Compact 清理
压缩后需要重置大量缓存和追踪状态:
export function runPostCompactCleanup(querySource?: QuerySource): void {
resetMicrocompactState() // 重置微压缩状态
// Context Collapse 重置(仅主线程)
// getUserContext 缓存清除(仅主线程)
// CLAUDE.md 文件缓存重置
clearSystemPromptSections() // 系统提示缓存
clearClassifierApprovals() // 分类器审批缓存
clearSpeculativeChecks() // 推测执行检查
clearBetaTracingState() // 追踪状态
clearSessionMessagesCache() // 会话消息缓存
}
注意主线程保护:sub-agent 也可能触发压缩,但它们与主线程共享模块级状态。如果 sub-agent 的压缩清理了主线程的缓存,会导致主线程状态损坏。通过 querySource 判断来保护。
22.9 Session Memory Compact — 免 API 调用的压缩
当 Session Memory(第 20 课)可用时,系统可以用它替代完整的摘要 API 调用:
export async function trySessionMemoryCompaction(
messages: Message[],
agentId?: AgentId,
autoCompactThreshold?: number,
): Promise<CompactionResult | null> {
// 前置条件检查
if (!shouldUseSessionMemoryCompaction()) return null
await waitForSessionMemoryExtraction() // 等待进行中的提取
const sessionMemory = await getSessionMemoryContent()
if (!sessionMemory || await isSessionMemoryEmpty(sessionMemory)) return null
// 计算保留多少消息
const startIndex = calculateMessagesToKeepIndex(messages, lastSummarizedIndex)
const messagesToKeep = messages.slice(startIndex)
.filter(m => !isCompactBoundaryMessage(m))
// 用 session memory 内容作为摘要
return createCompactionResultFromSessionMemory(
messages, sessionMemory, messagesToKeep, hookResults, transcriptPath,
)
}
优势:零额外 API 调用。Session Memory 在对话过程中就已经被提取了(第 20 课),压缩时直接使用。
保留消息策略:
export const DEFAULT_SM_COMPACT_CONFIG = {
minTokens: 10_000, // 至少保留 10K tokens 的近期消息
minTextBlockMessages: 5, // 至少保留 5 条有文本的消息
maxTokens: 40_000, // 最多保留 40K tokens
}
从 lastSummarizedMessageId 开始向前扩展,直到满足 minTokens 和 minTextBlockMessages 的最低要求,但不超过 maxTokens 的上限。还需要确保 tool_use/tool_result 配对完整性。
课后练习
练习 1:阈值计算
假设模型上下文窗口为 128K tokens,最大输出为 16K tokens。计算以下值:
- 有效上下文窗口大小
- autoCompact 触发阈值
- Warning 阈值
- Blocking 限制
练习 2:压缩路径分析
一个会话有 120 条消息,总共 150K tokens。Session Memory 已启用但内容为空(匹配模板)。autoCompact 触发。详细描述系统会走哪条压缩路径,以及为什么。
练习 3:post-compact 恢复
压缩前 readFileState 中有 10 个文件,其中 3 个超过 5K tokens。会话中调用了 2 个技能,一个 3K tokens,一个 18K tokens。计算 post-compact 恢复消息的总 token 预算消耗,并说明哪些内容会被截断。
练习 4:竞争条件
如果 CONTEXT_COLLAPSE 和 autoCompact 同时启用(由于配置错误),且上下文使用量达到 93%,会发生什么?追踪 shouldAutoCompact() 的代码路径,说明系统如何处理这种冲突。
本课小结
| 要点 | 内容 |
|---|---|
| 三层架构 | microCompact(每次 API 前)→ autoCompact(阈值触发)→ 手动 /compact |
| microCompact | 时间基础清理 / cache_edits API / 只清理 8 种工具结果 |
| autoCompact | 有效上下文 - 13K buffer = 触发阈值,3 次失败熔断 |
| 核心流程 | 分组 → 剥离图片 → 摘要 → 清缓存 → 恢复文件/技能/工具 |
| Token 预算 | 50K 总额,5K/文件(最多 5 个),5K/技能(25K 总额) |
| SM Compact | 用 Session Memory 替代摘要 API,零额外调用 |
| PTL 重试 | 压缩请求本身太长时截断最老的组,最多重试 3 次 |
| 实验功能 | REACTIVE_COMPACT(被动压缩)、CONTEXT_COLLAPSE(精细管理) |
| 清理保护 | sub-agent 压缩不清除主线程缓存,通过 querySource 判断 |
下一课预告
第 23 课:Prompt Cache 与 API 优化 — 深入 Claude Code 如何利用 Anthropic API 的 prompt cache 机制降低成本和延迟,包括 cache-safe parameters 的设计、cache break detection 系统、以及 forked agent 如何共享主对话的 cache prefix。