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

00 开场:一句“继续”,也可能消耗大量输入额度

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

明明只是让它“继续改”“再跑一下测试”“把刚才那个报错修掉”,输入框里就几个字。可消耗的额度还是让钱包有点受不了。

问题出在请求内容本身。

对 coding agent 来说,“继续”这两个字通常不会单独发送给模型。一次真实请求里,前面往往还有系统提示词、工具定义、项目规则、当前目录、历史对话、刚读过的文件、刚跑出来的测试结果,有时候还会带上一整套 MCP tool schema。

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

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

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

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

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

模型厂商有没有命中缓存,Agent 框架有没有在前缀里加入动态内容,中转站有没有把 cached input 按普通 input 收费,同一会话有没有被路由到不同 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 延迟也可能下降。

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

比如这些内容如果出现在请求靠前位置,并且每轮都变化,命中率就会受到影响:

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

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

比较稳妥的做法,是把长期稳定的内容放在靠前位置,把每轮变化的内容放到后面。工具 schema、系统规则、项目规则这类内容越稳定,缓存越容易发挥作用;时间戳、运行状态、临时提示这类内容越靠前,缓存越容易失效。

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

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

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

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

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

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

后面讲 Claude 的 cache breakpoint、5 分钟和 1 小时 TTL、OpenAI 的自动缓存、DeepSeek 的硬盘缓存,核心都围绕这一点展开:稳定前缀越清楚,缓存收益越容易计算;动态内容越早出现,账单越容易失控。

02 Claude 的缓存,控制很强,也很容易被打碎

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

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

tools → system → messages

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

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

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

一个简化示例:

