0%

Claude Code /compact 机制分析

用 Claude Code 写代码写到一半,突然感觉它”变笨了”——回复变慢、忘记之前说过的东西、开始犯低级错误。老手遇到这种情况,会默默敲一行 /compact,然后继续干活。今天我来分析一下 /compact 具体干了些啥。

从一个真实场景说起

我之前用 Claude Code 做一个用户模块重构,前 30 轮一切顺利,它清楚地记得数据库用的 PostgreSQL、接口风格是 RESTful、Redis 集群那天维护所以 session 要临时走 DB。

到了第 50 轮左右,它突然问我:”找不到 MySQL MCP 信息,请你提供一下?”

我:???(MCPSettings 就在文件中,你前面还知道呢?!)

上下文被撑爆了,早期的关键约束在模型的注意力里被”稀释”了。这就是为什么需要 /compact

但这篇文章不是教你怎么用这个命令——/compact ,这谁都会敲。我要拆的是它敲下去之后,Claude Code 内部到底跑了一套什么流程


一、先搞清楚:上下文窗口里都装了啥

在聊压缩之前,得先明白上下文窗口这个”桌子”上摆了什么东西。

很多人以为上下文就是一段一段的对话。但实际上,Claude Code 每次发请求给 API 的时候,组装的内容远比你想的复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──────────────────────────────────────────────────┐
│ 上下文窗口的真实构成 │
├──────────────────────────────────────────────────┤
│ │
│ ① 系统提示词(~20K tokens) │
│ 角色定义、行为约束、工具描述 │
│ │
│ ② CLAUDE.md 指令(动态注入) │
│ 用户级 + 项目级 + 目录级配置 │
│ │
│ ③ 对话历史 │
│ 用户消息 + 助手回复 + 工具调用 + 工具结果 │
│ │
│ ④ 工具定义(~15K tokens) │
│ 48+ 工具的 schema 描述 │
│ │
│ ⑤ 当前状态 │
│ TodoWrite 任务列表、最近文件读取内容 │
│ │
└──────────────────────────────────────────────────┘

这里面的大头是 ③对话历史——你每多聊一轮,就多几千 token。而且工具调用和返回结果的体积往往是对话本身的 3~5 倍。一个 Read 工具读个 500 行的文件,就是 2 万多 token。读 3 次不同文件,光工具结果就占了 6 万 token。

所以当 Claude Code 觉得”桌子快放不下了”,它需要一套机制来清理桌面。这就是 /compact 要干的事。


二、不只是摘要:压缩的四层管道

很多人以为 /compact 就是”把历史对话做个总结”。太简单了。

实际上,当你敲下 /compact 之后,Claude Code 内部会跑一个四层管道,从轻量到重量,逐级处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
敲下 /compact


┌─────────────────────┐
│ 第 1 层:Micro 压缩 │ ← 先试试轻量手段
└─────────┬───────────┘
│ 还不够?

┌─────────────────────┐
│ 第 2 层:Snip 裁剪 │ ← 去掉重复和冗余
└─────────┬───────────┘
│ 还不够?

┌─────────────────────────┐
│ 第 3 层:Context Collapse │ ← 分段折叠旧消息
└─────────┬───────────────┘
│ 还不够?

┌─────────────────────┐
│ 第 4 层:Full Compact │ ← 核弹级:全部摘要化
└─────────────────────┘

每一层都是前一层的”加码”。系统会先尝试成本最低的手段,不够再加码。这和内存管理的思路很像——先回收缓存,再杀进程,最后才 OOM Killer。

2.1 第 1 层:Micro 压缩——不动缓存键的”微创手术”

这是最轻量的一层。它的核心特点是:修改消息内容,但不改变缓存键

什么意思呢?Claude API 有 prompt caching 机制——如果你连续两次请求的前缀相同,第二次可以复用第一次的缓存,大幅降低成本和延迟。Micro 压缩的改动是”无害的”,不会破坏缓存命中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简化逻辑
function microCompact(messages: Message[]): Message[] {
return messages.map(msg => {
if (msg.type === 'tool_result') {
// 把超长的工具返回结果截短
if (msg.content.length > MICRO_COMPACT_THRESHOLD) {
return {
...msg,
content: msg.content.slice(0, KEEP_CHARS) +
`\n... [${msg.content.length} chars total, compacted]`
}
}
}
return msg
})
}

注意这里只动了 tool_result 的内容长度,消息的结构、顺序、角色都没变——缓存键依然匹配。

效果:一般能释放 10%~20% 的空间。成本几乎为零。

2.2 第 2 层:Snip 裁剪——去重 + 去冗余

Micro 压缩处理的是单条消息的长度。Snip 压缩处理的是消息之间的重复

写代码的过程中,同一个文件经常被反复读取。如果不处理,同一份文件内容可能在上下文里出现 3、4 次。Snip 的做法很直接:

1
2
3
4
5
第 3 轮:[Read] src/auth.ts → 500 行(完整保留)
第 8 轮:[Read] src/auth.ts → 500 行
→ 替换为:[File content already shown above]
第 15 轮:[Read] src/auth.ts → 520 行(内容有更新)
→ 保留(因为和第 3 轮不一样)

它不是无脑去重,而是会比较内容 hash——只有内容完全一样的才会被替换。文件有过修改的话,新版本会保留。

