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