{
  "model": "claude-sonnet-4-5",
  "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 Code CLI,情况会多一层封装。

我查了 Claude Code changelog 和本机更新后的 Claude Code 2.1.160 代码。changelog 里在 2.1.108 提到:新增 ENABLE_PROMPT_CACHING_1H 环境变量,用来启用 1 小时 prompt cache TTL;同时新增 FORCE_PROMPT_CACHING_5M,用来强制 5 分钟 TTL。后续 changelog 还修复过“1 小时 prompt cache TTL 被静默降级成 5 分钟”的问题。

在 2.1.160 的 CLI 代码里,cache_control 并非全局固定成 5m 或 1h。Claude Code 会先生成一个基础缓存配置:

function Ta({ scope, ttl } = {}) {
  return {
    type: "ephemeral",
    ...ttl && { ttl },
    ...scope === "global" && { scope }
  }
}

是否给某些缓存块传入 ttl: "1h",由另一个内部判断决定:

function yyH(querySource) {
  if (process.env.FORCE_PROMPT_CACHING_5M) return false
  if (process.env.ENABLE_PROMPT_CACHING_1H) return true
  // 之后再看账号状态、overage、querySource 和内部 allowlist
}

也就是说,Claude Code 发给 API 的 block 仍然是 cache_control,但 ttl: "1h" 是否出现,会受 CLI 自己的策略影响。ENABLE_PROMPT_CACHING_1H 可以打开 1 小时路径,FORCE_PROMPT_CACHING_5M 可以压回 5 分钟路径;没有强制环境变量时,CLI 还会按 query source、账号状态和内部配置决定哪些请求适合 1 小时缓存。

这也解释了为什么在 Claude Code 的用量里,看到的 TTL 不一定像手写 API 请求那样单一。最终响应 usage 里还可能拆出 cache_creation.ephemeral_1h_input_tokenscache_creation.ephemeral_5m_input_tokens,分别表示本轮写入 1 小时缓存和 5 分钟缓存的输入 token。

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

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

常见的失效来源包括:

这些变化单独看都不大,但它们如果出现在缓存断点之前,就会影响后面整段内容的复用。

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

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

这里有三个输入相关字段:

所以读 Claude 账单时,不能只看 input_tokens。完整输入规模要把这三项合起来看。真正影响成本的是它们分别落在哪种计价档位。

如果一轮请求里 cache_read_input_tokens 很高,说明大量重复上下文被复用。

如果连续多轮 cache_creation_input_tokens 很高、cache_read_input_tokens 很低,说明系统可能一直在写缓存,却没有有效读回来。

这类情况在 Agent 里不罕见。

比如一个框架每轮都把当前时间、剩余 token、cwd、git branch 写进靠前的 system prompt;或者 MCP 工具 schema 每轮顺序不稳定。用户看到的只是继续对话,Claude 看到的前缀已经变了,缓存自然很难稳定命中。

Claude 的强项在于控制感很强。你可以通过 breakpoint 明确告诉它哪些内容值得缓存,也可以通过 usage 字段看到缓存读写情况。

代价是,框架和应用必须认真管理上下文结构。

如果前缀管理做得好,Claude 的缓存非常适合长会话、代码仓库上下文、Agent 工具调用。

如果前缀每轮都变,缓存机制再强,也只能不断写入新缓存。

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

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

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

按 Anthropic 文档里的计费规则:

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

配置上,5 分钟 TTL 是默认值。只写 type 时,就是 5 分钟缓存:

{
  "cache_control": {
    "type": "ephemeral"
  }
}

1 小时 TTL 需要额外写 ttl 字段:

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

这个字段很重要。很多时候,讨论 5 分钟和 1 小时缓存,不能只讨论概念,还要看请求里到底有没有 ttl: "1h"。没有这个字段,走的就是默认 5 分钟路径。

不过 Claude Code CLI 用户还要多看一层:CLI 可能在不同 block 上写入不同 TTL。

Claude Code changelog 里,2.1.108 增加了两个环境变量:

本机 Claude Code 2.1.160 的代码里,cache_control 的生成逻辑也能看到这一点:基础函数会返回 type: "ephemeral",并在调用方传入 ttl 时附加 ttl 字段。另一个内部判断会先看 FORCE_PROMPT_CACHING_5M,再看 ENABLE_PROMPT_CACHING_1H,然后才进入 CLI 自己的 query source / 账号状态 / 内部配置判断。

因此,最新 Claude Code 里的 TTL 不能简单理解成“整个会话统一 5m”或“整个会话统一 1h”。更准确的说法是:Claude Code 会按自己的缓存策略给不同缓存块生成 cache_control,其中一部分可能是默认 5m,一部分可能带 ttl: "1h"

看账单时也要对应看 usage 里的拆分字段:

这两个字段比单纯看 cache_creation_input_tokens 更有用。前者说明写入了多少 5 分钟缓存,后者说明写入了多少 1 小时缓存。

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

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

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

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

比如:

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

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

如果一段内容只会用一次,1 小时写入成本就是纯额外成本。

如果前缀本身每轮都在变,1 小时 TTL 也救不了命中率。

如果应用只是短时间内连续调用,5 分钟 TTL 往往更划算。

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

原因也和前缀有关。长时间稳定的内容应该更靠前,例如工具定义、系统规则、项目规范;短时间稳定或变化频率更高的内容放在后面,例如最近对话、临时任务状态、当前用户问题。

一个比较合理的结构是:

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

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

对 Agent 框架来说,TTL 选择其实是在回答一个工程问题:这段上下文接下来会不会被多次用到,以及会在多长时间内被用到。

如果答案是“连续几分钟内反复用”,5 分钟通常够。

如果答案是“中间可能停二三十分钟,但还会回来继续”,1 小时才有价值。

如果答案是“这段内容每轮都变”,先别急着调 TTL,应该先处理上下文结构。

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

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

Claude 最近新增的 mid-conversation system messages,看起来像是一个系统提示词能力,实际和缓存关系很直接。

传统写法里,系统指令放在请求最前面的 top-level system 字段。这个位置适合放长期稳定的规则,比如角色设定、工具使用原则、安全边界、项目级约束。它靠前、稳定,后续多轮请求容易复用缓存。

问题出现在会话中途。

Agent 跑了一段时间后,经常会出现新的系统级约束:用户临时切换权限模式,工具可用性发生变化,剩余 token budget 降到某个阈值,应用发现磁盘文件已经变化,或者需要追加一条新的执行策略。

如果把这些新规则直接追加到 top-level system 字段,整个 system prompt 就变了。按照 Claude 的缓存顺序:

tools → system → messages

system 位于很靠前的位置。它一变,后面的 messages 缓存也会受到影响。对长会话和 Agent 来说,这意味着前面已经缓存好的历史上下文可能要重新处理。

Mid-conversation system messages 解决的是这个具体问题。

它允许在 messages 里追加一条 role: "system" 的消息,把中途新增的系统级指令放到它真正出现的位置,避免回头改最前面的 top-level system

一个简化结构是这样:

{
  "system": [
    {
      "type": "text",
      "text": "Initial stable system instructions...",
      "cache_control": { "type": "ephemeral" }
    }
  ],
  "messages": [
    { "role": "user", "content": "Start working on the repository." },
    { "role": "assistant", "content": "I will inspect the code." },
    {
      "role": "system",
      "content": "The user has switched to review-only mode. Do not edit files."
    },
    { "role": "user", "content": "Continue from here." }
  ]
}

这里的关键点是:前面的 tools、top-level system、已有 messages 都不用改。新增规则出现在消息历史后面,前面已经缓存的 prefix 仍然保持一致。

官方文档也明确提到:prompt caching 会按 toolssystemmessages 的顺序 hash 请求前缀;命中需要到 cache breakpoint 为止精确一致。修改 top-level system 会改变很靠前的前缀,导致 system prompt 和后续 cached messages miss。把新指令放到消息末尾,前面的缓存可以继续命中,只有新消息按 fresh input 处理。

这对 Agent 很有用。

比如 Claude Code 这类工具,运行过程中会不断接收新的运行状态:

这些信息如果都写回最前面的 system prompt,会让缓存很不稳定。放到 mid-conversation system message 里,至少不会破坏它之前已经稳定的那段前缀。

不过这个功能现在限制很明确。

根据 Anthropic 文档,它目前只支持 Claude Opus 4.8,不需要 beta header。平台侧也有限制:Claude API 可用,部分云平台还没跟上。

所以它现在仍然有明确适用边界。这里更适合作为缓存机制的一个新方向来看,不能直接当成所有 Claude 调用都可用的通用能力。

还有一个安全边界也要提。

Mid-conversation system message 依然是 system 级别指令。它适合放应用或操作员确认过的状态变化,不适合塞未经验证的工具结果、网页内容、检索文档或用户粘贴的大段文本。否则就相当于把不可信内容抬高成系统指令,提示注入风险会更高。

从缓存角度看,这个功能的价值很清楚:它让中途新增的系统级信息留在中途,避免回头改动最前面的稳定前缀。

这也是 Agent 上下文工程里很重要的一条原则:长期稳定的规则放前面,临时变化的信息放后面。Claude 把 mid-conversation system messages 做成一个独立能力,本质上是在给长会话和 Agent 一个更细的上下文更新位置。

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

OpenAI 的 prompt caching 更偏自动化。只要请求满足条件,系统会自动尝试复用前缀,不需要开发者手动写 cache breakpoint,也没有额外的 cache write 费用。

官方文档里的核心规则是:1024 token 以上的 prompt 自动进入缓存机制,命中依赖 exact prefix match。静态内容放前面,动态内容放后面,这个原则和 Claude 一样。

一个常见的 OpenAI 请求大概是:

[developer / system instructions]
[tools]
[conversation history]
[current user input]

如果 instructions、tools、历史片段保持一致,后面只是用户输入变化,缓存就有机会命中。工具定义顺序、结构化输出 schema、前置指令每轮变化,都会影响缓存效果。

OpenAI 的 usage 字段主要看 cached_tokens

{
  "usage": {
    "prompt_tokens": 2006,
    "completion_tokens": 300,
    "total_tokens": 2306,
    "prompt_tokens_details": {
      "cached_tokens": 1920
    }
  }
}

cached_tokens 表示这轮输入里有多少 prompt token 命中了缓存。请求低于 1024 token 时,这个字段也会出现,但值是 0。

和 Claude 相比,OpenAI 给开发者的控制更少。Claude 会拆出 cache_creation_input_tokenscache_read_input_tokensinput_tokens,还可以用 cache_control 管断点和 TTL;OpenAI 主要暴露 cached_tokens,能看命中数量,但很难精确控制哪一段写缓存、哪一段用更长保留时间。

OpenAI 还有一个关键参数:prompt_cache_key

OpenAI 会根据 prompt 初始前缀的 hash 路由请求。如果提供 prompt_cache_key,它会和前缀 hash 一起影响路由。对共享长前缀的产品流量,这个参数很重要。

比如一个企业应用里,很多用户共享同一套系统提示词、工具定义和策略说明。稳定使用 prompt_cache_key,可以让这些相同长前缀的请求更容易路由到合适的位置。

但粒度要控制。官方文档提到,如果同一个 prefix + prompt_cache_key 组合超过大约每分钟 15 个请求,部分请求可能溢出到其他机器,缓存效果会下降。太细,共享前缀不够集中;太粗,高峰期容易溢出。

缓存保留策略现在也有两个方向:in-memory 和 extended retention。

in-memory 缓存一般在不活跃 5 到 10 分钟后失效,最长大约 1 小时。extended retention 可以保留更久,部分模型最长到 24 小时。配置字段是 prompt_cache_retention

{
  "model": "gpt-5.5",
  "input": "Your prompt goes here...",
  "prompt_cache_retention": "24h"
}

保留时间更长,不代表稳定命中。exact prefix match 仍然是前提,路由和并发也会影响缓存效果。

cached tokens 仍然计入 TPM rate limits。缓存能降低延迟和输入成本,但不会让 rate limit 计算消失。

所以 OpenAI 缓存的优化重点很明确:

OpenAI 的优势是接入成本低、自动生效,适合产品化流量。短板是控制感弱:能看到命中了多少,但很难像 Claude 那样精确管理缓存断点和 TTL。

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 文档里提到三类持久化位置:

这几个规则解释了为什么 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:

deepseek-v4-pro 的 100 万 token:

这组价格放在一起看,差距非常夸张。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 这家公司也确实有点 AI 菩萨的味道。

它原本定价并不低,后来做过一轮优惠活动,价格打到 2.5 折,也就是原价的四分之一。更离谱的是,活动快结束时,它没有把价格拉回去,直接把原价改成了优惠价。对长期跑长上下文任务的人来说,这种定价方式比单纯发一轮限时优惠实在得多。

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

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

还有几个边界也要记住。

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

所以 DeepSeek 缓存的工程重点是:

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 里出现了两个环境变量:

这说明 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 编排。它不只是让主 agent 多想一会儿,而是让 Claude Code 更主动地拆任务、派子 agent、并行推进、再汇总结果。

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

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

如果一个任务本来只需要主会话连续处理,缓存还能沿着同一条前缀链往后走。进入多 agent 编排后,系统会同时生成多条上下文链,每条链都可能重新写缓存、重新读取项目规则、重新加载工具定义。再加上 agent 之间的 progress summary、结果汇总和后续 review,成本会明显上去。

这不是说 ultracode 不该用。它适合值得认真拆解的大任务。小修小改、单文件 bug、简单命令能解决的问题,用它反而容易把缓存和 token 都打散。

Claude Code 这些变化放在一起看,指向的是同一个工程问题:

Agent 框架要同时处理“能力”和“成本”。能力越强,越需要动态状态、工具调度、子任务、压缩和恢复;成本越稳,越需要稳定前缀、可控 TTL、少变的 tool schema、清楚的 usage 字段。

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

用户看到的是读文件、改代码、跑测试、拉 sub-agent。账单背后,是每一轮到底复用了多少稳定前缀,又因为多少动态状态重新计费。

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

上一节讲 Claude Code,是因为它有文档、有 changelog,也能看到它怎么处理缓存、MCP、compaction 和 subagent。

把视角换到 OpenCode、OpenClaw、Hermes 这类框架,问题会更普遍。它们不一定有 Claude Code 那么明确的缓存优化记录,但它们面对的工程约束差不多。

Agent 框架要做的事情太多了。

它要接模型,要接工具,要读文件,要跑命令,要处理 MCP,还要把项目规则、任务状态、历史消息塞进上下文。对用户来说,只是让 Agent 继续干活;对模型来说,每一轮请求都可能带着一大包上下文。

缓存问题就出在这里。

前面讲过,prompt caching 依赖稳定前缀。可 Agent 框架里,很多内容天生不稳定。当前目录会变,权限状态会变,工具列表会变,任务进度也会变。它们对 Agent 有用,但放得太靠前,就会影响后面的缓存复用。

MCP 会进一步放大这个问题。

一个 MCP server 接进来,可能带来一批工具。模型要调用工具,通常得先看到 tool schema。工具名、参数结构、说明文案,只要有变化,请求前缀就跟着变。

很多时候,用户根本没意识到这些变化。用户只看到 Agent 在读文件、跑测试、查资料,实际请求里已经多了不少工具说明和运行状态。

工具结果也很麻烦。

文件内容、测试日志、网页正文、搜索结果,这些东西都可能很长,而且每次不同。框架如果把它们长期留在历史里,后续请求就会一直带着这些动态内容走。缓存仍然可能命中,但命中条件会更苛刻。

compaction 和 subagent 也会改变成本结构。

compaction 可以把历史压短,省 token;代价是原来的历史被摘要替换,缓存前缀也会变。subagent 可以并行推进任务;代价是每个 subagent 都要有自己的上下文,工具、规则、任务说明都可能重新进入请求。

所以复杂 Agent 工具里的缓存表现,通常没有直连 API 测试那么稳定。

直连 API 做缓存测试,只要保持前缀一致,结果就容易解释。Agent 框架多了一层上下文组织逻辑,工具、状态、历史压缩、多模型兼容都会掺进来。最后账单变贵,不一定是模型没缓存,也可能是框架每轮都在改请求前缀。

这也是 provider-agnostic 框架的取舍。

Claude 有 cache_control 和 breakpoint,OpenAI 有自动前缀匹配和 prompt_cache_key,DeepSeek 有 hit/miss 字段。框架如果要同时兼容它们,就很难为某一家模型做得特别极致。

比较合理的做法,是把稳定内容固定住,把动态内容往后放。项目规则、常用工具说明、任务目标尽量稳定;当前状态、工具结果、临时摘要尽量后置;用不到的工具不要提前塞进上下文。

到这一层,缓存问题已经从模型文档进入框架工程。再往后看中转站,问题会更直接:上游到底有没有命中,usage 字段有没有透传,缓存省下来的成本最后算给了谁。

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。

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

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

所以这一节先不写测试方案。

判断中转站有没有把缓存收益还给用户,不能靠几句泛泛的“连续请求试一下”。真实情况会受模型、TTL、路由、上游账号、计费粒度、账单刷新延迟影响。测试设计不严谨,很容易把缓存没命中、账单没透传、路由打散、后台延迟这几件事混在一起。

这里先把原则说清楚:没有请求级 usage,没有缓存输入的计费说明,没有路由粘性说明,就很难对账。后面讲“怎么判断”时,再单独设计可执行的检查步骤,不把它揉在中转站这一节里。

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

前面讲了很多机制。真正落到使用上,不要一上来就问“它有没有缓存”。这个问题太粗。

更准确的问法是三层:有没有产生缓存字段,缓存字段有没有进入账单,账单有没有按缓存价格计算。

第一步,看接口返回。

具体字段前面已经讲过,这里不用再背一遍。关键是看它有没有把普通输入、缓存写入、缓存读取分开。

如果接口只返回一个总 input token,这条链路就已经少了一半信息。它可能真的没缓存,也可能缓存了但没透传。光靠总 token,判断不出来。

第二步,看账单口径。

接口里有缓存字段,不代表账单也按缓存算。很多平台会在 API 返回里保留 usage,但后台计费仍然只按总输入乘单价。

这时候要看账单有没有区分普通输入、缓存写入、缓存读取,或者至少有没有说明 cached input 的计费方式。

如果账单只写“输入 token 多少,单价多少”,那缓存收益到底给了谁,用户还是看不出来。

第三步,看连续会话里的比例。

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

这时再看缓存字段的变化。

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

但这里要注意,不能把一次测试看得太死。

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

这里还要留一个后面统一处理的点:有些 Agent 工具为了维持 5 分钟缓存,可能会产生很小的保活请求。如果后台请求记录里能看到这类请求,它也要算进成本。这个问题和 TTL 选择、Claude Code 行为更相关,后面顺稿时放回前面的 TTL 或 Claude Code 章节,不在这里展开。

第四步,看动态内容有没有打断前缀。

如果你在用 Agent 工具,先别急着怪模型。很多缓存失效来自请求组织方式。

比如每轮都把当前时间、git 状态、任务进度放在很前面;每轮都重新生成 tool schema;MCP 工具列表顺序不稳定;测试日志、网页正文、搜索结果长期挂在历史里。这些都会影响前缀复用。

这类问题在 Claude Code、OpenCode、Hermes、OpenClaw 里都可能出现,只是暴露程度不同。

第五步,看中转站有没有改写或打散请求。

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

如果用户侧请求稳定,但 usage 里长期看不到缓存收益,就要把中转站也纳入排查,不能只盯着模型本身。

第六步,别只看总价。

总价便宜,有可能是模型本来便宜;总价贵,也可能是输出 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 讲起,最后落到的其实不只是一个 API 功能。

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

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

Coding 和 Agent 场景完全不同。

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

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

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

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

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

这也是为什么中转站问题会被放到这篇文章里。

中转站本身没有原罪。很多人用中转站,是因为官方账号、支付、网络、额度、模型聚合都有门槛。问题在于,中转站如果不透传 usage,不说明缓存输入怎么计费,不保证同一会话的路由稳定,用户就很难判断真实成本。

更麻烦的是 Agent 框架。

Agent 框架越强,越容易引入动态上下文。工具定义、MCP、compaction、subagent、运行状态,这些都会影响缓存。框架如果只追求“能做更多事”,不处理上下文稳定性,最后成本会被复杂度吃掉。

一个成熟的 Agent 或 AI Gateway,应该把缓存放进成本治理里。

至少要能回答几个问题:

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

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

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

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

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

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