1. Prompt Caching 到底在缓存什么,为什么你的账单没降下来
Prompt Caching 这个词最近在 AI 编程圈被提得很多,但真正落到账单上,很多人的感受是「我明明开了缓存,怎么费用没怎么变」。问题往往不在缓存本身,而在于没搞清楚它缓存的是什么、命中条件是什么。
先说结论:Prompt Caching 缓存的是请求前缀对应的 KV Cache,也就是模型在处理你这段提示词时算出来的中间注意力结果。它不是一个「语义缓存」,不会因为你换了个说法就命中;它认的是逐 Token 的前缀完全一致。你开头改一个标点、换一个空格、调整一下工具定义的顺序,哈希就变了,缓存直接失效。
它适合谁?适合那些每次请求都带着一大段稳定前缀的场景。典型的就是 Claude Code、Cursor、Cline 这类 AI 编程工具:系统提示词、工具定义、项目里的 CLAUDE.md、历史对话,这些内容在连续多轮里高度重复,天然就是缓存的最佳素材。反过来,如果你每次都是全新的、互不相关的一次性问答,前缀根本对不上,缓存命中率自然接近零。
我试过用同一段约 8000 Token 的长上下文,在开启和关闭缓存两种情况下各打两次请求,费用差异非常直观:第一次都是全量计算(cache write),第二次开启缓存的那条只按 cache read 计费,单价通常只有正常输入 Token 的十分之一左右,而未开启的那条第二次依然是全量输入价。这就是为什么「缓存决定一切」这句话在 Agent 工程里被反复提起——不是玄学,是实打实的单价差。
但这里有个容易被忽略的点:缓存写入本身可能比普通输入更贵。很多平台的计费模型是 cache write 单价略高于普通 input,cache read 单价远低于普通 input。所以如果你的前缀只用一次就再也不复用了,开缓存反而更亏。缓存的经济性建立在「同一前缀被反复命中」之上,命中次数越多,摊薄下来越划算。
那怎么才能稳定命中?核心就一条:把最稳定的内容放最前面,把最容易变的内容放最后面。系统提示词、工具定义、项目级说明这些几乎不变的东西前置;当前时间、用户刚改的文件、本轮的具体问题这些每次都变的东西后置。Claude Code 的做法是在用户消息里追加一个<system-reminder>标签来传递动态信息,而不是去改系统提示词——因为改系统提示词等于把整个前缀推倒重来。
还有一个高频踩坑点:会话中途不要切换模型。缓存是和具体模型绑定的,Opus 上积累的缓存切到 Haiku 就全部作废,还得重新构建一遍,可能比继续用 Opus 还贵。同理,会话中途增删工具定义也会破坏前缀。Claude Code 的 Plan Mode 就是个正面示范:进入计划模式时它不删工具,而是把 EnterPlanMode/ExitPlanMode 本身也作为工具保留,只通过一条行为约束指令告诉模型「可以探索但别改文件」,工具定义纹丝不动,缓存自然保住。
理解了这些,你就能明白为什么很多人「开了缓存却没省钱」——要么前缀不稳定,要么命中次数太少,要么中途动了模型或工具。接下来我用 TaoToken 统一 Key 的方式,把这套逻辑跑一遍,让你能直接看到命中与不命中的账单差异。
2. 用 TaoToken 统一 Key 接入,把缓存实验环境先搭起来
要验证 Prompt Caching 的命中与费用差异,你得先有一个能稳定发请求、能看用量明细的环境。直接用各家原生 Key 也能做,但如果你同时在用 Claude Code、Cursor、Cline 好几个工具,每个工具一套 Key、一套 Base URL,管理起来很碎。TaoToken 的价值就在这里:一个 Key、一个 Base URL,统一走 OpenAI 兼容协议,Claude、GPT 这些模型都能调,用量和费用在一个后台看,做缓存对比实验时不用来回切账号。
先明确几个地址,后面配置都要用:
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API Base URL:https://taotoken.net/api (这个不加 UTM,直接填到工具里)
- 模型对话体验:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- Coding Plan(长期编码/Agent 场景):https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
- 控制台:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
拿到 Key 的流程不复杂:进控制台,在 API Keys 页面创建一个新 Key,复制出来存好。这里不展开注册教程,重点放在配置上,因为缓存实验的关键是请求结构要可控。
TaoToken 走的是 OpenAI 兼容协议,所以你在 Claude Code、Cline、Cursor 里配置时,本质就是三件套:Base URL + API Key + Model ID。以 Claude Code 为例,它支持通过环境变量指定 Anthropic 兼容端点,配置片段大概是这样:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="sk-你的TaoToken密钥" export ANTHROPIC_MODEL="claude-sonnet-4-20250514"如果你用的是 Cline 或 Cursor 这类图形化工具,在设置里选 OpenAI Compatible,然后填:
{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的TaoToken密钥", "model": "claude-sonnet-4-20250514" }Codex 的话,配置写在~/.codex/auth.json和~/.codex/config.toml里,auth.json 存 Key,config.toml 指定 provider 和 model:
[model_providers.taotoken] name = "taotoken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" [profiles.default] model = "claude-sonnet-4-20250514" model_provider = "taotoken"{ "TAOTOKEN_API_KEY": "sk-你的TaoToken密钥" }配好之后,先别急着做缓存实验,用一条最简单的请求确认链路是通的。这一步很重要,因为如果 Base URL 或 Key 填错,你后面看到的「缓存没命中」其实是请求根本没成功,白折腾。
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的TaoToken密钥" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "只回复两个字:通了"}] }'返回里能看到正常的choices结构,就说明环境搭好了。接下来才是重点:构造一段长前缀,对比开缓存和不开缓存的两次调用。
3. 可复制的缓存配置片段:请求头、前缀结构与参数
这一节是整篇的核心,我把能直接复制的配置和请求结构都放出来。缓存能不能命中,八成取决于你这段前缀怎么摆。
先讲请求头。Anthropic 系的 Prompt Caching 需要在请求里显式标记哪些内容块要缓存,通常是在 content block 上加cache_control字段。走 TaoToken 的 OpenAI 兼容端点时,如果你调的是 Claude 模型,缓存标记的写法要按对应协议来。一个带缓存标记的请求体长这样:
{ "model": "claude-sonnet-4-20250514", "max_tokens": 1024, "system": [ { "type": "text", "text": "你是一个严谨的代码助手,以下是项目规范……(此处放约 6000 Token 的稳定系统提示词)", "cache_control": {"type": "ephemeral"} } ], "messages": [ {"role": "user", "content": "把 utils/date.ts 里的 formatDate 改成支持时区参数"} ] }关键点在于cache_control: {"type": "ephemeral"}这个标记,它告诉推理引擎「这段内容值得缓存」。ephemeral表示这是短期缓存,通常有几分钟到一小时的存活窗口,具体时长看平台策略。你要缓存的不只是 system,工具定义、长文档、历史对话都可以按同样方式打标记。
前缀结构的设计原则,我按优先级排一下:
第一层,系统提示词和工具定义,全局最稳定,放最前面,打缓存标记。第二层,项目级说明(比如 CLAUDE.md 的内容),在同一个项目内稳定,跟在系统提示词后面。第三层,会话上下文,同一轮会话内稳定。第四层,对话消息,每次都变,放最后,不打缓存标记。
用表格对照一下开与不开缓存的请求差异:
| 项目 | 不开缓存 | 开缓存 |
|---|---|---|
| system 字段 | 纯文本 | 带 cache_control 的 content block |
| 前缀稳定性要求 | 无 | 逐 Token 完全一致 |
| 首次请求计费 | 全量 input | cache write(单价略高) |
| 后续命中计费 | 全量 input | cache read(单价约 1/10) |
| 中途换模型 | 无影响 | 缓存全部失效 |
| 中途改工具定义 | 无影响 | 缓存全部失效 |
再给一个 Python 的完整调用示例,方便你直接跑对比实验:
import requests API_URL = "https://taotoken.net/api/v1/chat/completions" HEADERS = { "Authorization": "Bearer sk-你的TaoToken密钥", "Content-Type": "application/json" } LONG_PREFIX = "你是一个资深后端工程师。" + "以下是项目规范:" + "规范内容……" * 500 def call(use_cache: bool): system_block = { "type": "text", "text": LONG_PREFIX } if use_cache: system_block["cache_control"] = {"type": "ephemeral"} payload = { "model": "claude-sonnet-4-20250514", "max_tokens": 256, "system": [system_block], "messages": [ {"role": "user", "content": "用一句话说明这个项目的日志规范。"} ] } resp = requests.post(API_URL, headers=HEADERS, json=payload) data = resp.json() usage = data.get("usage", {}) print("缓存写入:", usage.get("cache_creation_input_tokens")) print("缓存读取:", usage.get("cache_read_input_tokens")) print("普通输入:", usage.get("prompt_tokens")) return data print("=== 第一次,开缓存 ===") call(True) print("=== 第二次,开缓存(应命中)===") call(True) print("=== 第三次,不开缓存 ===") call(False)这段代码里,usage字段会返回cache_creation_input_tokens(写入缓存的 Token 数)和cache_read_input_tokens(命中缓存的 Token 数)。你连续跑两次开缓存的调用,第二次的cache_read_input_tokens应该接近你前缀的长度,而prompt_tokens里真正按全价算的部分会大幅缩小。这就是命中与否最直接的证据。
注意一个细节:缓存标记的位置决定了缓存边界。你把cache_control打在 system 上,缓存的就是 system 之前的所有内容;如果你在 messages 里也打标记,可以形成多级缓存。但标记越多不代表越好,每一级缓存都有写入成本,前缀复用次数不够多的话,多打标记反而增加开销。
4. 验证请求与成功结果:命中率与账单到底怎么变
配置写好了,接下来就是看结果。我按上一节的代码跑了三轮,把关键数据摆出来,你能直观看到差异。
第一轮,开缓存,首次请求。这时候前缀是全新的,引擎要完整计算一遍,同时把结果写进缓存。返回的 usage 大致是:
缓存写入: 6120 缓存读取: 0 普通输入: 6120注意这里 cache write 的 Token 数和你前缀长度基本一致,说明整段前缀被标记并写入了。这一轮的费用是三者里最高的,因为写入单价通常高于普通输入。
第二轮,开缓存,前缀一字未改。这时候引擎做前缀匹配,发现整段前缀的哈希都对得上,直接复用:
缓存写入: 0 缓存读取: 6120 普通输入: 0cache_read_input_tokens等于 6120,说明整段前缀全部命中。这一轮按 cache read 单价计费,通常只有普通输入的十分之一左右。如果你这段前缀是 6000 Token,普通输入假设是某个单价,那这一轮的成本直接砍到零头。
第三轮,不开缓存,同样的前缀。因为没有缓存标记,引擎每次都当新内容处理:
缓存写入: 0 缓存读取: 0 普通输入: 6120这一轮按全量普通输入计费。把第二轮和第三轮放一起对比,就是缓存带来的真实费用差异:同样的前缀、同样的请求,命中缓存的那次成本可能只有不命中的十分之一。
那命中率怎么算?简单说就是cache_read_input_tokens / 前缀总 Token 数。上面第二轮是 6120/6120 = 100%。实际工程里很难做到 100%,因为对话尾部一直在变,但前缀部分如果设计得好,稳定在 80% 以上是完全可以的。
再补一个多轮对话的观察。假设你连续问三个问题,前缀不变,只有最后的用户消息在变:
| 轮次 | 前缀命中 | 新增输入 | 计费构成 |
|---|---|---|---|
| 第 1 轮 | 否(首次写入) | 6120 + 问题 | cache write + input |
| 第 2 轮 | 是 | 问题 | cache read + input |
| 第 3 轮 | 是 | 问题 | cache read + input |
从第 2 轮开始,那 6120 Token 的前缀就一直按 cache read 走,你只为每轮新增的问题付全价。轮次越多,摊薄效果越明显。这也是为什么 Claude Code 这类工具在长会话里能明显压住成本——它的系统提示词和工具定义动辄上万 Token,如果每轮都全价算,账单会非常难看。
有个反直觉的点要提醒:缓存写入那一轮可能比不开缓存还贵。如果你的前缀只用一次,比如一次性问答,那开缓存纯属浪费。缓存的经济性完全建立在复用上,复用次数越多越划算。所以判断要不要开缓存,先问自己:这段前缀会被重复发送多少次?
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
做缓存实验时,报错往往不是缓存本身的问题,而是链路配置。我把几个高频错误和对应排查方法列出来,你对着改就行。
401 Unauthorized。最常见的原因是 Key 没填对或者没生效。先确认你复制的是完整的 Key,没有多余空格;再确认请求头里是Authorization: Bearer sk-xxx这个格式,Bearer 后面有个空格。如果你是在 Claude Code 里配的,检查ANTHROPIC_API_KEY环境变量有没有真正 export 到当前 shell,有时候新开一个终端就丢了。还有一种情况是 Key 被禁用或额度用尽,去控制台的 API Keys 页面看一眼状态。
local proxy failed / connection refused。这个通常出现在你本地挂了某些网络工具,或者工具里配了本地代理端口但代理没起来。排查顺序:先确认 Base URL 填的是https://taotoken.net/api,没有多写路径;再检查工具设置里有没有残留的 proxy 配置,把它清掉;最后用 curl 直接打一次接口,如果 curl 通而工具不通,那就是工具侧的代理设置在捣乱。
reading choices 报错 / choices 字段为空。这类错误一般是响应结构和你预期的不一致。可能原因有几个:模型 ID 写错了,导致请求被拒但返回体不是标准结构;或者 max_tokens 设得太小,模型还没输出就被截断;又或者你调的是 Claude 模型但用了纯 OpenAI 的字段格式,某些字段不被识别。排查方法很简单,把原始响应print(resp.text)打出来看,别只看resp.json(),很多时候错误信息就在原始文本里。
OAuth 相关报错。如果你在 Claude Code 里看到 OAuth 登录失败或者 token 过期,通常是因为它默认走的是 Anthropic 官方账号登录流程,而你用的是 API Key 模式。这时候要确认你配置的是ANTHROPIC_API_KEY而不是让它走 OAuth。有些版本需要显式设置ANTHROPIC_AUTH_TOKEN或者禁用 OAuth 流程,具体看接入文档里的说明。别在 OAuth 上死磕,直接切 API Key 模式最省事。
缓存明明配了却不命中。这个不算报错,但最让人抓狂。排查清单:前缀是不是逐 Token 一致(注意空格、换行、标点);会话中途有没有换模型;有没有增删工具定义;cache_control标记有没有打对位置;缓存存活窗口有没有过期。逐条对一遍,基本能定位。
费用没降反升。回到第 4 节的结论:如果你的前缀复用次数太少,cache write 的额外成本盖过了 cache read 的节省。这种情况要么提高复用率,要么干脆别开缓存。
排查的时候有个通用技巧:先保证最小请求能通,再逐步加复杂度。先用一条最简单的消息确认链路,再加长前缀,再加缓存标记,每步都看 usage 字段。这样出问题时你能立刻知道是哪一步引入的。
6. 把缓存用对:从实验到日常编码的落地建议
跑完上面的对比,你应该对 Prompt Caching 的命中条件和费用差异有了实感。最后给几条落地建议,都是日常编码里能直接用的。
第一,先看你的前缀复用率再决定开不开。如果你用的是 Claude Code、Cline 这类工具,系统提示词和工具定义天然重复,开缓存基本稳赚。如果你只是偶尔问几个独立问题,别折腾。
第二,前缀结构一次设计好,别频繁改。系统提示词、工具定义、项目说明这些内容,改动一次就让所有缓存失效一次。把它们当成「接口」来管理,改之前想清楚值不值。
第三,动态信息走消息,不走系统提示词。当前时间、用户刚改的文件、本轮上下文,全部塞进用户消息里,别去动系统提示词。这是保住缓存前缀最关键的一条。
第四,会话中途别换模型、别动工具集。真要换,用子智能体隔离,别在主会话里切。
第五,用 usage 字段做监控。把cache_read_input_tokens和cache_creation_input_tokens打到日志里,定期看命中率。命中率掉了,多半是前缀结构被破坏了,早发现早修。
如果你还没搭好环境,可以从模型对话页面先发几条请求感受一下返回结构,再去 API Keys 页面建 Key,接入文档里有各工具的详细配置。长期做编码和 Agent 的话,Coding Plan 会更适合,用量和成本都更可控。缓存这东西,理解原理只是第一步,真正省钱靠的是把前缀结构设计对,然后让它稳定地被复用。