draft · 仅 crs13 预览环境显示,正式站不会发布

【万字长文】LLM 缓存这笔账,藏着多少猫腻?

从 Claude、OpenAI、DeepSeek 到 Claude Code、中转站和 AI Agent 成本黑箱

00 开场,一句“继续”为什么也可能很贵

我第一次认真看 Claude Code 的用量,是因为它贵得有点离谱。

明明只是让它“继续改”“再跑一下测试”“把刚才那个报错修掉”,输入框里就几个字,额度却掉得很快。

问题出在请求内容本身。

对 coding agent 来说,“继续”这两个字通常只是最后追加的一小段。一次真实请求里,前面往往已经带着大量上下文。

用户新增输入只占最后一小段。真正占 token 的,常常是前面那批重复上下文。

如果这段重复输入命中缓存,后面几轮会便宜很多。比如 Claude 的缓存读取,价格可以低到普通输入的十分之一。没命中时,每一轮都可能按普通输入重新计费。

这也是 LLM 缓存容易被低估的地方。

很多人把它当成厂商文档里的一个底层优化,或者 usage 里的几个字段。到了 Agent 场景,缓存命中率会直接改变成本结构。同样是十万 token 的上下文,一轮按缓存输入读取,另一轮按普通输入计费,账单差异会非常明显。

这件事还很难只靠用户自己判断。

模型厂商有没有命中缓存,Agent 框架有没有在前缀里加入动态内容,中转站有没有把缓存输入按普通输入收费,同一会话有没有被路由到不同 key、workspace 或 region,都会影响最后的调用成本。

如果只看模型单价,很容易漏掉真正影响长会话成本的部分。Claude、OpenAI、DeepSeek 的标价只能说明一层问题。每一轮输入里有多少重复内容、重复内容有没有命中缓存、命中之后有没有体现在账单里,才是 Agent 成本里更关键的部分。

这篇文章就从这笔账开始拆。

先讲缓存到底缓存了什么,再看 Claude、OpenAI、DeepSeek 的规则差异;然后回到 Claude Code、OpenCode、Hermes、OpenClaw 这些 Agent 工具,分析它们为什么容易出现缓存命中率不稳定;最后再看中转站和模型网关里更敏感的一层:缓存省下来的钱,最后有没有算给用户。

01 缓存到底缓存了什么

要讲缓存,先得把它和几件容易混在一起的东西分开。

Prompt caching 的重点是输入前缀复用。它和模型长期记忆、语义相似检索属于不同问题。模型厂商发现这次请求的前半段,和之前某次请求的前半段一致,于是已经处理过的那部分输入可以用更低成本读取。

关键点在“前缀一致”。

一个简化后的 Agent 请求,大概可以拆成这样:

[tools]
[system]
[project rules]
[conversation history]
[tool results]
[new user message]

普通聊天里,用户可能不太关心这个结构。到了 AI Agent,前面的固定内容会变得很大,真正新增的内容反而经常很短。

真正新增的内容,很多时候只是最后那句用户指令。

缓存能省的,就是前面那批重复输入。

如果 [tools][system][project rules] 这些内容连续多轮保持一致,后续请求追加新消息时,前缀就有机会命中缓存。模型不需要每次都按普通输入价格处理这段内容,首 token 延迟也可能下降。

反过来,缓存也很容易被动态内容打断。

比如当前时间、随机 request id、重新排序的工具列表、动态拼接的 system prompt。如果这些内容出现在请求靠前位置,并且每轮都变化,命中率就会受到影响。

这类问题在 Agent 框架里很常见。用户并没有输入多少新内容,但框架每轮都在前缀里加入动态字段,导致本来可以复用的上下文被当成新输入处理。

所以,prompt caching 不能只看模型厂商有没有支持。Agent 怎么组织上下文,同样重要。

比较稳妥的做法,是把长期稳定的内容放在靠前位置,把每轮变化的内容放到后面。稳定内容越稳定,缓存越容易发挥作用;动态内容越靠前,缓存越容易失效。

还要注意一点:缓存主要影响输入侧。

模型最终输出的内容,每轮仍然要生成。缓存省的是重复上下文的读取成本和预填充延迟。它不会让输出免费,也不会自动提升回答质量。

因此,看缓存有没有发挥作用,不能只看总 token。更有用的是看输入 token 里有多少被缓存读取,有多少新写入缓存,有多少完全没有命中。

不同厂商给这些字段起的名字不一样。

Claude 里常见的是 cache_creation_input_tokenscache_read_input_tokensinput_tokens。OpenAI 会在 usage 里给出 cached_tokens。DeepSeek 会给出 prompt_cache_hit_tokensprompt_cache_miss_tokens

