[功能请求] 每次压缩都会将约 97k 未变更的 token 重写进缓存,因为摘要被放在了最前面(在 10 智能体集群上每月浪费约 7 亿 token 和 6,700 美元)
预检清单
- [x] 我已搜索 已有请求,确认该功能尚未有人提议
- [x] 这是一项单一功能请求(而非多项功能)
问题描述
一个长会话在一个下午会进行多次自动压缩(auto compact),而每一次压缩都会丢弃约 97,000 个 token 的缓存——这些内容根本没有改变。同样的 CLAUDE.md、同样的技能(skills)、同样的工具 schema、同样的 MCP 指令,每次压缩时都会被重新发送并按写入(write)重新计费。
原因看起来在于组装顺序,而非任何昂贵的操作。压缩之后,重新构建的上下文把压缩摘要放在最前面,把稳定的内容放在它后面。提示缓存(prompt caching)按前缀匹配,因此摘要是必然会命中的缓存未命中(cache miss),而它后面的所有内容都会被写入。这些内容没有任何一部分依赖于它们前面的摘要。
我所发现的顺序,在每个压缩边界处读取会话记录(transcript)后得出:
system
compaction summary <- 每次都唯一,因此前缀在此处断裂
file attachments 会话已读取的文件
invoked skills
deferred tool schemas
agent listing
MCP server instructions
SessionStart hook output
next turn
CLAUDE.md 及其导入项 在下一次读取其管辖目录时重新挂载
下表来自一次真实会话的快照,使用 count_tokens 接口针对 Opus 统计,而非按字符数估算:
| 落在摘要之后的内容 | Claude token 数 | 压缩之间是否变化 |
|---|---|---|
CLAUDE.md 及其导入项 |
76,668 | 否 |
| 已调用的技能(invoked skills) | 9,410 | 否 |
| 延迟加载的工具 schema | 4,119 | 否 |
| 智能体列表(agent listing) | 3,873 | 否 |
| MCP 服务器指令 | 2,130 | 否 |
| SessionStart 钩子输出 | 1,297 | 否 |
| 会话已读取的文件 | 4,936 | 是,跟随读取内容变化 |
| 合计 | 102,433 |
其中约 97,500 个 token 在下一次压缩时与上一次相同。 它们位于一个按设计就唯一的块之后,因此每次都被作为重写(write)而非读取(read)。一个在下午自动压缩若干次的会话,每次都要为这同样的 97,000 个 token 支付一次缓存写入的费用,而内容从未改变。
其中 CLAUDE.md 占了四分之三。本项目的 CLAUDE.md 是一行指针,导入了一份共享的指令文件,这就是该链条达到这一规模的原因。
这些内容没有一项依赖于摘要。无论摘要是否在前面,它们都完全相同。
建议方案
按易变性(volatility)排序重建后的上下文,把最稳定的放在最前,让长前缀得以保留:
system
CLAUDE.md 及其导入项
invoked skills
deferred tool schemas
MCP server instructions
SessionStart hook output
agent listing
file attachments
compaction summary <- 唯一,因此断点应放在末尾
next turn
把摘要移到末尾不会丢失任何信息。无论如何它都是唯一的,无论如何都要产生一次写入费用。改变的是:位于它前面的约 97,000 个稳定 token 可以从缓存中读取。
智能体列表和文件附件故意放在该块的尾端。二者都不保证稳定:列表会在会话中途新增智能体时变化,文件集合也会在会话读取新内容时变化。把它们放最后意味着任一项发生变化时,前缀在此处断裂,而不是在指令文件和 schema 之前。
如果整体重排不便落地,一个更小的版本仍然有帮助:在同一会话的各次压缩之间,将这些重新挂载的块保持在固定的位置和固定的顺序,从而形成一个稳定区域,而不是随会话碰巧读取的内容浮动。
如果你们愿意接受来自外部的改动,我愿意提交 PR(pull request)。产出上表所用的脚本不大,我可以一并附上,这样「之前」与「之后」是可核验的,而非仅仅口头声明。
替代方案
缩减摘要之后的内容规模是有效的:少加载一些技能和工具,少往会话里读取内容。但这会把成本转嫁给用户,而把上面这些削减掉以避免为相同的 token 重复付费,都不像合理的取舍。
裁剪 CLAUDE.md 会带来线性收益,但顺序问题原封不动。指令文件之所以有这么大,是因为工作本身需要它。
优先级
高 — 对生产力有显著影响
功能类别
性能与速度
使用案例
- 十个智能体全天运行,每个大约每小时自动压缩一次。
- 每次压缩都先把摘要放在最前面,然后把技能列表、工具 schema、智能体列表和 MCP 指令放在它后面发出——与上一次完全相同。
- 下一次读取该项目时,会再次把
CLAUDE.md及其导入项挂在摘要之后。在本例中,这条链约 77,000 个 token。 - 每次压缩约有 97,500 个 token 被按缓存写入计费,而非缓存读取——而内容从未改变。
这相当于每天 240 次压缩、每天 2,340 万 token,以及每月 7.02 亿 token 的缓存写入,原本可以走缓存读取。
按公开费率计算,使用长时间运行的编程会话实际能够命中的 1 小时缓存 TTL(time-to-live,生存时间):
| 模型 | 缓存写入 | 缓存读取 | 每月浪费 |
|---|---|---|---|
| Sonnet 5 | 4.00 美元 / MTok | 0.20 美元 / MTok | 2,668 美元 |
| Opus 5 | 10.00 美元 / MTok | 0.50 美元 / MTok | 6,669 美元 |
换一种说法,在 Sonnet 上,这 7.02 亿 token 每月的写入费用为 2,808 美元,而按读取计费只需 140 美元。Opus 上编排者加 Sonnet 上工作者的混合集群,成本介于两行之间。
所有假设都是公开的,任何人都可以针对自己的部署重算:10 个并发智能体、每个智能体每小时一次压缩、24 小时、30 天、每次压缩 97,500 个未变化 token、公开的每百万 token 费率。关键变量是最后一项,而它是经过实测的,而非假设的。
完成重排之后,该前缀将在每次压缩时存活下来并从缓存中读取。只有摘要以及之后的会话轮次是新的。
补充说明
相关且描述同一领域中不同机制的引用:
-
94177 衡量的是缓存断开事件:TTL 到期、微压缩(microcompact)、会话恢复。本请求讨论的是单次压缩内部的组装顺序,属于另一类原因。
-
70459 描述的是陈旧的预计算保留了一大段原样前缀,并且该前缀是被创建进缓存(cache write),而非从缓存读取(cache read)。问题相邻,修复方式不同。
方法,以便可以核验数字。 会话记录位于 ~/.claude/projects/*/*.jsonl。找到携带 isCompactSummary 的记录,然后读取其后的附件记录。每一块都取自真正承载注入文本的字段:skills 中每个条目的 content、工具与智能体差异的 addedLines、MCP 的 addedBlocks、文件的 content.file.content,以及 nested_memory 记录的 content(用于指令文件)。Token 数来自针对 Opus 运行的 count_tokens 接口,作用于提取出的文本。
两处双重计数的陷阱——如果你对整条记录求和而不是选取具体字段,二者都会让数字虚高。nested_memory 记录可能对同一文件携带两次内容,一次作为 content,再一次作为 rawContent。hook_success 记录也会把其输出携带两次,一次作为 content,一次作为 stdout。
关于范围的说明:上述规模属于单个项目,指令文件较小的项目按比例支出更少。顺序问题是结构性的,与具体规模无关。无论稳定内容合计是多少,它目前都落在一个按设计就唯一的块之后,因此永远无法成为它本来可以成为的缓存命中。