当 AI 的"记忆"快要爆炸时,它是如何自救的?
你有没有遇到过这种情况:和 Claude Code 聊到一半,它突然变"健忘"了?或者你手动输入/compact,眼看着整段对话被一段摘要替代?
这背后,藏着一套精密到令人惊叹的"上下文压缩"系统。
今天我们来聊一聊/compact上下文压缩,希望对你有所帮助、有所借鉴~
为什么需要上下文压缩?一个你必须理解的残酷现实
残酷现实:AI 的“记忆”是会爆炸的
每个大语言模型都有一个上下文窗口(Context Window)。Claude 的默认窗口是200K tokens(约 15 万字)。听起来很大?但当你在一个复杂项目里让 Claude Code 帮你读文件、搜代码、执行命令时——每一次工具调用的输入输出都会吃掉 tokens。
读文件 ×10执行命令 ×5代码搜索 ×3多轮对话 ×N你让 Claude Code 帮你重构一个认证模块,它读了 10 个文件、跑了 5 个命令、搜了 3 次代码……此时 token 用量可能已经飙到了 15 万。再来几轮对话,它就"炸了"——API 直接拒绝请求。
不是 AI 不聪明,是:你把它的“脑子”塞满了。
Claude Code 的解决方案?在你不知不觉间,悄悄把"记忆"压缩。
本质不是压缩,而是“分层记忆管理”
Claude Code 的上下文压缩并不是一刀切,而是一套三层递进架构:
┌─────────────────────────────────────────────────┐ │ 第一层:Microcompact(微压缩) │ │ 压缩工具调用结果,不动对话内容 │ │ 触发:每次查询前自动执行 │ ├─────────────────────────────────────────────────┤ │ 第二层:Auto-compact(自动压缩) │ │ 用 AI 生成整段对话摘要,替换原始消息 │ │ 触发:token 用量超过阈值时自动触发 │ ├─────────────────────────────────────────────────┤ │ 第三层:Manual /compact(手动压缩) │ │ 用户主动执行,可附加自定义压缩指令 │ │ 触发:用户输入 /compact │ └─────────────────────────────────────────────────┘第一层:Microcompact——给工具输出"瘦身"
这是最轻量的一层。它不触碰对话内容,只压缩工具调用的返回结果。
看看哪些工具的输出会被压缩:
// src/services/compact/microCompact.tsconstCOMPACTABLE_TOOLS=newSet<string>([FILE_READ_TOOL_NAME,// 文件读取...SHELL_TOOL_NAMES,// Shell 命令GREP_TOOL_NAME,// 代码搜索GLOB_TOOL_NAME,// 文件匹配WEB_SEARCH_TOOL_NAME,// 网络搜索WEB_FETCH_TOOL_NAME,// URL 抓取FILE_EDIT_TOOL_NAME,// 文件编辑FILE_WRITE_TOOL_NAME,// 文件写入])比如你让 Claude Code 读了一个 2000 行的文件,过了几轮对话之后,那次读取的完整内容其实已经不那么重要了——Microcompact 会把它压缩成更短的表示,同时保留关键信息。
图片和文档也不放过——它们被替换为简单的[image]和[document]占位符:
constIMAGE_MAX_TOKEN_SIZE=2000// 图片按 2000 tokens 估算第二层:Auto-compact——AI 的"自动体检"
这是整套系统最精妙的部分。
每次 Claude Code 向 API 发送请求前,都会执行一次"体检"——检查当前 token 用量是否已经逼近上限。这个检查的核心是一组精心设计的阈值:
// src/services/compact/autoCompact.ts// 为压缩输出预留的 tokens(压缩摘要本身也需要空间)constMAX_OUTPUT_TOKENS_FOR_SUMMARY=20_000// 自动压缩的缓冲区——距离上限还剩 13K tokens 时触发exportconstAUTOCOMPACT_BUFFER_TOKENS=13_000// 警告阈值——距离自动压缩阈值还剩 20K 时开始警告exportconstWARNING_THRESHOLD_BUFFER_TOKENS=20_000// 硬限制缓冲——手动压缩的最后防线,仅留 3K tokensexportconstMANUAL_COMPACT_BUFFER_TOKENS=3_000用一张图来理解这些阈值在 200K 上下文窗口中的位置:
0 200K tokens ├────────────────────────────────────────────────────────┤ │ │ │ [可用空间] │ [警告区] │ [自动压缩触发] │ [输出预留20K] │ │ │ 20K │ 13K │ │ │ ↑ ↑ ↑ │ │ ~147K ~167K ~180K │阈值计算的源码:
// 有效上下文窗口 = 模型上下文窗口 - 压缩输出预留exportfunctiongetEffectiveContextWindowSize(model:string):number{constreservedTokensForSummary=Math.min(getMaxOutputTokensForModel(model),MAX_OUTPUT_TOKENS_FOR_SUMMARY,// 20,000)letcontextWindow=getContextWindowForModel(model,getSdkBetas())returncontextWindow-reservedTokensForSummary}// 自动压缩阈值 = 有效窗口 - 13K 缓冲exportfunctiongetAutoCompactThreshold(model:string):number{consteffectiveContextWindow=getEffectiveContextWindowSize(model)returneffectiveContextWindow-AUTOCOMPACT_BUFFER_TOKENS}以 200K 窗口为例:有效窗口 = 200K - 20K =180K,自动压缩阈值 = 180K - 13K =167K。也就是说,当你的对话 tokens 超过167K时,自动压缩就会悄悄启动。
但触发前还有一系列安全检查:
// src/services/compact/autoCompact.ts - shouldAutoCompact()// 1. 不在"压缩Agent"中递归压缩(防死锁!)// 2. 不在协调Agent中压缩(防状态损坏)// 3. 用户没有禁用自动压缩// 4. token 用量确实超过了阈值还有一个熔断机制——如果连续 3 次自动压缩都失败了,就停止重试:
constMAX_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."// 修复前,有些会话疯狂重试了 3272 次!每天全球浪费 25 万次 API 调用!这个注释太真实了——它告诉我们,即便是 Anthropic 的工程师,也经历了从"没有熔断"到"加上熔断"的血泪教训。
第三层:Manual /compact——用户的"紧急手术刀"
当你手动输入/compact时,触发的是第三层。它和 Auto-compact 共用核心压缩引擎(compactConversation()),但有几个关键区别:
1. 支持自定义压缩指令
你可以告诉 AI “压缩时重点关注什么”:
/compact 关注 TypeScript 代码变更和测试输出,包含完整的代码片段这段文字会作为customInstructions追加到压缩 Prompt 的末尾:
// src/services/compact/prompt.tsexportfunctiongetCompactPrompt(customInstructions?:string):string{letprompt=NO_TOOLS_PREAMBLE+BASE_COMPACT_PROMPTif(customInstructions&&customInstructions.trim()!==''){prompt+=`\n\nAdditional Instructions:\n${customInstructions}`}prompt+=NO_TOOLS_TRAILERreturnprompt}2. 三条压缩路径的优先级
手动/compact的执行流程并不是直接调用压缩引擎,而是有一条三级尝试链:
// src/commands/compact/compact.tsexportconstcall:LocalCommandCall=async(args,context)=>{constcustomInstructions=args.trim()// 路径 1:Session Memory 压缩(最轻量,无自定义指令时优先尝试)if(!customInstructions){constsessionMemoryResult=awaittrySessionMemoryCompaction(messages,context.agentId)if(sessionMemoryResult){getUserContext.cache.clear?.()runPostCompactCleanup()return{type:'compact',compactionResult:sessionMemoryResult}}}// 路径 2:Reactive Compact(如果启用了 reactive-only 模式)if(reactiveCompact?.isReactiveOnlyMode()){returnawaitcompactViaReactive(messages,context,customInstructions,reactiveCompact)}// 路径 3:传统压缩(先 Microcompact 瘦身,再 AI 总结)constmicrocompactResult=awaitmicrocompactMessages(messages,context)constresult=awaitcompactConversation(microcompactResult.messages,context,awaitgetCacheSharingParams(context,microcompactResult.messages),false,// suppressFollowUpQuestions = falsecustomInstructions,false,// isAutoCompact = false)return{type:'compact',compactionResult:result}}注意传统路径的第一步——先执行 Microcompact。这意味着手动/compact实际上同时触发了第一层和第三层,先用微压缩给工具输出瘦身,再用 AI 生成整体摘要,双管齐下让压缩效果最大化。
3. 不受熔断机制限制
自动压缩有"连续失败 3 次就停止"的熔断保护,但手动/compact没有——因为这是用户的主动操作,用户有权决定"我就要压缩"。
压缩的核心:那个"总结 Prompt"到底长什么样?
当自动压缩或手动/compact被触发后,Claude Code 需要让 AI阅读整段对话,然后生成一份高质量摘要。
这个总结 Prompt 的设计极其讲究。首先,它会用一段强硬的前置指令防止 AI "手痒"去调用工具:
// src/services/compact/prompt.tsconstNO_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.`为什么要这么强硬?源码注释透露了原因:
在 Sonnet 4.6+ 的自适应思考模型上,即便有弱一些的尾部指令,模型有时候仍然会试图调用工具。在缓存共享的 fork 路径中,maxTurns 设为 1,一旦工具调用被拒绝就意味着没有文本输出。在 4.6 上这个概率是 2.79%,而 4.5 只有 0.01%。
然后是压缩 Prompt 的主体——它要求 AI 输出 9 个严格的章节:
constBASE_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 点——所有用户消息都要保留。这意味着即使对话被压缩了,你之前说过的每一句话都会以某种形式被记住。第 9 点更有趣——它要求包含原对话的直接引用:
“Include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there’s no drift in task interpretation.”
防止"任务漂移"——这是工程上一个非常精妙的设计。
另一个亮点是<analysis>+<summary>双层结构:
// AI 先在 <analysis> 标签中"打草稿"(思考过程)// 然后在 <summary> 标签中输出正式摘要// 最终 formatCompactSummary() 会把 <analysis> 剥掉——只保留 <summary>exportfunctionformatCompactSummary(summary:string):string{letformattedSummary=summary// 剥掉分析部分——它是提升摘要质量的"草稿纸",// 摘要写完后就没有信息价值了formattedSummary=formattedSummary.replace(/<analysis>[\s\S]*?<\/analysis>/,'',)// 提取并格式化摘要部分constsummaryMatch=formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/)if(summaryMatch){constcontent=summaryMatch[1]||''formattedSummary=formattedSummary.replace(/<summary>[\s\S]*?<\/summary>/,`Summary:\n${content.trim()}`,)}returnformattedSummary.trim()}这种“先让 AI 充分思考,再提取精华”的 Prompt 工程技巧,值得每一位做 AI 应用开发的同学借鉴。
压缩的核心:那个"总结 Prompt"到底长什么样?
当自动压缩或手动/compact被触发后,Claude Code 需要让 AI阅读整段对话,然后生成一份高质量摘要。
这个总结 Prompt 的设计极其讲究。首先,它会用一段强硬的前置指令防止 AI "手痒"去调用工具:
// src/services/compact/prompt.tsconstNO_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.`为什么要这么强硬?源码注释透露了原因:
在 Sonnet 4.6+ 的自适应思考模型上,即便有弱一些的尾部指令,模型有时候仍然会试图调用工具。在缓存共享的 fork 路径中,maxTurns 设为 1,一旦工具调用被拒绝就意味着没有文本输出。在 4.6 上这个概率是 2.79%,而 4.5 只有 0.01%。
然后是压缩 Prompt 的主体——它要求 AI 输出 9 个严格的章节:
constBASE_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 点——所有用户消息都要保留。这意味着即使对话被压缩了,你之前说过的每一句话都会以某种形式被记住。第 9 点更有趣——它要求包含原对话的直接引用:
“Include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there’s no drift in task interpretation.”
防止"任务漂移"——这是工程上一个非常精妙的设计。
另一个亮点是<analysis>+<summary>双层结构:
// AI 先在 <analysis> 标签中"打草稿"(思考过程)// 然后在 <summary> 标签中输出正式摘要// 最终 formatCompactSummary() 会把 <analysis> 剥掉——只保留 <summary>exportfunctionformatCompactSummary(summary:string):string{letformattedSummary=summary// 剥掉分析部分——它是提升摘要质量的"草稿纸",// 摘要写完后就没有信息价值了formattedSummary=formattedSummary.replace(/<analysis>[\s\S]*?<\/analysis>/,'',)// 提取并格式化摘要部分constsummaryMatch=formattedSummary.match(/<summary>([\s\S]*?)<\/summary>/)if(summaryMatch){constcontent=summaryMatch[1]||''formattedSummary=formattedSummary.replace(/<summary>[\s\S]*?<\/summary>/,`Summary:\n${content.trim()}`,)}returnformattedSummary.trim()}这种“先让 AI 充分思考,再提取精华”的 Prompt 工程技巧,值得每一位做 AI 应用开发的同学借鉴。
压缩完成后发生了什么?"善后工作"比你想的复杂得多
压缩生成摘要只是第一步。之后还有一系列精密的善后操作:
1. 构建"压缩边界标记"
// src/services/compact/compact.tsconstboundaryMarker=createCompactBoundaryMessage(isAutoCompact?'auto':'manual',// 标记是自动还是手动触发preCompactTokenCount??0,// 压缩前的 token 数messages.at(-1)?.uuid,// 最后一条消息的 UUID)这个边界标记就像一个"分隔线"——告诉系统:“从这里往前的对话已经被压缩了。”
2. 重新注入关键上下文
压缩会把所有消息替换成摘要,但有些信息不能丢:
// 并行生成后续需要的附件const[fileAttachments,asyncAgentAttachments]=awaitPromise.all([// 重新注入最近读过的文件内容createPostCompactFileAttachments(preCompactReadFileState,context,POST_COMPACT_MAX_FILES_TO_RESTORE),// 重新注入异步 Agent 信息createAsyncAgentAttachmentsIfNeeded(context),])// 如果有计划文件,重新注入constplanAttachment=createPlanAttachmentIfNeeded(context.agentId)// 如果有技能被调用过,重新注入constskillAttachment=createSkillAttachmentIfNeeded(context.agentId)3. 压缩后的消息结构
最终,被压缩的对话变成了这样一个结构:
exportfunctionbuildPostCompactMessages(result:CompactionResult):Message[]{return[result.boundaryMarker,// 1. 系统边界标记...result.summaryMessages,// 2. AI 生成的摘要...(result.messagesToKeep??[]),// 3. 保留的近期消息(如有)...result.attachments,// 4. 重新注入的文件/计划/技能...result.hookResults,// 5. 钩子执行结果]}4. 大面积缓存清理
// src/services/compact/postCompactCleanup.tsexportfunctionrunPostCompactCleanup(querySource?:QuerySource):void{resetMicrocompactState()// 重置微压缩状态getUserContext.cache.clear?.()// 清除用户上下文缓存(让记忆文件重新注入)resetGetMemoryFilesCache('compact')// 重置记忆文件缓存clearSystemPromptSections()// 清除动态系统提示clearClassifierApprovals()// 清除权限分类决策clearSpeculativeChecks()// 清除推测性检查clearBetaTracingState()// 清除遥测状态clearSessionMessagesCache()// 清除会话消息缓存}但有两样东西故意不清除——源码注释给出了理由:
不清除已调用的技能内容:重新注入完整技能列表(约 4K tokens)是纯粹的 cache_creation 开销,收益微乎其微。模型的 Schema 中仍然有 SkillTool,invoked_skills 附件会保留已使用的技能内容。
这种**“每一个 token 都要精打细算”** 的工程思维,才是大规模 AI 应用的真功夫。
Prompt Too Long 容错:当压缩本身也"炸了"
有一个极端情况:对话太长了,连压缩请求本身都超出了 prompt 长度限制。
Claude Code 为此设计了一套PTL(Prompt Too Long)重试机制:
// src/services/compact/compact.tsletptlAttempts=0for(;;){summaryResponse=awaitstreamCompactSummary({messages:messagesToSummarize,summaryRequest,// ...})summary=getAssistantMessageText(summaryResponse)// 如果没有 PTL 错误,正常退出if(!summary?.startsWith(PROMPT_TOO_LONG_ERROR_MESSAGE))break// PTL 了!砍掉最老的消息组,重试ptlAttempts++consttruncated=ptlAttempts<=MAX_PTL_RETRIES// 最多重试 3 次?truncateHeadForPTLRetry(messagesToSummarize,summaryResponse):nullif(!truncated){thrownewError(ERROR_MESSAGE_PROMPT_TOO_LONG)// 实在不行,报错}messagesToSummarize=truncated// 用截断后的消息重试}策略是:每次砍掉最老的 “API 轮次组”,最多重试 3 次。如果 3 次之后还是太长,就只能认输了。
Prompt Cache 共享:省钱的"骚操作"
压缩过程中调用 AI 生成摘要也是要花钱的。Claude Code 想了一个办法——复用主对话的 Prompt Cache:
// 两条路径:// 路径 1:Prompt Cache 共享(省钱)// 用 runForkedAgent() 搭主线程的 cache 便车// 复用系统提示、工具定义等已缓存的 tokens// 路径 2:独立流式请求(保底)// 直接调用模型,提供最小工具集// 设置 maxOutputTokens 上限为 20,000源码中的实验数据显示:
“2026 年 1 月的实验确认:不共享缓存的路径有 98% 的缓存未命中率,消耗了全舰队约 0.76% 的 cache_creation tokens(每天约 380 亿 tokens),集中在临时环境(CCR/GHA/SDK)。”
每天 380 亿 tokens 的浪费——这就是为什么缓存共享不是"优化",而是"必须"。
我们可以用这些知识做什么?
1. 合理使用/compact指令
你可以给/compact传递自定义指令:
/compact 关注 TypeScript 代码变更和测试输出,包含完整的代码片段这段指令会被追加到压缩 Prompt 中,让摘要更聚焦于你关心的内容。
2. 理解自动压缩的时机
当你看到 Claude Code 突然"卡"了一下,或者输出了"Compacting conversation…"之类的提示——它正在自动压缩。这不是 Bug,是 Feature。
3. 环境变量调优
# 手动设置自动压缩触发百分比(比如用 80% 的窗口就开始压缩)CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=80# 限制上下文窗口大小(测试用)CLAUDE_CODE_AUTO_COMPACT_WINDOW=100000# 禁用自动压缩(不推荐,除非你知道自己在做什么)DISABLE_AUTO_COMPACT=true4. 借鉴到自己的 AI 应用中
如果你正在构建基于 LLM 的应用,Claude Code 的这套压缩架构值得深度借鉴:
- 多层递进压缩:先压缩工具输出,再压缩整体对话
- 熔断机制:失败 N 次后停止重试
- 结构化摘要 Prompt:用严格的章节要求保证摘要质量
<analysis>草稿 +<summary>成稿:提升输出质量的 Prompt 技巧- 缓存共享:在压缩这种"辅助操作"中复用主流程的缓存
总结
Claude Code 的上下文压缩系统,远不是一个简单的"总结对话"功能。它是一套包含三层压缩架构、动态阈值计算、PTL 容错重试、Prompt Cache 共享、结构化摘要生成、熔断保护的完整工程系统。
每一行代码背后都是真实的线上问题和性能数据——从每天浪费 25 万次 API 调用的熔断修复,到每天 380 亿 tokens 的缓存优化。
这才是工业级 AI 应用该有的样子。
本文基于 2026 年 3 月公开暴露的 Claude Code 源码快照进行分析,仅用于教育和技术研究目的。原始代码所有权属于 Anthropic。