字段名字不同,真正要看的问题很直接:这一轮输入里,重复内容有没有被低价复用。

只要这个问题看不见,模型单价就只能解释账单的一小部分。长会话和 Agent 的真实成本,还取决于上下文怎么排、缓存怎么命中、命中之后有没有体现在账单里。

02 Claude 的缓存,控制强,也容易用错

Claude 的 prompt caching 很适合拿来讲 Agent 成本,因为它的机制足够明确,usage 字段也比较细。

Claude 缓存看的是请求里的有序前缀。按照 Anthropic 文档,缓存会按这个顺序引用内容:

tools → system → messages

这意味着,前面的内容变化,会影响后面内容的缓存。

如果 tool definition 变了,后面的 system 和 messages 缓存都可能受到影响。如果 system prompt 变了,后面的 messages 缓存也会受到影响。到了 Agent 场景,这一点很关键,因为最靠前的内容往往也最容易被框架动态拼接。

Claude 的缓存通过 cache_control 指定缓存位置。可以理解成告诉 API:到这里为止,这一段前缀值得缓存。

一个简化示例:

{
  "model": "claude-opus-4-8",
  "system": [
    {
      "type": "text",
      "text": "You are a coding assistant. Follow the project rules below..."
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Here is the repository context and coding rules...",
          "cache_control": {
            "type": "ephemeral"
          }
        },
        {
          "type": "text",
          "text": "Now fix the failing test."
        }
      ]
    }
  ]
}

重点是位置。cache_control 放在稳定内容之后,后续用户问题可以变化,前面的稳定内容还有机会被复用。

默认情况下,{"type":"ephemeral"} 对应 5 分钟 TTL。需要 1 小时 TTL 时,原始 API 请求里要显式加上 ttl 字段:

{
  "cache_control": {
    "type": "ephemeral",
    "ttl": "1h"
  }
}

所以,Claude API 层面要看的字段主要有两个:typettltype 表示这是临时缓存,ttl 决定缓存保留默认 5 分钟还是 1 小时。5 分钟是默认路径,1 小时需要明确配置,写入成本也更高。

Claude 对命中条件要求很严格。缓存断点之前的内容需要保持一致。哪怕语义差不多,只要文本、结构、顺序发生变化,都可能影响命中。

这对 Agent 框架提出了一个很直接的要求:稳定内容要真的稳定。

常见的失效来源包括 tool schema 变化、工具顺序变化、system prompt 加入动态状态、项目规则位置移动、工具结果结构变化。

这些变化单独看都不大,但只要出现在缓存断点之前,就会影响后续内容复用。

Claude 的 usage 字段比较适合拿来读账:

{
  "usage": {
    "input_tokens": 842,
    "cache_creation_input_tokens": 18640,
    "cache_read_input_tokens": 73210,
    "output_tokens": 1260
  }
}

这三个输入相关字段分别对应不同账本。

input_tokens 是本轮按普通输入计费的部分。

cache_creation_input_tokens 是本轮新写入缓存的部分,写入本身会有溢价。

cache_read_input_tokens 是本轮从缓存读取的部分,价格通常明显低于普通输入。

如果只看总 token,很容易误判。一次请求可能看起来输入很多,但大部分都是 cache read,实际成本并不高。反过来,如果 cache_creation_input_tokens 很高,cache_read_input_tokens 很低,就说明你一直在写缓存,很少读回来。

对 Claude 来说,最值得盯的是缓存写入和读取的比例,单看“有没有开缓存”意义不大。

写入高、读取低,说明前缀不稳定,或者复用次数不够。读取高、普通输入低,说明稳定前缀确实被复用了。这个差别,最后会直接体现在 Agent 账单里。

03 5 分钟和 1 小时,TTL 要按使用节奏选

Claude prompt caching 默认是 5 分钟 TTL,也可以选择 1 小时 TTL。

这两个选项的差别不只是缓存时间长短,还直接影响写入成本。

按 Anthropic 文档里的计费规则:

也就是说,写入缓存本身并不免费。1 小时 TTL 写入更贵,只有后续确实能多次读回来,才有意义。

5 分钟 TTL 适合高频连续会话。

比如 Claude Code 连续修 bug、跑测试、读文件、再改代码。用户和 Agent 都在几分钟内连续互动,前面的稳定上下文会被反复使用。只要缓存持续命中,5 分钟已经足够覆盖大部分连续工作流。

而且 5 分钟缓存命中后会刷新。只要会话没有长时间中断,它不一定会很快失效。

1 小时 TTL 更适合中间会有明显停顿的场景。

比如用户读完 Agent 输出后,过二三十分钟再继续;side agent 跑了一个长任务,主会话等待它返回;或者应用提前预热一段大型上下文,随后一段时间内反复使用。

