Claude Code 传统对话压缩

7 阅读10分钟

compactConversation 逐行源码分析

文件: src/services/compact/compact.ts:387-763


概述

compactConversation 是 Claude Code 中传统对话压缩的核心函数。当对话上下文超过 token 阈值时,它调用 LLM(通常是 Haiku)生成摘要,用摘要 + 附件替换旧消息,释放上下文窗口空间。


函数签名(387-394)

export async function compactConversation(
  messages: Message[],            // 待压缩的消息数组
  context: ToolUseContext,         // 工具执行上下文
  cacheSafeParams: CacheSafeParams, // 缓存安全参数(fork-agent 需要)
  suppressFollowUpQuestions: boolean, // 是否抑制后续追问
  customInstructions?: string,     // 用户自定义压缩指令(/compact your instructions)
  isAutoCompact: boolean = false,  // 是否为自动压缩触发
  recompactionInfo?: RecompactionInfo, // 重压缩信息(自动压缩时使用)
): Promise<CompactionResult>

返回值 CompactionResult 包含:

  • boundaryMarker — 边界标记消息
  • summaryMessages — 摘要消息(对用户可见为 compact summary)
  • attachments — 附件(文件、代理信息、plan 等)
  • hookResults — session start hook 产出的消息
  • userDisplayMessage — 显示给用户的通知(结合 pre/post hooks)
  • preCompactTokenCount — 压缩前 token 数
  • postCompactTokenCount — 压缩 API 调用的 token 消耗
  • truePostCompactTokenCount — 压缩后上下文实际大小
  • compactionUsage — API usage 指标

逐行分析

前置校验(396-398)

