用 Claude Code 做用户模块重构,前 30 轮一切顺利,它清楚地记得数据库用的 PostgreSQL、接口风格是 RESTful、Redis 集群那天维护所以 session 要临时走 DB。到了第 50 轮左右,它突然问”找不到 MySQL MCP 信息,请你提供一下”。MCPSettings 就在文件中,它前面还知道这件事。上下文被撑爆之后,早期的关键约束在模型的注意力里被稀释了。这篇文章要拆的是 /compact 敲下去之后,Claude Code 内部到底跑了一套什么流程。
上下文窗口里都装了啥
在聊压缩之前,需要先明白上下文窗口的真实构成。Claude Code 每次发请求给 API 的时候,组装的内容远比”一段一段的对话”复杂得多。
| 组成部分 | 大致体积 | 说明 |
|---|---|---|
| 系统提示词 | ~20K tokens | 角色定义、行为约束、工具描述 |
| CLAUDE.md 指令 | 动态注入 | 用户级 + 项目级 + 目录级配置 |
| 对话历史 | 持续增长 | 用户消息 + 助手回复 + 工具调用 + 工具结果 |
| 工具定义 | ~15K tokens | 48+ 工具的 schema 描述 |
| 当前状态 | 较小 | TodoWrite 任务列表、最近文件读取内容 |
这里面的大头是对话历史。每多聊一轮就多几千 token,而且工具调用和返回结果的体积往往是对话本身的 3 到 5 倍。一个 Read 工具读个 500 行的文件就是 2 万多 token,读 3 次不同文件,光工具结果就占了 6 万 token。当 Claude Code 觉得空间快不够用了,它需要一套机制来清理,这就是 /compact 要做的事。
四层渐进式压缩管道
/compact 并不是简单地”把历史对话做个总结”。敲下这个命令之后,Claude Code 内部会跑一个四层管道,从轻量到重量逐级处理。系统会先尝试成本最低的手段,不够再加码,这和内存管理的思路类似:先回收缓存,再杀进程,最后才动用 OOM Killer。
| 层级 | 名称 | 核心策略 | 有损/无损 |
|---|---|---|---|
| 第 1 层 | Micro 压缩 | 截短单条超长工具返回 | 无损 |
| 第 2 层 | Snip 裁剪 | 去重 + 去冗余 | 无损 |
| 第 3 层 | Context Collapse | 分段折叠旧消息 | 有损 |
| 第 4 层 | Full Compact | 全量摘要替换历史 | 有损 |
Micro 压缩:不动缓存键的微创操作
最轻量的一层,核心特点是修改消息内容但不改变缓存键。Claude API 有 prompt caching 机制,连续两次请求的前缀相同时,第二次可以复用缓存来降低成本和延迟。Micro 压缩的改动是”无害的”,不会破坏缓存命中。
1 | // 简化逻辑 |
这里只动了 tool_result 的内容长度,消息的结构、顺序、角色都没变,缓存键依然匹配。这一层一般能释放 10% 到 20% 的空间,成本几乎为零。
Snip 裁剪:去重和去冗余
Micro 压缩处理的是单条消息的长度,Snip 压缩处理的是消息之间的重复。写代码的过程中,同一个文件经常被反复读取,不处理的话同一份文件内容可能在上下文里出现 3、4 次。Snip 的做法很直接:对于内容完全相同的重复读取,替换为 [File content already shown above];文件有过修改的新版本则保留原样,通过内容 hash 来判断。
除了去重,Snip 还会对几类特殊的工具输出做裁剪。Bash 命令的 stdout 如果特别长,只保留最后 N 行,因为尾部通常比头部更有信息量。Grep 结果如果太多,只保留匹配数量统计加前 20 条。Write 操作的结果直接替换成 [File written successfully],因为内容已经在文件系统里了。这一层的释放幅度通常在 15% 到 30%,取决于任务中重复读取文件的频率。
Context Collapse:分段折叠
前两层都是无损压缩,信息没丢只是变短了。从这一层开始进入有损压缩的领域。
Context Collapse 把早期的对话消息分组合并,每组生成一个小摘要。不是一口气全摘要,而是一段一段地折叠。第 1 到 5 轮可能折叠为”讨论了数据库选型,决定用 PostgreSQL”,第 6 到 10 轮折叠为”完成了认证模块设计,确定了 JWT 方案”,而最近的几轮原文保留。越老的折叠得越狠,越新的保留得越完整,因为第 3 轮对话的逐字记录大概率用不到,但第 25 轮的内容可能直接关系到当前工作。
折叠的粒度不是固定的,而是根据上下文压力动态调整。压力小的时候不折叠,压力大的时候折叠范围扩大。
Full Compact:全量摘要
如果前三层做完还不够,就到了最重量级的一步。Full Compact 把当前所有历史消息(包括已经折叠过的摘要)打包发给 Claude,让它生成一个全局摘要,然后用这个摘要替换掉所有历史消息,只保留最近的几轮对话原文。压缩前可能是 180K tokens,压缩后降到 30K tokens 左右。
这个全局摘要不是随便写的。Claude Code 会给模型一个专门的 compact prompt,指导它保留用户的原始目标和关键约束、已经做出的重要决策及其原因、已完成的步骤和修改的文件、正在进行但未完成的任务、遇到的问题和当前的解决方案。对于代码修改,要求保留具体的文件路径和关键实现逻辑,同时丢弃无关的闲聊和已经过时的讨论。
这一层能释放 60% 到 80% 的空间,代价是丢失细节。摘要中没提到的内容就真的没了。
压缩之后的上下文重建
压缩只是上半场,下半场是重建。压缩完成后,Claude Code 会做一系列恢复动作。
CLAUDE.md 中的指令始终在系统提示词里,不受压缩影响。但 Claude Code 会在压缩后重新检查 CLAUDE.md 有没有更新,如果在对话过程中改了 CLAUDE.md,compact 之后新内容会生效。
一个容易被忽略的细节是文件内容的重建。压缩后,之前读过的文件内容都丢了,在摘要里只保留了文件名和大致描述。如果模型在后续工作中需要这些文件的内容,它会重新发起 Read 调用。这就是为什么 compact 之后经常会看到 Claude Code 突然开始读一堆文件,它不是在犯傻,是在重建文件缓存。
TodoWrite 的任务列表是独立于对话历史存储的。Compact 之后,任务列表会自动恢复到压缩前的状态,哪些完成了、哪些进行中、哪些待办,一字不差。这也是为什么建议用 TodoWrite 来管理长任务的进度,它是 compact 之后最重要的状态锚点。
自动触发机制
/compact 是手动触发的,但实际使用中大多数压缩是自动触发的。Claude Code 会持续监控上下文的使用率,当达到一定阈值时自动启动压缩流程。
| 上下文使用率 | 动作 |
|---|---|
| < 60% | 不压缩 |
| 60%~75% | Micro 压缩 + Snip 裁剪 |
| 75%~85% | Context Collapse |
| > 85% | Full Compact(全量摘要) |
| 95%+ | 紧急压缩,激进裁剪 |
具体的阈值不是固定的,会根据模型的最大上下文窗口动态计算。200K 的模型和 1M 的模型触发点不一样。
这里有一个历史 bug 值得一提:Claude 3 Opus 的 1M 上下文窗口曾被错误地按 200K 计算,导致自动压缩触发得太早。用户明明还有 80 万 token 的空间,系统就开始压缩了。这个问题在 v2.1.117 才修复。
Hooks:压缩前后的扩展点
Claude Code 允许在压缩发生的前后插入自定义逻辑。
PreCompact Hook 在压缩执行前触发。它有一个关键能力:如果 hook 返回退出码 2 或者输出 {"decision":"block"},压缩会被取消。这在正在做关键操作、不希望压缩打断当前状态的场景下有用。Hook 的输出也会被附加到压缩流程中,但这些内容在生成摘要时可能会被改写或丢弃,所以不要把关键信息只放在这里。
1 | { |
PostCompact Hook 在压缩完成后触发,接收 trigger 字段("manual" 或 "auto")和 compact_summary 字段(压缩生成的摘要文本)。PostCompact 不能修改压缩结果,只能观察和利用。典型用法是把摘要写到日志文件里方便事后审计。
1 | { |
社区里有一个项目叫 Post Compact Reminder,它在 PostCompact hook 里把用户的关键偏好重新注入一遍,防止压缩后模型忘了用户的习惯。思路很巧妙。
隐藏参数和实战技巧
/compact 支持带参数,比如 /compact 保留数据库选型和接口设计的讨论细节。这个参数会作为压缩指引传给生成摘要的模型,告诉它在做摘要时特别注意保留这方面的信息。在做复杂重构、涉及很多细节决策的时候,主动指引一下可以防止默认摘要把这些内容丢掉。不过指引太长反而会稀释其他重要信息的权重,简短精确地一句话说清要保留什么,效果最好。
关于什么时候该主动 compact,有几条经验。感觉模型变慢了,或者模型开始重复之前的建议,都是上下文快要溢出的信号。对话超过 30 轮就应该考虑 compact,在一个大阶段完成时做 compact 效果尤其好,因为阶段切换时的摘要天然比较完整。主动 compact 的摘要质量通常比自动触发时更高,因为自动触发时上下文已经快满了,模型用来思考摘要的空间本身就不够。
在敲 /compact 之前,花 30 秒做几件事会明显提升压缩效果。用 TodoWrite 更新任务进度确保当前状态被记录下来,把关键决策写进 CLAUDE.md 让这些内容不依赖对话历史,确认重要文件内容已经保存因为 compact 后模型需要重新读取。
CLAUDE.md 里可以加一个专门的 Compact Instructions 段落来指导压缩行为:
1 | ## Compact Instructions |
这些指令在每次压缩时都会被传给摘要模型,相当于给摘要加了个白名单。
还有一个坑要避免:不要在 compact 之后立刻又大量读文件跑命令。如果上下文很快又满了,系统会再次触发压缩。连续 3 次压缩后上下文立刻又满的话,Claude Code 会停止压缩并给出错误提示,而不是陷入死循环。这个 bug 在 v2.1.79 之前是真实存在的,系统会陷入压缩然后填满、再压缩再填满的死循环。现在加了保护,但了解这个历史还是有用的。
信息丢失和补救
compact 一定会丢信息,摘要不可能 100% 还原原始对话。通常丢失的东西包括具体的代码细节(只保留了”修改了 xxx 文件”但具体改了什么丢了)、早期的讨论过程(为什么选方案 A 不选方案 B 的推理过程)、工具调用的完整返回值、以及图片和截图内容。
最直接的补救方式是用文件代替记忆。重要的代码、配置、设计文档直接写到文件里,compact 后模型可以重新读取,不要依赖模型还记得。TodoWrite 的内容是独立存储的,不随 compact 丢失,可以把它当成外部状态存储。硬约束类的信息比如”用 PostgreSQL 不用 MySQL”写进 CLAUDE.md 最保险,它在每次请求都在系统提示词里,不可能被压缩掉。Compact Instructions 则告诉摘要模型哪些东西不能丢,比事后发现丢了再补要高效得多。
compact 机制的设计优先级很清晰:能不动的不动,能少动的少动,必须动的尽量保留关键信息,关键信息之外再考虑细节。理解了这个优先级排序,就知道什么时候该 compact、compact 之前该准备什么、compact 之后该怎么快速恢复状态。