这些场景里,5 分钟可能过期,1 小时 TTL 才能保住前缀复用。

但 1 小时 TTL 不适合无脑打开。

如果一段内容只会用一次,1 小时写入成本就是纯额外成本。如果前缀本身每轮都在变,1 小时 TTL 也救不了命中率。如果应用只是短时间内连续调用,5 分钟 TTL 往往更划算。

Claude Code 用户还要多看一层。

Claude Code changelog 里,2.1.108 增加了两个环境变量:ENABLE_PROMPT_CACHING_1H 用来启用 1 小时 prompt cache TTL,FORCE_PROMPT_CACHING_5M 用来强制 5 分钟 TTL。后续 changelog 还修复过“1 小时 prompt cache TTL 被静默降级成 5 分钟”的问题。

在 Claude Code 2.1.160 里,它并不会简单地把整个会话固定成 5m 或 1h。它会按自己的策略给不同缓存块生成 cache_control。usage 里也能看到 ephemeral_5m_input_tokensephemeral_1h_input_tokens 这类拆分。

还有一个需要单独留意的成本边界。

如果后台请求记录里能看到 Agent 工具为了维持 5 分钟缓存而产生很小的保活请求,这类请求也要计入成本。粗略算一下,如果一小时 12 次保活,每次读取一次缓存前缀,读缓存按 0.1x 算,就是 1.2x;再加上第一次 5 分钟 cache write 的 1.25x,总计约 2.45x。相比直接写 1 小时缓存的 2x,这种保活策略未必更便宜。

这只是成本敏感性分析,不能直接当作 Claude Code 的确定实现结论。实际还要看保活请求是否真的读取完整缓存前缀、是否产生输出、是否被中转站按原价计费。

Claude 还支持混用不同 TTL,但顺序有要求:长 TTL 内容要放在短 TTL 内容前面。

原因也和前缀有关。长时间稳定的内容应该更靠前;短时间稳定或变化频率更高的内容放在后面。

一个比较合理的结构是:

[工具定义 / 系统规则 / 项目规范]  →  1h cache
[近期对话 / 本轮任务上下文]        →  5m cache
[当前用户输入]                    →  no cache

这只是参考结构,方向很清楚:越稳定、越常复用的内容,越值得放到更长 TTL;越临时、越动态的内容,越应该靠后。

所以,TTL 不能只看时长。真正要看的,是写入成本、复用次数和会话节奏。

04 Mid-conversation system messages,为什么它和缓存有关

Claude 新增的 mid-conversation system messages,看起来是一个消息结构功能,实际和缓存关系很大。

传统做法里,system prompt 通常在请求最前面。它负责定义模型角色、行为边界、工具使用规则、开发规范。问题是,只要你改了最前面的 system prompt,后续整段 messages 的缓存都有可能受影响。

这在 Agent 场景里很常见。

任务中途,系统可能需要追加新规则。比如切换权限模式、加入临时约束、说明某个工具状态、补充安全边界。老做法很容易把 top-level system 改掉。对缓存来说,这等于在最靠前的位置动刀。

Mid-conversation system messages 的意义,是允许在 messages 中间追加 role: system

这样前面的 system、tools、历史消息可以保持不动,新规则作为后续消息进入上下文。前缀不变,前面的缓存就更容易继续复用。

一个简化结构:

[tools]
[top-level system]
[history already cached]
[new role=system message]
[new user message]

这比每次改 top-level system 更适合长会话。

它适合中途新增策略、权限提示、临时模式、任务约束。比如 Agent 先跑了一轮排查,中途发现要切换到更保守的修改模式,就可以追加一条 system message,不用重写最前面的 system prompt。

不过,这个能力不能乱用。

System message 权限很高,不应该塞入不可信内容。网页正文、工具返回、用户上传文件、外部日志,都不应该直接变成 system message。否则就会把不可信输入放到高权限位置。

按当时查到的 Anthropic 文档,mid-conversation system messages 支持 Claude Opus 4.8,支持 Claude API 和 Claude Platform on AWS,不支持 Bedrock、Vertex、Foundry。这个边界写进文章时要保守,因为平台支持范围可能变化。

从缓存角度看,这个功能的核心价值很明确:不要为了中途追加规则,去改最前面的稳定前缀。

长会话里,前缀越稳定,越容易复用。Agent 框架如果能把中途变化放到后续位置,就能少打碎已经建立好的缓存。

05 OpenAI 的缓存,自动省心,但控制感弱

OpenAI 的 prompt caching 走的是另一条路线。

它不要求开发者手动加 cache_control。对支持的模型来说,只要请求前缀足够长,系统会自动尝试缓存。