try {
  if (messages.length === 0) {
    throw new Error(ERROR_MESSAGE_NOT_ENOUGH_MESSAGES)
  }
行号说明
396整段 try-catch-finally
397-398空消息直接抛出"消息不足"异常,避免后续流程处理空数组

预统计(401-404)

const preCompactTokenCount = tokenCountWithEstimation(messages)
const appState = context.getAppState()
void logPermissionContextForAnts(appState.toolPermissionContext, 'summary')
行号说明
401估算当前消息总 token 数,用于日志和边界标记
403-404获取 appState 用于权限记录,void 前缀表示 fire-and-forget(ant 内部埋点)

进度通知 - PreCompact(406-409)

context.onCompactProgress?.({ type: 'hooks_start', hookType: 'pre_compact' })
行号说明
406-409通知 UI 即将执行 pre_compact hooks(onCompactProgress 是可选的,所以 ?.

执行 PreCompact Hooks(411-429)

context.setSDKStatus?.('compacting')
const hookResult = await executePreCompactHooks(
  { trigger: isAutoCompact ? 'auto' : 'manual',
    customInstructions: customInstructions ?? null },
  context.abortController.signal,
)
customInstructions = mergeHookInstructions(customInstructions, hookResult.newCustomInstructions)
const userDisplayMessage = hookResult.userDisplayMessage

context.setStreamMode?.('requesting')
context.setResponseLength?.(() => 0)
context.onCompactProgress?.({ type: 'compact_start' })
行号说明
412通知 SDK 状态为 compacting
413-419执行 pre-compact hooks(用户配置的 pre_compact hook 脚本),传入触发方式(auto/manual)和自定义指令
420-423合并 hook 产出的自定义指令,保留 userDisplayMessage
427-429切换 UI 状态为"请求中",并发送 compact_start 事件

Prompt 缓存共享检测(431-438)

const promptCacheSharingEnabled = getFeatureValue_CACHED_MAY_BE_STALE(
  'tengu_compact_cache_prefix', true,
)
行号说明
431-438GB feature flag tengu_compact_cache_prefix。用于 forked-agent 路径复用主会话的 prompt cache。注释说明了实验数据:关闭后 98% cache miss,每天增加 ~38B token 的 cache_creation

构建压缩请求消息(440-443)

const compactPrompt = getCompactPrompt(customInstructions)
const summaryRequest = createUserMessage({ content: compactPrompt })
行号说明
440根据是否有自定义指令生成压缩 prompt(包含/不包含自定义内容)
441-443将压缩 prompt 包装为一条 user 消息,作为后续 LLM 调用的输入

LLM 摘要生成循环(445-491)

let messagesToSummarize = messages
let retryCacheSafeParams = cacheSafeParams
let summaryResponse: AssistantMessage
let summary: string | null
let ptlAttempts = 0
for (;;) {
  summaryResponse = await streamCompactSummary({
    messages: messagesToSummarize,
    summaryRequest,
    appState, context,
    preCompactTokenCount,
    cacheSafeParams: retryCacheSafeParams,
  })
  summary = getAssistantMessageText(summaryResponse)
  if (!summary?.startsWith(PROMPT_TOO_LONG_ERROR_MESSAGE)) break

  // PTL 重试逻辑
  ptlAttempts++
  const truncated = /* 截断最早的消息组 */ truncateHeadForPTLRetry()
  if (!truncated) { throw new Error(ERROR_MESSAGE_PROMPT_TOO_LONG) }
  messagesToSummarize = truncated
  retryCacheSafeParams = { ...retryCacheSafeParams, forkContextMessages: truncated }
}
行号说明
445-449messagesToSummarize 可变,因为 PTL 重试时需要截断;ptlAttempts 记录重试次数
450无限循环,break 在摘要成功时退出
451-458streamCompactSummary() 调用 LLM(通常用 Haiku)生成摘要,使用流式方式读取
459从 LLM 响应中提取文本
460检查是否命中 prompt-too-long(消息总长超过模型上下文窗口)
462-467CC-1180 修复: 如果压缩请求本身超出上下文,截断最早的消息组后重试,最多 MAX_PTL_RETRIES
469-478超过重试上限 → 记录失败事件并抛出异常
484-490更新截断后的消息数组和缓存参数,继续下一轮尝试

truncateHeadForPTLRetry 是压缩流程中的最后逃生口——当压缩请求本身(发给 LLM 总结的消息列表)都因为太长而返回 413 prompt-too-long 时调用。详细逻辑可查看文档:truncateHeadForPTLRetry 分析

摘要结果校验(493-515)

if (!summary) {
  throw new Error('Failed to generate conversation summary...')
} else if (startsWithApiErrorPrefix(summary)) {
  throw new Error(summary)
}
行号说明
493-506LLM 返回了空内容 → 记录失败事件并抛出异常
507-515LLM 返回了 API 错误前缀 → 记录失败事件并将错误内容作为异常抛出

保存文件缓存状态(517-522)

const preCompactReadFileState = cacheToObject(context.readFileState)
context.readFileState.clear()
context.loadedNestedMemoryPaths?.clear()
行号说明
518将当前文件读取缓存(FileReadTool 缓存的文件路径 → 内容映射)快照为普通对象
521-522清除文件缓存和嵌套 memory 路径缓存——压缩后这些引用不再有效,需要在 post-compact 附件中重建

关于 sentSkillNames 的说明(524-529)

// Intentionally NOT resetting sentSkillNames: ...
行号说明
524-529特意不清除 sentSkillNames(已发送的 skill 列表)。理由是重新注入完整 skill_listing(~4K tokens)会浪费 cache_creation 资源,模型仍然有 SkillTool schema 和 invoked_skills attachment

并行生成附件(531-544)

const [fileAttachments, asyncAgentAttachments] = await Promise.all([
  createPostCompactFileAttachments(preCompactReadFileState, context, POST_COMPACT_MAX_FILES_TO_RESTORE),
  createAsyncAgentAttachmentsIfNeeded(context),
])
行号说明
532-539并行生成两类附件(关键性能优化):
533-537createPostCompactFileAttachments — 基于预压缩时的文件缓存快照,恢复压缩前模型引用过的文件内容。POST_COMPACT_MAX_FILES_TO_RESTORE 限制最大还原文件数
538createAsyncAgentAttachmentsIfNeeded — 检查是否有 in-progress 的异步代理,有则生成进度恢复附件

更多附件生成(545-585)

const planAttachment = createPlanAttachmentIfNeeded(context.agentId)
const planModeAttachment = await createPlanModeAttachmentIfNeeded(context)
const skillAttachment = createSkillAttachmentIfNeeded(context.agentId)
// Re-announce delta attachments
getDeferredToolsDeltaAttachment(...)
getAgentListingDeltaAttachment(...)
getMcpInstructionsDeltaAttachment(...)
行号类型说明
545-548Plan attachment当前会话在 plan mode 中?恢复 plan 状态
552-555Plan mode instructions提供 plan mode 指令,确保压缩后模型继续按 plan mode 工作
558-561Skill attachment如果会话中用到了 skill,恢复 skill 上下文
567-574Deferred tools delta重新声明延迟加载工具的 delta 信息。callSite: 'compact_full' 标记调用源
575-577Agent listing delta重新声明代理列表的 delta
578-585MCP instructions delta重新声明 MCP 指令的 delta

设计要点:压缩吞噬了原有的 delta attachment,所以需要用当前状态重新声明。空消息历史 diff 全量声明。

执行 SessionStart Hooks(587-594)

context.onCompactProgress?.({ type: 'hooks_start', hookType: 'session_start' })
const hookMessages = await processSessionStartHooks('compact', {
  model: context.options.mainLoopModel,
})
行号说明
587-590通知 UI 开始 session_start hooks
592-594执行 session start hooks(CLAUDE.md 等)

创建边界标记消息(596-611)

const boundaryMarker = createCompactBoundaryMessage(
  isAutoCompact ? 'auto' : 'manual',
  preCompactTokenCount ?? 0,
  messages.at(-1)?.uuid,
)
const preCompactDiscovered = extractDiscoveredToolNames(messages)
if (preCompactDiscovered.size > 0) {
  boundaryMarker.compactMetadata.preCompactDiscoveredTools = [...preCompactDiscovered].sort()
}
行号说明
598-601创建压缩边界标记消息,记录触发方式(auto/manual)、压缩前 token 数和最后消息 UUID
603-611提取压缩前所有已发现(discovered)的 deferred tool 名称,存入 boundary marker 元数据。压缩后的摘要不保留 tool_reference 块,但 API schema filter 需要这个信息来继续发送已加载的工具 schema

构建摘要消息(613-624)

const transcriptPath = getTranscriptPath()
const summaryMessages: UserMessage[] = [
  createUserMessage({
    content: getCompactUserSummaryMessage(
      summary, suppressFollowUpQuestions, transcriptPath,
    ),
    isCompactSummary: true,
    isVisibleInTranscriptOnly: true,
  }),
]
行号说明
613获取会话转录文件路径
615-617getCompactUserSummaryMessage 包装 LLM 生成的摘要文本为 ContentBlockParam[],包含抑制追问标记和 transcript 链接
621-622isCompactSummary: true 用于 UI 渲染,isVisibleInTranscriptOnly: true 表示对用户不可见(仅转录中可见)

Token 统计(626-645)

const compactionCallTotalTokens = tokenCountFromLastAPIResponse([summaryResponse])
const truePostCompactTokenCount = roughTokenCountEstimationForMessages([
  boundaryMarker, ...summaryMessages, ...postCompactFileAttachments, ...hookMessages,
])
const compactionUsage = getTokenUsage(summaryResponse)
行号说明
629-631compactionCallTotalTokens = 压缩 API 调用的总 token 数(近似压缩前 token 数)。注释强调语义上是 API 调用总消耗不是压缩后上下文大小
637-642truePostCompactTokenCount = 压缩后上下文真实 token 估算(边界 + 摘要 + 附件 + hooks)。注释说明下一轮 shouldAutoCompact 会在这个值上再加 ~20-40K 的 system prompt + tools + userContext,所以接近阈值时可能触发新一轮压缩
645提取压缩 API 调用的 usage 详情(input_tokens, output_tokens 等)

缓存断点检测重置(697-703)

if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
  notifyCompaction(context.options.querySource ?? 'compact', context.agentId)
}
markPostCompaction()
行号说明
697-703通知缓存断点检测系统发生了压缩,重置缓存读取基线,避免压缩后的 cache drop 被误报为 break。markPostCompaction() 更新全局状态标记

保留会话元数据(706-711)

reAppendSessionMetadata()
行号说明
706-711重新在消息末尾追加会话元数据(自定义标题、标签),确保其在 16KB tail 窗口内——否则 --resume 时读不到自定义标题,显示自动生成的名字

执行 PostCompact Hooks(719-728)

context.onCompactProgress?.({ type: 'hooks_start', hookType: 'post_compact' })
const postCompactHookResult = await executePostCompactHooks({
  trigger: isAutoCompact ? 'auto' : 'manual',
  compactSummary: summary,
}, context.abortController.signal)
行号说明
719-722通知 UI 开始 post_compact hooks
723-729执行 post-compact hooks,传入压缩摘要内容

构建最终结果(731-748)

const combinedUserDisplayMessage = [
  userDisplayMessage,
  postCompactHookResult.userDisplayMessage,
].filter(Boolean).join('\n')

return {
  boundaryMarker,
  summaryMessages,
  attachments: postCompactFileAttachments,
  hookResults: hookMessages,
  userDisplayMessage: combinedUserDisplayMessage || undefined,
  preCompactTokenCount,
  postCompactTokenCount: compactionCallTotalTokens,
  truePostCompactTokenCount,
  compactionUsage,
}
行号说明
731-736合并 pre-compact hook 和 post-compact hook 的 userDisplayMessage(去空,换行连接)
738-748返回完整的 CompactionResult,包含所有附件、摘要、统计信息

错误处理与清理(749-762)

} catch (error) {
  if (!isAutoCompact) {
    addErrorNotificationIfNeeded(error, context)
  }
  throw error
} finally {
  context.setStreamMode?.('requesting')
  context.setResponseLength?.(() => 0)
  context.onCompactProgress?.({ type: 'compact_end' })
  context.setSDKStatus?.(null)
}
行号说明
749-756错误处理:手动压缩(/compact)→ 显示错误通知;自动压缩 → 静默失败(下一轮会重试),不显示弹窗
757-762finally 清理:无论成功/失败,恢复 UI 状态为 requesting、通知 compact_end、清除 SDK 状态

