[FEATURE] Every compaction re-writes ~97k unchanged tokens to cache because the summary is assembled first ( 700M tokens and $6.7k wasted per month on...

发布时间 2026-09-14 17 天前
来源 Claude Code(GitHub Issues · RSSHub)
字数 3,163 字
查看原文

补充自 2026年09月14日

AI智能总结

用户反馈 Claude 在长会话中频繁触发自动压缩,每次压缩都会把约 97,000 个未变更的 token 写入提示缓存,造成重复计费。

[功能请求] 每次压缩都会将约 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 会带来线性收益,但顺序问题原封不动。指令文件之所以有这么大,是因为工作本身需要它。

优先级

高 — 对生产力有显著影响

功能类别

性能与速度

使用案例

  1. 十个智能体全天运行,每个大约每小时自动压缩一次。
  2. 每次压缩都先把摘要放在最前面,然后把技能列表、工具 schema、智能体列表和 MCP 指令放在它后面发出——与上一次完全相同。
  3. 下一次读取该项目时,会再次把 CLAUDE.md 及其导入项挂在摘要之后。在本例中,这条链约 77,000 个 token。
  4. 每次压缩约有 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。

关于范围的说明:上述规模属于单个项目,指令文件较小的项目按比例支出更少。顺序问题是结构性的,与具体规模无关。无论稳定内容合计是多少,它目前都落在一个按设计就唯一的块之后,因此永远无法成为它本来可以成为的缓存命中。