这对产品开发很友好。

你不需要像 Claude 那样手动决定 breakpoint,也不需要给内容块标记 TTL。对大量产品化请求来说,只要前面有稳定长前缀,比如固定系统提示词、产品说明、长文档开头,OpenAI 就有机会自动复用。

OpenAI 文档里强调的是 exact prefix match。也就是说,前缀要完全一致,才有机会命中。

缓存从 1024 token 以上开始生效。命中信息会出现在 usage 里的 cached_tokens。静态内容应该放前面,动态内容放后面。这些原则和 Claude 的前缀逻辑是一致的,只是控制方式不同。

OpenAI 还提供 prompt_cache_key

它的价值不在于强制缓存某段内容,而在于帮助平台把有相同长前缀的请求路由到更可能命中缓存的位置。对产品化流量来说,这个字段很实用。比如同一个文档、同一个客服知识库、同一个应用模板,可以用稳定的 cache key 提高复用机会。

但 OpenAI 的控制感也弱一些。

你不能像 Claude 那样精确指定缓存断点,也不能为不同片段手动设置 5 分钟或 1 小时 TTL。它适合结构稳定、交给平台自动优化的场景。

OpenAI 还有几个容易忽略的边界。

缓存 token 仍然计入 TPM 这类速率限制。也就是说,缓存可以降低成本和延迟,但不等于这些 token 从限流里消失。

缓存也有保留时间。文档里提到,缓存通常会在 5 到 10 分钟不活跃后失效,最长约 1 小时;部分模型有 extended retention,可以保留更久。这个范围适合连续会话和高频产品流量,不适合指望隔很久之后还能稳定命中。

所以 OpenAI 的缓存适合省心,但不要把它理解成无限期记忆。

对开发者来说,最重要的还是请求结构:稳定前缀放前面,动态字段放后面;能复用的长内容保持一致;需要共享长前缀的场景,认真设置 prompt_cache_key

06 DeepSeek 的缓存,硬盘缓存和极端价差

DeepSeek 的 Context Caching 很值得单独讲,因为它和 Claude、OpenAI 的体感差异很大。

Claude 需要管理 cache_control,OpenAI 自动做前缀缓存。DeepSeek 也默认开启,但它明确叫上下文硬盘缓存。用户不需要改代码,每次请求都会触发缓存构建;后续请求如果和之前请求有重叠前缀,重叠部分就有机会从缓存里读取。

DeepSeek 的关键点在于:前缀要先被持久化成一个完整的 cache prefix unit。

官方文档里说得很明确:后续请求只有完整匹配某个已经持久化的 cache prefix unit,才能命中缓存。

这和简单理解的“前面有一段相同就一定命中”有差别。

举个简化例子:

第一次:A + B
第二次:A + B + C

第二次可以命中 A + B,因为第一轮请求结束后,A + B 已经成为一个 cache prefix unit。

但如果是这样:

第一次:A + B
第二次:A + C

第二次未必能直接命中。因为之前持久化的是 A + B,而 A + C 没有完整匹配这个 unit。

不过 DeepSeek 会检测多次请求之间的 common prefix。前两次请求都带着 A,系统可能会把 A 持久化成新的 cache prefix unit。等第三次请求变成:

第三次:A + D

这时就有机会命中 A

这个机制对长文档问答、长上下文复用、Agent 连续任务都很重要。它意味着 DeepSeek 的缓存命中不只看“文本有没有重叠”,还要看这段重叠内容有没有被系统写成可匹配的缓存单元。

DeepSeek 文档里提到三类持久化位置:请求边界、多次请求里检测到的 common prefix、长输入或长输出里的固定 token 间隔。

这几个规则解释了为什么 DeepSeek 有时第一轮、第二轮看起来没命中,第三轮反而开始命中。缓存构建需要几秒钟,系统也会在后台整理 prefix unit。

DeepSeek 的 usage 字段很直观。

prompt_cache_hit_tokens 表示本轮输入里命中缓存的 token。

prompt_cache_miss_tokens 表示本轮输入里没有命中缓存的 token。

这两个字段比只看总 input token 有用得多。因为 DeepSeek 的 hit 和 miss 价格差距非常大。

按 DeepSeek 当前中文价格页,deepseek-v4-flash 的 100 万 input token:cache hit 是 0.02 元,cache miss 是 1 元,output 是 2 元

deepseek-v4-pro 的 100 万 token:cache hit input 是 0.025 元,cache miss input 是 3 元,output 是 6 元

这组价格放在一起看,差距非常夸张。Flash 的 hit / miss 输入价格差 50 倍;Pro 的 hit / miss 输入价格差 120 倍。

