AI Pulse
📡 X 信号

AI Agent上下文压缩时,怎么省Prompt缓存的钱?

agent 压缩上下文的那一下,经常是整段对话里最贵的一次请求。这时候上下文已经快满了,摘要请求要是没命中 prompt cache,就等于把十几万 token 按原价重新读一遍。

我在给自己的 SSH 客户端 Hatoba 做 AI 助手,第一版就踩了这个坑。压缩时换了一个不带工具的系统提示词,tools 也去掉了,历史里的工具调用还被压成了文本,从第一个 token 开始就和缓存对不上。

所以我翻了一圈 Claude Code、Codex、Qwen Code、OpenCode、Cline、Roo Code、pi、Gemini CLI 和 Aider 的源码、文档和 issue,看看大家是怎么处理的。

先说原理。prompt cache 是前缀匹配,Anthropic 的顺序是 tools、system、messages,前面任何一个字节变了,后面全部失效。给摘要单独写一个系统提示词,或者把工具删掉,等于主动放弃整段缓存。

Anthropic 文档还写明了,改 tool_choice 也会让 messages 部分的缓存失效,所以想用 tool_choice 禁止模型调工具同样不行。

看下来大家分成两派。

一派把压缩请求当成主请求的分叉,也就是把上一次正常请求原样复制一份,工具、系统提示词、完整历史、thinking 设置都不动,只在最后多加一条总结指令。

Claude Code 官方文档写的就是这种做法。指令附在最后一条 user 消息里,工具也留在请求里,只在文字上要求模型只输出文本、不要调工具。缓存还热的时候,/compact 基本只花输出的钱。

Qwen Code 为这件事专门写了设计文档。主路径用同样的系统提示词、工具和完整历史,末尾加一句不要调用工具。模型如果还是调了工具或者回了空,就丢掉结果,退回不复用缓存的独立请求再试一次。PR 里一个测试样例的缓存命中率从 0% 到了 96.7%。

Codex 在 OpenAI、Azure、Bedrock 上走服务端压缩,请求带同样的工具、instructions 和 cache key,末尾加一个 compaction_trigger,服务端返回一段加密的摘要。

旧版 Cline 更直接,把总结指令贴在下一次正常请求后面,让模型用 summarize_task 工具交回摘要。

另一派单独发一个摘要请求,用一段专门的短系统提示词,不带工具,历史要么压成一段文本,要么改写过。OpenCode v1、现在的 Cline SDK、Roo Code、pi、Gemini CLI、Aider 都是这样。

Gemini CLI 还会再调一次模型,检查摘要有没有漏东西。Codex 给其他服务商用的本地压缩也把 tools 去掉了。

比较有意思的是几个半路翻车的例子。

OpenCode V2 修过一次,PR 里明确说之前的压缩请求换了系统提示词和工具集,用不上热缓存,于是改成了共享。但它只把要被总结的那部分旧历史发出去。

有用户在 issue 里统计了 132 次压缩,缓存读取的中位数只有 4% 到 7%,按 Anthropic 计价花了 70.51 美元,如果都按缓存读取算只要 8.17 美元。原因维护者还没确认,报告人怀疑就是只发了半段历史,缓存断点和主循环对不上。

Claude Code 自己也翻过车。有人在 issue 里抓包发现,开了 advisor 工具之后,压缩请求和主循环的工具数量差了一个(66 和 67),system 也有一块不一样,每次压缩都把整段对话重新写一遍缓存,后来才修掉。

Anthropic 自己 SDK 里之前的客户端压缩,摘要请求连 system 和 tools 都不带,现在已经删掉,换成了服务端压缩。

pi 则是故意不用缓存。它给摘要请求关掉了缓存写入,代码注释的意思是一次性的摘要没必要写缓存。它也试过复用缓存的做法,第二天就回滚了,原因没写。

现在有个 PR 把它做成可选,而且要先满足几个条件:模型和 thinking 设置一致,距离上次请求没超过缓存有效期的 90%,窗口放得下,估算下来比单独请求便宜。缓存已经过期的话,分叉请求读不到任何东西,还要多付一笔写缓存的钱,pi 加这些判断就是为了避开这种情况。

想少踩坑的话,下面这几条可以直接照着做。

压缩请求就是上一次正常请求加一条指令。tools、system、完整历史、模型、thinking 配置逐字节不动,指令放在最后。

不要靠去掉工具或者改 tool_choice 来禁止调工具。用文字要求,再加兜底,模型调了工具或者回了空就丢掉结果,换独立请求重试。

发完整历史,别只发要被总结的那一段。

别为了省钱换小模型来压缩,缓存是按模型分的。Roo Code 干脆删了自定义压缩模型,理由是只有对话本来的模型才总结得准。

压缩请求是一次性的,就算要写缓存也用 5 分钟 TTL,Claude Code 就是这么分的,主循环用 1 小时,压缩用 5 分钟。缓存已经冷了的话,可以像 pi 那样直接走不写缓存的独立请求。

阈值要给摘要输出留空间,用窗口减去一个预留量,别写死一个百分比。OpenCode V2 是窗口减 max(10%, 16k),pi 是窗口减 16384,Qwen 是 85% 且不超过窗口减 33k。

压缩之后的历史反正是冷的,系统提示词一定要冻结,至少 tools 和 system 还能命中。摘要用 user 消息注入,外面包一层说明,这是之前对话的摘要,不是新指令。摘要里很可能带着网页和命令输出的原文,不包这一层,它们就成了用户说的话。

清理旧的工具输出也是在改历史,从改动的位置开始缓存全部失效。要清就攒着一次清一批,Qwen 一次清到阈值的一半,免得每一轮都破坏前缀。

用数据验收。看压缩请求的 cache_read_input_tokens,有条件就像 Codex 那样写个测试,直接比对压缩请求和上一次正常请求的 body。

出处: Claude Code 文档 Qwen Code 设计文档 OpenCode 缓存统计 issue pi 可选复用缓存 PR Anthropic prompt caching

Hatoba

查看 X 原帖

订阅 AI Pulse

每天 08:00 · 12:30 · 18:30 · 23:50 更新