Skip to main content

上下文管理:长对话的生存术

和 Claude 聊了很久之后,它偶尔会忘记你之前说过的某个要求。这不是它在敷衍你,而是上下文压缩的必然代价。源码里 Anthropic 在「保留什么」上做了非常精细的设计。

压缩何时触发

本节源码:src/services/compact/autoCompact.ts

源码里有两种主流压缩路径,再加一种轻量补充。

自动压缩(autoCompact):对话的 token 数接近上下文窗口上限时,系统自动触发。源码里定义的精确阈值:

AUTOCOMPACT_BUFFER_TOKENS = 13_000
autoCompactThreshold = effectiveContextWindow - 13_000

也就是说,当 token 用量达到「有效上下文窗口 − 13000」时触发自动压缩。对于 200K 的上下文窗口,大约在 187K 时开始压缩。13000 token 的缓冲区给系统足够的空间完成压缩操作本身。

手动压缩:用户执行 /compact 命令时触发,通常在感觉对话变慢、或想清理上下文时使用。

Micro Compact:还有一种更轻量的压缩方式——不压缩整个对话,只针对单个工具的返回结果做清理(比如一次 grep 返回几百行,压缩后只保留关键发现)。

源码还提到了两种额外的压缩模式:

  • 反应式压缩(reactive compaction)——API 返回 context too long 错误时触发
  • 上下文折叠(context folding)——一种实验性的更高效压缩方式

这些模式和自动压缩互斥,同一时间只有一种生效。

9 段式结构化压缩

本节源码:src/services/compact/prompt.ts

压缩不是随便「总结一下」。源码的 compact/prompt.ts 定义了一个严格的 9 段式结构化提取模板,模型必须按以下格式输出:

#段落内容重要性
1Primary Request and Intent用户的所有显式请求和意图最高
2Key Technical Concepts讨论过的技术概念、框架、技术
3Files and Code Sections具体的文件名、代码段、修改记录
4Errors and Fixes遇到的错误和修复方法中高
5Problem Solving解决问题的过程和正在进行的排查
6All User Messages所有非工具结果的用户消息最高
7Pending Tasks明确被要求但尚未完成的任务
8Current Work压缩前正在做的工作的详细描述最高
9Optional Next Step下一步计划(必须与用户最近的请求直接相关)

注意第 6 段、第 8 段、第 9 段的「最高」加粗——这是压缩的精华,也是最容易出问题的地方。

第 6 段:所有用户消息必须完整保留

List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.

为什么? 因为用户的每一句话都可能包含隐含的偏好和修正。

举个例子:在第 3 轮对话中,用户说了一句「别用 var,用 const」。如果压缩时丢掉了这句话,AI 在后续的代码中可能又开始用 var。用户以为 AI 已经学会了,实际上那条修正是「被遗忘的」。

用户的消息是压缩中损失最大的部分,因为它们不仅是信息,还代表了意图和约束。

analysis + summary 的两阶段压缩

本节源码:src/services/compact/autoCompact.ts

压缩过程分为两个阶段:

  • analysis 阶段——模型先在标签内做自由的分析思考,确保覆盖了所有要点。这个阶段的输出不会被保留到最终摘要里
  • summary 阶段——基于 analysis 的结果,按 9 段格式输出结构化摘要,成为下一轮对话的初始上下文

为什么需要两阶段?直接要求模型输出结构化摘要,容易遗漏细节。analysis 阶段让模型先「自由思考」一遍,确保不遗漏,然后再格式化输出。

源码注释还透露了一个具体的工程难题:

// Aggressive no-tools preamble. 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).

翻译:压缩时模型只有 1 轮输出机会。如果它尝试调用工具(被拒绝),不会产生任何文本输出,压缩就失败了。Sonnet 4.6 的失败率是 2.79%(4.5 只有 0.01%),所以他们加了一段非常激进的「禁止调用工具」前言来降低失败率。

这个细节暴露了一个真实的工程难题:不同模型版本行为不同,需要不同的 prompt 策略。模型升级不是免费的午餐——新版可能引入新问题。

第 9 段的防漂移设计

本节源码:src/services/compact/prompt.ts

第 9 段(Next Step)的 prompt 里有一段很长的警告:

IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests. Do not start on tangential requests or really old requests that were already completed without confirming with the user first. 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.

这段规则解决的是任务漂移问题——压缩后,模型可能根据摘要里提到的某个旧任务开始工作,而不是继续用户最近的请求。要求 verbatim quotes,用原始对话锚定模型的理解,防止每次压缩都偏移一点点,多轮之后完全跑偏。

为什么要求「直接引用」而不是「描述/总结」?

因为压缩本质上是一次「翻译」:把长对话翻译成短摘要。翻译会丢失细微差别。用户说「把按钮改小一点」,如果压缩成「调整按钮样式」,模型可能连颜色一起改了。直接引用原文就是用原始文本锚定模型的理解,防止多次压缩累积偏移。

Partial Compact vs Full Compact

本节源码:src/services/compact/autoCompact.ts

源码中 Partial Compact 和 Full Compact 的 prompt 有微妙的差异:

  • Full Compact 的 prompt 说 "create a detailed summary of the conversation"
  • Partial Compact 的 prompt 说 "summary of the RECENT portion of the conversation — the messages that follow earlier retained context"

这个用词不是随意的。它告诉模型:前面的消息还在上下文里,你只需要总结新增的部分,不要重复已经存在的内容。既省 token,又避免摘要的冗余。