对 DeepSeek 来说,cache hit 会直接决定这一轮长输入是几分钱级别,还是几块钱级别。

我自己在五一期间跑过一批 DeepSeek 调用,总量大约 15 亿 token,最后花费不到 50 元人民币。平均下来,大约是 0.033 元 / 100 万 token

这个数字放在普通 input 价格下很难成立。它说明真正影响总成本的关键,已经从标价表上的 cache miss 单价,转向大量重复上下文有没有落到 cache hit 档位。

这也是为什么 DeepSeek 特别适合讨论中转站和网关账单。

如果网关只告诉你总 input token,不区分 prompt_cache_hit_tokensprompt_cache_miss_tokens,你根本看不出上游到底用了哪种价格。上游大量命中缓存,网关仍然按普通 input token 给用户计费,用户也很难从账单里发现。

还有几个边界也要记住。

DeepSeek 的缓存只匹配输入前缀,输出仍然每次生成,会受 temperature 等参数影响。它也不承诺 100% 命中,官方说是 best-effort。缓存不用之后会自动清理,一般是几小时到几天。

Claude 的缓存强调显式控制,OpenAI 的缓存强调自动路由,DeepSeek 是默认开启的磁盘 prefix 复用系统。它的使用门槛很低,但账单解释一定要看 hit / miss。对长上下文和 Agent 场景来说,这个差异会直接反映到成本上。

07 Claude Code 最近的缓存相关变化

Claude Code 这一段,不适合写成 changelog 逐条解释。

真正值得讲的是:Claude Code 正在从单个 coding agent,变成一个会调度工具、压缩上下文、切换模式、拉起子任务的工程系统。系统越复杂,越容易制造动态上下文;动态上下文越多,缓存越难稳定。

这就是 Claude Code 最近一系列缓存相关变化的共同背景。

一个 coding agent 的请求前缀,通常不只是 system prompt。它还包括工具定义、项目规则、CLAUDE.md、权限模式、MCP schema、当前目录、git 状态、上下文预算、历史消息和工具结果。这里面有些内容应该长期稳定,有些内容每轮都会变。

缓存优化的核心,就是把这两类东西分开。

Claude Code 在 TTL 上已经有明显策略化痕迹。2.1.108 里出现了两个环境变量:ENABLE_PROMPT_CACHING_1HFORCE_PROMPT_CACHING_5M。这说明 Claude Code 并没有把缓存时间当成单一默认值处理。

连续修 bug、跑测试、读文件,5 分钟通常够用。用户离开一段时间、side agent 执行长任务、远程会话恢复,1 小时缓存才更有价值。

后续修复过的几个问题,也都能放到这个逻辑里看。

比如 1 小时 TTL 曾经被静默降级成 5 分钟。用户以为自己保住了长会话缓存,实际中途停顿后可能重新写入。再比如 cache_creation_input_tokens 显示异常,会让用户误判缓存写入成本。sub-agent progress summaries 缺少 prompt cache,则会在多 agent 场景里放大 cache creation。

这些不只是 UI 或统计 bug。它们会直接影响用户对账单的判断。

系统提示词也是一样。

Claude Code 后来加过 --exclude-dynamic-system-prompt-sections,目标是改善跨用户 prompt caching。这个变化说明,system prompt 里有一部分内容本来就不该混在稳定前缀里。

比如当前时间、cwd、git 状态、权限模式、token budget、工具可用状态,这些信息对 Agent 有用,但每轮都可能变化。它们如果出现在很靠前的位置,会让原本稳定的 system prompt 变成动态前缀。

Tool schema 是另一类问题。

MCP server 一多,tool definitions 会很长,而且容易变化。Claude Code changelog 里提到过 MCP tool definitions deferred,也就是延迟加载 MCP 工具定义。这类改动的意义不只是少塞一点 token,它也减少了工具 schema 对缓存前缀的扰动。

/model 切换提醒也属于同一类问题。

Claude Code 会提醒用户:中途切换模型后,下一轮会重新读取完整历史,且没有缓存。这个提醒很重要。长会话里,切模型不只是换一个推理能力,也可能意味着原来的缓存链路断掉,下一轮输入成本突然变高。

Auto-compaction 则是另一个取舍。

压缩上下文可以避免撑爆 context window,也能减少后续输入长度。但 compaction 会改写历史,原来的 messages prefix 会变成摘要。它可能降低总 token,也可能打断原有缓存链。

所以 compaction 不会天然省钱。它要在“缩短上下文”和“保持缓存连续性”之间做取舍。

最近的 ultracode / orchestration mode,会把这个问题再放大一层。

ultracode 可以理解成高 effort 加自动多 agent 编排。它会让 Claude Code 更主动地拆任务、派子 agent、并行推进、再汇总结果。