完整执行流程图

compactConversation(messages, context, ...)
  │
  ├─ 空消息检查 → throw
  │
  ├─ 保存 preCompactTokenCount
  ├─ 执行 PreCompact Hooks
  │   └─ 合并自定义指令
  │
  ├─ LLM 摘要循环(最多 MAX_PTL_RETRIES 次)
  │   ├─ streamCompactSummary()  ← 调用 LLM(Haiku)
  │   ├─ 提取摘要文本
  │   ├─ PTL 命中?→ 截断头部长消息 → 重试
  │   └─ 成功退出
  │
  ├─ 校验摘要 → 空/ApiErrorthrow
  │
  ├─ 保存并清除文件读取缓存
  │
  ├─ 并行生成附件:
  │   ├─ createPostCompactFileAttachments()
  │   ├─ createAsyncAgentAttachmentsIfNeeded()
  │   ├─ createPlanAttachmentIfNeeded()
  │   ├─ createPlanModeAttachmentIfNeeded()
  │   ├─ createSkillAttachmentIfNeeded()
  │   ├─ getDeferredToolsDeltaAttachment()
  │   ├─ getAgentListingDeltaAttachment()
  │   └─ getMcpInstructionsDeltaAttachment()
  │
  ├─ 执行 SessionStart Hooks
  │
  ├─ 创建边界标记 (createCompactBoundaryMessage)
  │   └─ 携带 preCompactDiscoveredTools
  │
  ├─ 构建摘要消息 (getCompactUserSummaryMessage)
  │
  ├─ Token 统计 + 埋点 (tengu_compact)
  ├─ notifyCompaction() + markPostCompaction()
  ├─ reAppendSessionMetadata()
  ├─ (可选) KAIROS 转录写入
  │
  ├─ 执行 PostCompact Hooks
  │
  └─ 返回 CompactionResult
       ├─ boundaryMarker
       ├─ summaryMessages
       ├─ attachments (文件/代理/plan/skill/MCP)
       ├─ hookResults
       ├─ userDisplayMessage
       └─ token 统计 (pre / post / true)

关键设计要点

设计决策目的
LLM 生成摘要保证摘要质量,但消耗 token
PTL 重试解决极端大消息导致压缩本身超限的问题(CC-1180)
文件缓存快照-清除-重建避免 stale 文件引用,但保留下游需要的信息
附件并行生成缩短压缩时间
Delta attachment 重新声明压缩吞噬了原有 delta,需用当前状态重建
自动压缩静默失败避免用户困惑,下一轮会自动重试
TruePostCompactTokenCount区分"压缩 API 消耗"和"压缩后实际上下文大小"
promptCacheSharingEnabled控制 forked-agent 是否复用主会话的 prompt cache(减少 cache miss)