Micro Compact:工具结果的精细压缩

本节源码:src/services/compact/microCompact.ts

前面讨论的 Full Compact 和 Partial Compact 都是对整个对话的「大型手术」。但 Claude Code 还有一种更精细的「微手术」:Micro Compact,定义在 microCompact.ts

它不压缩整个对话,只针对单个工具的返回结果做清理,把旧的工具结果替换成一条简短的占位信息:

const TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]'

不是所有工具结果都会被清理。源码里有一个白名单:

const COMPACTABLE_TOOLS = new Set([
FILE_READ_TOOL_NAME, // 文件读取
...SHELL_TOOL_NAMES, // Shell 命令
GREP_TOOL_NAME, // 搜索
GLOB_TOOL_NAME, // 文件查找
WEB_SEARCH_TOOL_NAME, // 网页搜索
WEB_FETCH_TOOL_NAME, // 网页抓取
FILE_EDIT_TOOL_NAME, // 文件编辑
FILE_WRITE_TOOL_NAME, // 文件写入
])

两种触发方式

  • 基于时间的触发:如果用户离开一段时间(上次对话过去了很久),服务端 Prompt 缓存可能已经失效,整个前缀都要重新处理。这时候就「趁机清理」旧的工具结果,减少重新处理的 token 数。源码逻辑是:计算当前最后一条助手消息和现在的时间差,超过阈值就清理。

  • 基于缓存编辑的触发(Cached MC):更高级的方式,利用 API 的 cache_edits 功能直接在服务端删除缓存中的工具结果,不需要重新发送完整消息。这样既省 token,又不破坏已有的缓存。

Micro Compact 的设计哲学值得学习:

与其等到上下文快满了再做一次大手术(Full Compact),不如在平时就持续做小清理。这类似于内存管理中的「增量 GC」思想——频繁的小清理比偶尔的大清理对系统影响更小。如果你在构建长对话 AI 应用,这种设计哲学值得借鉴。

压缩的成本:信息必然丢失

本节源码:src/services/compact/autoCompact.ts

理解了压缩机制之后,有一个事实必须正视:无论压缩策略多精巧,信息丢失都是必然的

压缩的本质是用更短的文本表达更长的对话内容。9 段结构再详细,它也是让模型「选择忘掉什么」,而不是「什么都不忘」。

不同类型的信息有不同的丢失风险:

信息类型丢失风险原因
隐含的偏好用户说「别用 var」,这种 one-liner 很容易被压缩掉
尝试过但失败的方案Errors and Fixes 段会保留错误,但试过的方案细节容易丢
中间讨论过程讨论了 A、B、C 三种方案最终选了 B,压缩后可能只剩「选了 B」
代码的具体修改第 3 段保留文件和代码,但长达会话早期的修改可能被覆盖
用户的直接请求第 6 段要求完整保留所有用户消息,这是保护最严的部分
正在做的工作第 8 段要求逐字引用,保护力度强

如何减少压缩带来的信息损失

本节源码:src/services/compact/autoCompact.ts

理解了压缩机制,你就能反过来利用它:

  1. 把关键约束写进 CLAUDE.md —— CLAUDE.md 在系统提示中,压缩时根本不会碰它。如果你有一条规则需要整个项目期间都遵守(比如「使用 TypeScript」「函数命名用 camelCase」),写进 CLAUDE.md 比在对话里反复说更可靠。

  2. 在长会话中定期重申关键要求 —— 如果发现 AI 开始「忘记」某条规则,别生气。它可能刚经历了一次自动压缩,那条规则的原始上下文已经被摘要替换了。再说一遍就好

  3. 及时使用 /compact 而不是等自动触发 —— 自动压缩时你没法干预,手动压缩可以提示保留哪些/compact 命令支持附加说明,告诉模型这次要特别注意什么。

  4. 关注 /compact 后的第一轮回复 —— 压缩后的第一轮回复最容易出问题。如果发现 AI 理解偏了,立刻纠正——这一轮的纠正会成为压缩摘要的一部分。

  5. 大任务拆成多个会话 —— 压缩是对「单会话内」的妥协。如果任务复杂到需要多次压缩,不如拆成多个会话,每个会话的 CLAUDE.md 里写清上下文。压缩的摘要不如模型的完整上下文可靠

给 AI 产品开发者的教训

本节源码:src/services/compact/autoCompact.ts

模型升级需要 prompt 适配。 4.5 和 4.6 的失败率差异(0.01% vs 2.79%)说明,模型版本变化可能破坏已有的 prompt 策略。持续监控模型行为、及时调整 prompt,是一个长期的工程任务。

其他几条结构性建议:

  • 结构化压缩比自由总结可靠得多 —— 如果你让 AI「总结一下前面的对话」,它可能遗漏关键细节。给它一个明确的模板(哪些信息必须保留、哪些可以省略、保留到多细),结果会好得多。9 段式模板就是这种思路的具体化。

  • 防漂移需要主动设计 —— 每次压缩都可能引入理解的微小偏差,几次压缩之后偏差会累积。要求 verbatim quotes、限制下一步必须与最近请求相关,都是主动防漂移的措施,而不是默认行为

  • 用户消息是压缩中最不能丢的信息 —— 工具数据可以再查,代码可以再读,但用户说过的对话代表了意图和约束。丢失用户消息等于丢失对齐。从 9 段表中第 6 段被标为「最高」可以看出:这是产品设计层面的取舍,比工程实现更上层。