这对复杂任务有价值,比如大型重构、深度 PR review、跨模块排查。但从缓存角度看,它会制造更多独立上下文:每个 sub-agent 都有自己的 prompt、工具、项目规则、历史片段和执行结果。它们不一定共享主会话已经命中的缓存,也不一定有相同的前缀结构。

换句话说,ultracode 能提高任务推进能力,也会放大 token 消耗和缓存不稳定性。

小修小改、单文件 bug、简单命令能解决的问题,用它反而容易把缓存和 token 都打散。

Claude Code 这些变化放在一起看,指向的是同一个工程问题:Agent 框架要同时处理能力和成本。能力越强,越需要动态状态、工具调度、子任务、压缩和恢复;成本越稳,越需要稳定前缀、可控 TTL、少变的 tool schema、清楚的 usage 字段。

缓存已经不只是 Claude Code 的附属优化。到了 coding agent 这个层级,它已经变成上下文工程的一部分。

08 为什么 Agent 框架天然容易缓存命中率不高

上一节讲 Claude Code,是因为它把缓存问题暴露得比较清楚。换到 OpenCode、OpenClaw、Hermes 这类框架,机制细节会不同,但压力来源差不多。

Agent 框架要同时处理模型、工具、文件系统、终端、MCP、历史消息和任务状态。能力越多,请求结构越复杂;请求结构越复杂,稳定前缀越难维护。

这就是它们比直连 API 更难稳定命中缓存的原因。

直连 API 的缓存测试很干净。只要前缀保持一致,第二轮、第三轮就容易解释。Agent 框架多了一层上下文组织逻辑,每轮到底塞了哪些工具、状态、摘要和历史片段,用户通常看不全。

MCP 是一个典型例子。一个 server 接进来,可能带来一批工具。模型调用工具前,需要先看到 tool schema。工具描述、参数结构、顺序只要变化,请求前缀就会变化。

工具结果也会带来同样的问题。文件内容、测试日志、网页正文、搜索结果,通常很长,也经常变化。框架如果把这些内容长期留在历史里,后续请求就会继续携带动态内容。

compaction 和 subagent 也不能只看能力提升。compaction 会把历史压成摘要,减少 token,同时改写原来的前缀。subagent 能并行推进任务,也会生成新的上下文和工具定义。复杂任务值得用,小任务硬拆就容易把成本打散。

所以 provider-agnostic 框架很难把某一家模型的缓存机制用到极致。Claude 有 breakpoint,OpenAI 有自动前缀匹配和 prompt_cache_key,DeepSeek 有 hit / miss 账本。框架为了兼容多家供应商,通常会牺牲一部分针对性优化。

比较合理的方向,是把稳定内容固定住,把动态内容往后放;用不到的工具不要提前暴露;压缩和 subagent 要有明确边界。

到这一层,缓存问题已经从模型文档进入框架工程。

09 中转站猫腻,缓存省下的钱到底给了谁

前面几节讲的是模型和 Agent 框架。到中转站这里,问题会变得更现实。

模型有没有缓存,是一回事。用户有没有拿到缓存收益,是另一回事。

Claude、OpenAI、DeepSeek 都会在 usage 里返回缓存相关信息。字段名字不同,但作用差不多:让你知道这轮输入里,哪些按普通输入算,哪些走了缓存优惠。

这些信息一旦经过中转站,就可能消失。

很多 OpenAI-compatible API 会把不同供应商的 usage 统一成一套简单格式。接入方便了,缓存账本也被压平了。用户最后只看到总 input token,看不到上游到底省了多少。

第一层问题由此出现。

上游可能已经按缓存读取收了便宜价,中转站仍然按普通输入给用户计费。它不需要改请求,只要账单不区分普通输入和缓存输入,用户就很难发现。DeepSeek 这种 hit / miss 价差很大的模型尤其明显。

第二层问题是路由。

Prompt caching 依赖前缀一致,也依赖请求落到能复用缓存的位置。中转站如果在多个 key、workspace、region、上游账号之间轮询,同一个会话的请求就可能被打散。

用户看到的模型名可能一直没变,比如 claude-opus-4-8gpt-5.5deepseek-v4-pro。但背后实际走到哪个上游渠道,用户通常不知道。

有时中转站自己也未必掌握完整链路。它上面可能还接了别的聚合层、云厂商入口、区域路由或备用渠道。模型名保持一致,不代表缓存位置也一致。

第三层问题是请求被中转站改写。

有些中转站会插入自己的 system prompt,改 tool schema,或者为了兼容 OpenAI-compatible 格式重排 messages。用户以为自己发出去的是原始请求,实际到上游时,前缀已经变了。

