【无标题】
第五章:对话压缩系统——上下文窗口的生命周期管理
第五章:对话压缩系统——上下文窗口的生命周期管理
系列说明:本章深入 Claude Code 的对话压缩机制(Compact)。这是记忆系统的重要组成部分——当对话上下文逼近模型窗口上限时,压缩系统负责将历史对话提炼为结构化摘要,使新的工作得以继续。本章涵盖:压缩架构全貌、触发条件与阈值计算、核心执行流程、压缩提示词设计、微压缩机制,以及与记忆系统的交互。
一、为什么需要对话压缩?
Claude 的上下文窗口是有限的。在一次较长的编程会话中,随着代码阅读、工具调用、错误修复的累积,消息列表的 token 数量会不断增长,最终逼近模型的上下文上限。
无压缩时的后果:
- token 数超过窗口上限 → API 返回
prompt_too_long错误 - 用户被迫手动重开会话,丢失所有工作上下文
- 长期任务无法完成
Claude Code 的解决方案是对话压缩(Compact):在上下文接近上限前,用另一个 Claude 实例把整段对话历史提炼成一份结构化摘要,用摘要替换原始消息,释放大量 token 空间,同时保留关键工作上下文。
二、核心压缩执行:compactConversation()
services/compact/compact.ts:387-763
完整压缩分六个阶段:
2.1 阶段一:前置准备
// 执行 pre_compact hooks(可注入自定义指令)
const hookResults = await executePreCompactHooks()
// 获取压缩提示词(含自定义指令)
const prompt = getCompactPrompt(customInstructions)
setStreamMode('requesting')
2.2 阶段二:摘要生成(带重试)
compact.ts
// 优先路径:复用主 agent 的 prompt cache(近零额外 token 开销)
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_compact_cache_prefix', true)) {
try {
summary = await runForkedAgent(...) // 与主 agent 共享 cache prefix
logEvent('tengu_compact_cache_sharing_success', { cacheHitRate, ... })
} catch {
logEvent('tengu_compact_cache_sharing_fallback', { reason: 'error' })
}
}
// 降级路径:直连 API
if (!summary) {
summary = await queryModelWithStreaming(...)
}
prompt-too-long 重试机制:
// compact.ts:243-291
export function truncateHeadForPTLRetry(
messages: Message[],
ptlResponse: AssistantMessage,
): Message[] | null {
const groups = groupMessagesByApiRound(input)
if (groups.length < 2) return null
const tokenGap = getPromptTooLongTokenGap(ptlResponse)
let dropCount: number
if (tokenGap !== undefined) {
// 精确计算:丢弃足够覆盖 gap 的最老消息组
let acc = 0
for (const g of groups) {
acc += roughTokenCountEstimationForMessages(g)
dropCount++
if (acc >= tokenGap) break
}
} else {
// 无法解析 gap:丢弃最老的 20% 消息组
dropCount = Math.max(1, Math.floor(groups.length * 0.2))
}
dropCount = Math.min(dropCount, groups.length - 1) // 至少保留 1 组
// ...
}
最多重试 3 次(MAX_PTL_RETRIES = 3),每次从最老的消息组开始丢弃。
2.3 阶段三:文件与附件恢复
压缩后,模型的"读文件状态"被清空,但最近访问的文件需要重新注入,否则模型不知道文件当前内容:
// compact.ts:1415-1464
function createPostCompactFileAttachments() {
// 读取最近访问的文件列表(时间倒序)
const recentFiles = readFileState.getRecentFiles()
const POST_COMPACT_TOKEN_BUDGET = 50_000
const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
// 按 token 预算逐个恢复,最多 5 个文件
let budgetRemaining = POST_COMPACT_TOKEN_BUDGET
for (const file of recentFiles.slice(0, POST_COMPACT_MAX_FILES_TO_RESTORE)) {
const content = readWithTokenLimit(file, POST_COMPACT_MAX_TOKENS_PER_FILE)
budgetRemaining -= estimateTokens(content)
if (budgetRemaining < 0) break
attachments.push(createFileAttachment(file, content))
}
}
此外还恢复:
createPlanAttachmentIfNeeded():如果有未完成的 Plan,重新附加createSkillAttachmentIfNeeded():保留已调用技能的上下文- 工具 delta 和 MCP 指令重新注入
2.4 阶段四:消息重组
// 压缩边界标记(UI 显示为 "✻ Conversation compacted")
const boundaryMarker = createCompactBoundaryMessage({
trigger: isAutoCompact ? 'auto' : 'manual',
timestamp: Date.now(),
preCompactMessageCount: messages.length,
preCompactTokenCount,
})
// 最终消息结构:
// [边界标记] + [摘要消息] + [保留的最近消息] + [文件附件] + [hook 结果]
2.5 阶段五:分析与遥测
// compact.ts:650-695
logEvent('tengu_compact', {
preCompactTokenCount,
postCompactTokenCount,
truePostCompactTokenCount, // 实际消息估算大小
willRetriggerNextTurn, // 压缩后是否立即会再次超限(重要健康指标)
isAutoCompact,
compactionCacheReadTokens, // cache 命中节省的 token
compactionCacheCreationTokens, // cache 创建消耗的 token
promptCacheSharingEnabled,
isRecompactionInChain, // 同一会话链内的连续压缩
turnsSincePreviousCompact,
})
willRetriggerNextTurn 是关键健康指标:如果压缩后 token 数仍然超过阈值,说明本次压缩没有真正解决问题,下一轮会立即再次触发——这通常意味着摘要本身太长或附件太大。
2.6 阶段六:后置清理
// postCompactCleanup.ts
export function runPostCompactCleanup(querySource?: QuerySource) {
resetMicrocompactState() // 重置微压缩状态
getUserContext.cache.clear() // 清除用户上下文缓存
resetGetMemoryFilesCache() // 重置记忆文件缓存(触发重新加载)
clearSystemPromptSections() // 清除系统提示各节缓存
if (feature('CONTEXT_COLLAPSE')) {
resetContextCollapse()
}
}
resetGetMemoryFilesCache() 是与记忆系统的关键交互点:压缩后,CLAUDE.md 文件列表缓存被清除,下一轮对话会重新从磁盘加载最新的 CLAUDE.md 和 MEMORY.md——这确保了压缩后记忆状态与文件状态保持一致。
三、压缩提示词:让模型"准确压缩"
源文件:
services/compact/prompt.ts
压缩提示词是整个系统中经过最精细设计的部分,分三段拼接。
3.1 NO_TOOLS_PREAMBLE——强制禁用工具
prompt.ts:19-26
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.
`
注释解释了这么强硬的措辞的来历:
// Aggressive no-tools preamble. The cache-sharing fork path inherits the
// parent's full tool set (required for cache-key match), and on Sonnet 4.6+
// adaptive-thinking models the model sometimes attempts a tool call despite
// the weaker trailer instruction. With maxTurns: 1, a denied tool call means
// no text output → falls through to the streaming fallback (2.79% on 4.6 vs
// 0.01% on 4.5). Putting this FIRST and making it explicit about rejection
// consequences prevents the wasted turn.
关键约束:cache-sharing 路径必须继承主 agent 的完整工具集(因为工具集是 cache key 的一部分),但模型不能调用工具——所以提示词必须足够强硬。Sonnet 4.6 在弱提示下的工具调用率是 2.79%,加上这段后降至接近 0。
3.2 BASE_COMPACT_PROMPT——9 节结构化摘要
prompt.ts:61-143
const BASE_COMPACT_PROMPT = `Your task is to create a detailed summary of the conversation so far...
Your summary should include the following sections:
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 节"All user messages"的设计意义:用户的原始消息是意图的最直接载体。如果只保留摘要,模型在理解用户"为什么这样要求"时会丢失细节。第 6 节保留所有用户消息原文,作为意图溯源的锚点。
第 9 节"Optional Next Step"的约束:
IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent
explicit requests. If your last task was concluded, then only list next steps if
they are explicitly in line with the users request. Do not start on tangential
requests or really old requests that were already completed without confirming
with the user first.
If there is a next step, include direct quotes from the most recent conversation
showing exactly what task you were working on and where you left off.
这段约束防止"续接漂移"——模型恢复工作后接着做的不是用户最后要求的事,而是自己推断的"下一步"。通过要求直接引用最近对话原文,将漂移风险降至最低。
3.3 分析块与摘要块
// formatCompactSummary() — prompt.ts:311-335
export function formatCompactSummary(summary: string): string {
// 移除 <analysis>...</analysis>(模型的思考过程,不进入上下文)
let formatted = summary.replace(/<analysis>[\s\S]*?<\/analysis>/g, '')
// 移除 <summary>...</summary> 标签本身,保留内容
formatted = formatted.replace(/<summary>([\s\S]*?)<\/summary>/g, '$1')
// 清理多余空行
return formatted.trim()
}
模型先在 <analysis> 块里做思考(按时间顺序检查每条消息、验证完整性),再在 <summary> 块里输出结构化摘要。formatCompactSummary() 在摘要注入上下文前,将 <analysis> 块完整剥离——思考过程消耗 token 但不该占用上下文空间。
四、部分压缩:partialCompactConversation()
compact.ts:772-1106
部分压缩支持两个方向,用于更精细的上下文控制:
| 方向 | 含义 | 摘要结构差异 |
|---|---|---|
'from' |
压缩 anchor 之后的消息 | 第 8 节:Current Work(当前进行中) |
'up_to' |
压缩 anchor 之前的消息 | 第 8 节:Work Completed(已完成),第 9 节:Context for Continuing(续接上下文) |
'up_to' 方向的设计意图:把已完成的历史工作压缩成摘要,让"已完成"部分占更少 token,同时保留最近的未完成工作消息完整不压缩。
五、微压缩:microcompactMessages()
5.1 什么是微压缩?
微压缩是一个更轻量的预处理步骤:不调用额外的 Claude 实例,只是清理掉冗余的工具调用结果,在不生成摘要的前提下释放一定的 token 空间。
5.2 可压缩工具集
microCompact.ts:40-50
const COMPACTABLE_TOOLS = new Set([
FILE_READ_TOOL_NAME, // 文件读取结果
BASH_TOOL_NAME, // Bash 命令输出
GREP_TOOL_NAME, // 搜索结果
GLOB_TOOL_NAME, // 文件列表
WEB_SEARCH_TOOL_NAME, // 网页搜索结果
WEB_FETCH_TOOL_NAME, // 网页内容
FILE_EDIT_TOOL_NAME, // 编辑操作结果
FILE_WRITE_TOOL_NAME, // 写入操作结果
])
这些工具的调用结果在历史消息中可以被截断或清空——模型不需要在上下文中保留上 10 轮前读取的文件完整内容,只需保留最近的几次。
5.3 基于时间的微压缩
timeBasedMCConfig.ts
type TimeBasedMCConfig = {
enabled: boolean
gapThresholdMinutes: 60 // 触发间隔(= prompt cache TTL)
keepRecent: 5 // 保留最新 N 个工具结果
}
触发逻辑:检测 (now - lastAssistantTimestamp) > 60分钟。
原因:Anthropic 的 prompt cache TTL 约为 1 小时,超过后 cache 会失效——既然 cache 已经失效,继续保留旧的工具调用结果没有意义(它们既不在 cache 里节省 token,也不能为模型提供有效上下文),不如直接清理。
六、与记忆系统的交互
6.1 压缩后重置记忆缓存
// postCompactCleanup.ts
resetGetMemoryFilesCache() // 清除 CLAUDE.md / MEMORY.md 缓存
clearSystemPromptSections() // 清除系统提示各节缓存(含记忆节)
压缩后的下一轮对话会重新加载 MEMORY.md——如果后台提取代理在压缩期间写入了新记忆,压缩完成后会立即生效。
6.2 会话记忆压缩
sessionMemoryCompact.ts
这是一种比完整压缩更轻量的方案:不生成新摘要,而是利用 ~/.claude/session-memory/ 中已有的会话滚动摘要来替代旧消息。
type SessionMemoryCompactConfig = {
minTokens: 10_000 // 压缩后最少保留的 token
minTextBlockMessages: 5 // 最少保留的消息数
maxTokens: 40_000 // 压缩后硬上限
}
执行优先级高于完整压缩:autoCompactIfNeeded() 和 /compact 命令都先尝试会话记忆压缩,失败后才降级到完整压缩。
其他
CompactionResult` 数据结构
export interface CompactionResult {
boundaryMarker: SystemMessage // 压缩边界标记消息
summaryMessages: UserMessage[] // 摘要消息列表
attachments: AttachmentMessage[] // 恢复的文件附件
hookResults: HookResultMessage[] // Hook 执行结果
messagesToKeep?: Message[] // 保留的最近消息
userDisplayMessage?: string // 显示给用户的状态信息
preCompactTokenCount?: number // 压缩前 token 数
postCompactTokenCount?: number // 摘要生成消耗的 token
truePostCompactTokenCount?: number // 实际压缩后消息估算大小
compactionUsage?: ReturnType<typeof getTokenUsage>
}
七、章节小结
| 设计决策 | 原因 |
|---|---|
| 自动压缩预留 13K buffer | 给压缩本身的 API 调用留出空间 |
| 电路断路器(3次失败) | 防止无法恢复的上下文每轮浪费 API |
| cache-sharing 优先路径 | 复用 prompt cache,接近零额外开销 |
| 9节结构化摘要 | 保留续接工作所需的全部信息 |
<analysis> 块被剥离 |
思考过程不进入上下文,节省 token |
| 压缩后重置记忆缓存 | 使后台写入的新记忆立即生效 |
| 时间触发微压缩(60分钟) | cache 失效后旧工具结果无价值 |
| 会话记忆压缩优先 | 轻量级方案优先,保留更多原始消息 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)