另外,Snip 还会对一些特殊的工具输出做处理:

  • Bash 命令的 stdout 如果特别长,只保留最后 N 行(通常尾部比头部更有信息量)
  • Grep 结果如果太多,只保留匹配数量统计 + 前 20 条
  • Write 操作的结果,直接替换成 [File written successfully](反正内容已经在文件系统里了)

效果:去重通常能释放 15%~30%,取决于任务中重复读取文件的频率。

2.3 第 3 层:Context Collapse——分段折叠

前两层都是”无损压缩”——信息没丢,只是变短了。从这一层开始,进入有损压缩的领域。

Context Collapse 的思路是:把早期的对话消息分组合并,每组生成一个小摘要。不是一口气全摘要,而是一段一段地折叠:

1
2
3
4
5
第 1~5 轮 → [折叠] "讨论了数据库选型,决定用 PostgreSQL"
第 6~10 轮 → [折叠] "完成了认证模块设计,确定了 JWT 方案"
第 11~15 轮 → [折叠] "实现了密码重置功能,遇到 SMTP 配置问题"
第 16~20 轮 → [原文保留]
第 21~25 轮 → [原文保留]

越老的折叠得越狠,越新的保留得越完整。这符合直觉——你大概率不需要第 3 轮对话的逐字记录,但第 25 轮的内容可能直接关系到当前工作。

折叠的粒度不是固定的,而是根据上下文压力动态调整。压力小的时候不折叠,压力大的时候折叠范围扩大。

2.4 第 4 层:Full Compact——“核弹级”全量摘要

如果前三层做完还不够,就到了最后一步——也是最重量级的一步。

Full Compact 做的事情是:

  1. 把当前上下文中所有历史消息(包括已经折叠过的摘要)打包
  2. 发给 Claude,让它生成一个全局摘要
  3. 用这个摘要替换掉所有历史消息
  4. 只保留最近的几轮对话原文
1
2
3
4
5
6
7
压缩前:
[摘要1] + [摘要2] + ... + 第 45~50 轮原文
→ 总计 180K tokens

压缩后:
[全局摘要] + 第 48~50 轮原文
→ 总计 30K tokens

这个全局摘要不是随便写的。Claude Code 会给模型一个专门的 compact prompt,指导它怎么组织摘要内容:

1
2
3
4
5
6
7
8
9
请对以上对话生成一份结构化摘要,务必保留:
1. 用户的原始目标和关键约束
2. 已经做出的重要决策及其原因
3. 已完成的步骤和修改的文件
4. 正在进行但未完成的任务
5. 遇到的问题和当前的解决方案

对于代码修改,保留具体的文件路径和关键实现逻辑。
丢弃无关的闲聊和已经过时的讨论。

效果:能释放 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
2
3
4
5
6
7
上下文使用率    动作
───────────── ────────────────────────
< 60% 不压缩
60%~75% Micro 压缩 + Snip 裁剪
75%~85% Context Collapse
> 85% Full Compact(全量摘要)
95%+ 紧急压缩,激进裁剪

具体的阈值不是固定的,会根据模型的最大上下文窗口动态计算。200K 的模型和 1M 的模型,触发点不一样。

有一个坑要注意:Claude 3 Opus 曾经出过一个 bug——它的 1M 上下文窗口被错误地按 200K 计算,导致自动压缩触发得太早。用户明明还有 80 万 token 的空间,系统就开始压缩了。这个问题在 v2.1.117 才修复。


五、Hooks:压缩前后的”钩子”

这是 Claude Code 的一个高级特性——你可以在压缩发生的前后,插入自定义逻辑。

5.1 PreCompact Hook:压缩前最后一道防线

1
2
3
4
5
6
7
8
9
10
11
{
"hooks": {
"PreCompact": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "echo '{\"decision\":\"block\"}' && exit 2"
}]
}]
}
}

PreCompact 在压缩执行前触发。它有两个关键能力:

能力 1:阻止压缩。如果 hook 返回退出码 2,或者输出 {"decision":"block"},压缩会被取消。这在什么场景下有用?比如你正在做一个关键操作,不希望压缩打断当前状态。

能力 2:注入额外上下文。Hook 的输出会被附加到压缩流程中。但要注意——这些内容在生成摘要时可能会被改写或丢弃,所以不要把关键信息只放在这里。

5.2 PostCompact Hook:压缩后的”备忘录”

1
2
3
4
5
6
7
8
9
10
11
{
"hooks": {
"PostCompact": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "echo 'Compact summary: ${COMPACT_SUMMARY}'"
}]
}]
}
}

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 秒做这几件事:

  1. 用 TodoWrite 更新任务进度——确保当前状态被记录下来
  2. 把关键决策写进 CLAUDE.md——这些内容不依赖对话历史
  3. 重要文件内容确认已经保存——compact 后模型需要重新读取

7.3 CLAUDE.md 里的 Compact Instructions

你可以在 CLAUDE.md 里加一个专门的段落,指导压缩行为:

1
2
3
4
5
6
7
## Compact Instructions
压缩时务必保留:
- 当前处理的任务和进度
- 数据库使用 PostgreSQL 15
- API 风格是 RESTful,不用 GraphQL
- Redis 集群维护期间 session 走 DB
- 已修改的文件清单

这些指令在每次压缩时都会被传给摘要模型。相当于给摘要加了个”白名单”。

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 之后该怎么快速恢复状态。

毕竟,工具再智能,也需要一个懂它的人来驱动。

欢迎关注我的其它发布渠道