这和缓存直接相关。Prompt caching 看的是请求前缀,前缀被中转站改过,用户自己保持稳定也没用。更麻烦的是,这类改写通常不会出现在用户侧日志里。用户只能看到自己发给中转站的请求,看不到中转站转给上游的最终请求。

所以判断中转站,不能只看标价。

便宜单价没有意义,关键要看它怎么处理缓存账本。

至少看三件事。

第一,usage 字段有没有透传。最好能看到供应商原始字段,不能只给一个总 token。

第二,缓存输入怎么计费。上游低价读取的部分,有没有按低价算给用户。

第三,同一会话有没有路由粘性。连续任务里,请求最好固定在同一组上游资源上,不要每轮乱跳。

没有请求级 usage,没有缓存输入的计费说明,没有路由粘性说明,就很难对账。

这已经是成本解释权的问题。用户看不到上游 usage,也看不到最终请求,就只能相信平台给出的总价。

10 怎么判断自己有没有被缓存坑到

真正落到使用上,不要一上来就问“它有没有缓存”。这个问题太粗。

更准确的问法有三层:接口有没有返回缓存信息,账单有没有保留这些信息,计费有没有按缓存价格计算。

先看接口返回。

具体字段前面已经讲过,关键是看它有没有把普通输入、缓存写入、缓存读取分开。如果接口只返回一个总 input token,这条链路就已经少了一半信息。它可能真的没缓存,也可能缓存了但没透传。光靠总 token,判断不出来。

再看账单口径。

接口里有缓存字段,不代表账单也按缓存算。很多平台会在 API 返回里保留 usage,但后台计费仍然只按总输入乘单价。账单如果只写“输入 token 多少,单价多少”,缓存收益到底给了谁,用户还是看不出来。

然后看连续会话里的比例。

不要拿完全不同的请求去比。比较有意义的场景,是同一个项目、同一个任务、同一个会话里,前面大段上下文基本稳定,只在最后追加新问题。

正常情况下,后续几轮应该逐渐出现更多缓存读取,普通输入占比下降。如果每一轮都是大量普通输入,缓存读取几乎没有,就要怀疑前缀被打断了。

但一次测试不能说明全部问题。

不同供应商的缓存 TTL、最小 token 门槛、写入时机都不同。DeepSeek 还有缓存构建延迟。中转站也可能有账单刷新延迟。一次没命中,不等于链路有问题;连续多轮都看不到缓存痕迹,才值得追。

如果你在用 Agent 工具,还要看动态内容有没有打断前缀。

当前时间、git 状态、任务进度、tool schema、MCP 工具列表、测试日志、网页正文,都可能影响前缀复用。这类问题在 Claude Code、OpenCode、Hermes、OpenClaw 里都可能出现,只是暴露程度不同。

中转站也要纳入排查。

同一个 base url、同一个 key、同一个模型名,不代表上游缓存位置稳定。中转站可能在多个账号、workspace、region 或备用渠道之间调度,也可能插入自己的 system prompt,或者为了兼容格式重排 messages。

最后,别只看总价。

总价便宜,有可能是模型本来便宜;总价贵,也可能是输出 token 多。缓存主要影响输入侧,尤其是重复长前缀。判断缓存问题,要看输入里的普通部分和缓存部分,不能只看最终花了多少钱。

个人用户最该留意接口 usage、平台账单和请求间隔。团队还要记录 key、项目、用户、模型、base url、workspace、region。平台不能导出这些维度,排查会很难。

更靠谱的做法,是把每一轮请求当成一条账本记录:用了哪个模型,走了哪条链路,输入里多少是普通输入,多少是缓存输入,账单又按什么口径收钱。

只要这条账本断了,用户就只能看平台给出的总价。总价本身解释不了缓存。

11 模型选择和工程建议

讲到这里,模型选型就不能只看标价了。

长上下文和 Agent 场景里,真实成本大概由三件事决定:模型基础价格、缓存命中率、链路有没有把缓存优惠传给你。

举个粗算例子。

假设普通输入价格记作 1,缓存读取按 0.1 算。

一个中转站打 3 折,但实际缓存命中率只有 50%。这一轮输入的有效成本大概是:

0.5 × 1 + 0.5 × 0.1 = 0.55

再乘 3 折,就是 0.165

另一个中转站打 6 折,但缓存命中率能到 95%。有效成本大概是:

0.05 × 1 + 0.95 × 0.1 = 0.145

再乘 6 折,就是 0.087

看起来 6 折更贵,算完反而便宜得多。它大概只有前者的一半成本。

这就是为什么“几折中转”这个指标很容易误导人。折扣只作用在明面单价上,缓存命中率会直接改变计费基数。长上下文任务里,命中率差一点,价格排序就可能反过来。

所以模型和供应链选择,应该按场景看。

如果是重度 Agent、长会话、代码任务,Claude 仍然很合适。它的优势是可控:breakpoint、TTL、usage 字段都比较明确。代价是工程要求高。上下文放错位置,动态内容太靠前,TTL 选错,账单就会很难看。

如果是产品化调用,OpenAI 的自动缓存更省心。稳定前缀够长,系统会自动尝试复用。它适合结构稳定、调用规模大、工程团队不想手动管理缓存点的场景。

如果是价格敏感的长输入复用,DeepSeek 很值得看。它的缓存命中和未命中价差大,长文档、多轮问答、重复上下文任务会很受益。但前提是调用链路能看到缓存账本。字段不透传,优势就容易被中转站抹平。

Agent 框架的选型,也不要只看“支持多少工具”。

更应该看它怎么组织上下文:稳定规则会不会每轮重写,tool schema 能不能按需加载,MCP 工具会不会一次性全塞进去,compaction 有没有策略,subagent 有没有成本边界。

工具越多,能力越强,但请求也越容易变。缓存命中率最后看的是前缀能不能稳定,工具数量本身不能说明问题。

中转站也是一样。

便宜折扣只是入口。更重要的是三件事:usage 字段有没有透传,缓存输入怎么计费,同一会话有没有路由粘性。

如果一个平台给你 3 折,但每轮都打散缓存,或者把缓存输入按普通输入算,那它未必便宜。如果另一个平台折扣没那么狠,但路由稳定、缓存字段透明、账单能对上,长任务跑下来可能更省。

企业和团队还要多看一层治理能力。

至少要能按项目、用户、key、模型看到输入、输出和缓存相关统计。否则账单涨起来之后,只能看到“某个模型花了很多钱”,看不到钱花在普通输入、重复上下文、工具结果,还是路由打散上。

最后可以用一个简单判断框架。

短输入、低频调用,看模型单价就够了。

长上下文、多轮会话,要看缓存命中率。

Agent 和中转站场景,要看上下文组织和账单透明度。

到了团队规模,缓存就不能再当成模型附带功能了。它应该进入成本治理指标。否则你以为自己买的是便宜模型,实际可能是在为重复上下文、动态工具和不透明链路付钱。

12 结尾,缓存是长上下文时代的真实账本

这篇文章从 prompt caching 讲起,最后落到的是一套成本账本。

缓存决定的是:模型到底有没有反复读取同一批上下文,重复读取时按什么价格算,省下来的钱最后到了谁手里。

在普通聊天里,这个问题没那么明显。用户输入通常很短,成本压力更多来自模型输出。哪怕带一点历史消息,输入侧的重复成本也不一定是最主要矛盾。

Coding 和 Agent 场景完全不同。

这类任务经常是输入远大于输出。模型每轮都要处理大量已有上下文,最后可能只产出几行修改、一次命令结果,或者一个很短的下一步动作。输入侧占比被拉高以后,缓存命中率就会直接影响总成本。

长上下文越常见,缓存就越重要。

以后模型窗口会继续变大,Agent 能读的东西会越来越多,工具也会越来越多。上下文变长本身不可怕,可怕的是每一轮都把长上下文按普通输入重新付费。

所以看模型成本,不能只看价格表。

价格表告诉你的是单价。usage 告诉你的是这轮请求怎么计费。账单告诉你的是平台最后怎么收钱。三者对不上,用户就只能看一个总价,完全不知道钱花在哪里。

这也是中转站和 Agent 框架必须放进同一篇文章里讨论的原因。

中转站不透传 usage,不说明缓存输入怎么计费,不保证同一会话的路由稳定,用户就很难判断真实成本。Agent 框架只追求“能做更多事”,不处理上下文稳定性,最后成本也会被复杂度吃掉。

一个成熟的 Agent 或 AI Gateway,至少要能回答几个问题:

这些问题听起来细,但它们决定长期成本。

对个人用户来说,最实际的建议很简单:不要只看“几折”“不限量”“超低价”。长上下文和 Agent 场景里,要看 usage 字段,看账单口径,看连续任务里的缓存表现。

对团队来说,更要把缓存当成成本指标。否则预算超了以后,大家只能争论“模型是否太贵”,却看不到真正的原因可能是上下文组织太乱、工具塞得太多、路由不稳定,或者中转链路把缓存账本抹掉了。

缓存早就不只是省钱小技巧。

它是长上下文和 Agent 工具真正跑起来以后,最基础的一本账。

谁能看懂这本账,谁就能知道自己是在为模型能力付钱,还是在为重复上下文、不透明路由和无效工具调用付钱。