Source: https://aiprimetech.io/zh/blog/prompt-caching-explained/ 2026 年 17 日,六月 · 6 分钟 · API # 详解提示词缓存:何时能省钱,何时不能 A 作者 [AI Prime Tech](https://aiprimetech.io/zh/) · AI 辅助撰写 一个周一的凌晨 9:12,我们的一个客服智能体的成本开始比预期高出 4x。 模型没有产生幻觉,流量图没有出错,也没有人意外地从 Haiku 切换到 Opus。问题更简单:每个请求都包含同一份长度为 42,000 个 token 的政策手册,但我们的提示词缓存命中率几乎为零,因为我们在手册前面加上了每次更新的时间戳和用户专属元数据。 内容相同。模型相同。应用相同。却没有节省任何缓存成本。 提示词缓存听起来像是会自动生效的功能,直到你尝试用它降低账单。实际上,它只对一种非常特定的提示词结构有利:在多个请求中重复使用的一段较长且稳定的前缀。只要理解这句话,通常就能预测缓存是否会省钱,还是悄无声息地毫无作用。 ## 提示词缓存究竟缓存什么 提示词缓存不是语义记忆。提供商并不是在说:“这看起来和你之前发送的内容相似。”它更接近于: “对于这个模型和缓存策略,这个请求的开头与之前的 token 序列足够匹配,因此我们可以复用该前缀的内部计算。” 这一区别很重要。 当你反复发送相同的长前缀时,提示词缓存通常会有所帮助: - 系统指令 - 工具定义 - 长篇文档集 - 产品目录 - 代码库上下文 - 法律或政策手册 - 少样本示例 - 在多个请求中重复使用的静态检索结果 如果每个请求的开头都不同,缓存通常帮助不大: - 顶部的动态时间戳 - 共享内容之前的用户专属数据 - 随机排列的示例顺序 - 可缓存部分中每个请求各不相同的 UUID - 顺序或格式发生变化的检索片段 - 放在稳定指令之前的聊天历史 工程上的核心做法是:将稳定且计算成本高的上下文放在前面,将易变的上下文放在后面。 ## Anthropic 与 OpenAI:目标相似,控制方式不同 Anthropic 和 OpenAI 都支持提示词缓存,但开发者的使用体验有所不同。 Anthropic 提供的提示词缓存控制更为显式。你可以为内容块标记缓存控制设置,然后查看用量字段,例如 cache_creation_input_tokens 和 cache_read_input_tokens。这样更容易判断你是在创建缓存条目,还是从中读取内容。 在支持的模型上,OpenAI 的提示词缓存更加自动化。当提示词前缀被重复使用时,缓存的输入 token 可能会按不同价格计费,并体现在用量元数据中。通常,你不需要以同样的方式设置显式缓存断点;而是要组织好请求,确保可复用前缀在字节和 token 层面保持稳定。 实际差异在于控制能力与便利性之间的取舍。 | 维度 | Anthropic 式显式缓存 | OpenAI 式自动缓存 | 开发者控制能力 | 较高;可以标记可缓存的内容块 | 较低;由提供商检测可复用前缀 | 可调试性 | 通常可通过缓存创建和读取计数更清楚地了解情况 | 取决于模型/API 提供的用量元数据 | 提示词结构的重要性 | 极高 | 极高 | 最适用场景 | 大型静态内容块、工具、文档、示例 | 多次调用中重复使用的长前缀 | 常见失效原因 | 缓存断点放在动态内容之后 | 缓存尚未匹配到内容,前缀就已发生变化 | 理解方式 | “我在写入和读取一个近似有名称的前缀” | “提供商识别出了这个重复出现的前缀” 实现细节因模型和提供商而异,而且仍在持续演进。但架构层面的规则是稳定的:缓存依赖重复的前缀,而不是相似的提示词。 ## 定价模型:创建与读取 理解提示词缓存的一种实用方法,是将输入 token 分成三类: - 普通输入 token:按常规输入价格计费。 - 缓存创建 token:用于创建缓存条目的 token,通常按加价或专门的写入费率计费。 - 缓存读取 token:从已有缓存条目中读取的 token,通常按折扣费率计费。 Anthropic 通过以下用量字段直接体现这种区别: ``` { "usage": { "input_tokens": 812, "cache_creation_input_tokens": 42000, "cache_read_input_tokens": 0, "output_tokens": 684 } } ``` 在后续请求中,如果前缀命中缓存,你希望看到的结果应更接近: ``` { "usage": { "input_tokens": 936, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 42000, "output_tokens": 711 } } ``` 首次调用创建缓存。后续调用从缓存中读取。 经济效益取决于提供商当前的定价、缓存 TTL 和模型。由于价格会变化,我更倾向于在代码中用变量建模,而不是将具体数字硬编码到设计文档中。 下面是我在判断某条调用路径是否值得实现缓存时使用的一个小型 Python 函数: ``` def cache_savings( cached_tokens: int, normal_input_per_mtok: float, cache_write_per_mtok: float, cache_read_per_mtok: float, requests: int, ) -> dict: mtok = cached_tokens / 1_000_000 no_cache = requests * mtok * normal_input_per_mtok with_cache = ( mtok * cache_write_per_mtok + (requests - 1) * mtok * cache_read_per_mtok ) return { "no_cache": round(no_cache, 6), "with_cache": round(with_cache, 6), "savings": round(no_cache - with_cache, 6), "savings_pct": round((1 - with_cache / no_cache) * 100, 2), } print(cache_savings( cached_tokens=42_000, normal_input_per_mtok=3.00, cache_write_per_mtok=3.75, cache_read_per_mtok=0.30, requests=100, )) ``` 这里的价格仅用于示例。关键在于计算方式。 对于长度为 42,000 个 token 的可复用前缀和 100 次请求,缓存通常效果很好。如果前缀相同但只有一次请求,而写入价格又高于普通输入价格,那么缓存反而可能更贵。 这就是第一个重要的易踩坑之处:提示词缓存依靠成本摊销。如果不重复使用缓存的前缀,就无法获得收益。 ## 盈亏平衡公式 你可以用以下公式估算盈亏平衡点: ``` no_cache_cost = N * T * normal_price cache_cost = T * write_price + (N - 1) * T * read_price ``` 其中: - N 是使用同一前缀的请求次数 - T 是可缓存的 token 数量 - normal_price 是普通输入 token 的价格 - write_price 是缓存创建价格 - read_price 是缓存读取价格 满足以下条件时,缓存可以省钱: ``` T * write_price + (N - 1) * T * read_price < N * T * normal_price ``` 该 T 在只考虑成本的盈亏平衡计算中会被约去,这意味着复用次数和相对价格决定了是否值得使用缓存。但 token 数量在实际运行中仍然很重要:在 500 个 token 上节省 80% 的成本微不足道;在 80,000 个 token 上节省同样的比例,就会成为账单上值得关注的一项。 一个实用的粗略估算版本: ``` break_even_requests = (write_price - read_price) / (normal_price - read_price) ``` 如果缓存写入仅比普通输入稍贵,而读取便宜得多,那么达到盈亏平衡所需的复用次数可能很少。如果缓存写入很贵,或者前缀很少重复,缓存的收益就很不稳定。 ## 如何组织提示词以保持前缀稳定 最有效的提示词缓存优化不是某个 API 标志,而是提示词的布局。 不佳的布局: ``` Request ID: req_8f19a Current time: 2026-02-14T09:12:33Z User: customer_4812 Locale: en-US SYSTEM INSTRUCTIONS: ... POLICY MANUAL: [42,000 tokens of stable text] USER QUESTION: Can I get a refund after 45 days? ``` 这会使前缀缓存失效,因为提示词从开头就出现了差异。 更好的布局: ``` SYSTEM INSTRUCTIONS: ... POLICY MANUAL: [42,000 tokens of stable text] DYNAMIC REQUEST METADATA: Request ID: req_8f19a Current time: 2026-02-14T09:12:33Z User: customer_4812 Locale: en-US USER QUESTION: Can I get a refund after 45 days? ``` 稳定的内容块放在前面。动态的内容块放在后面。 对于 Anthropic 式显式缓存,其概念结构可能如下: ``` { "model": "claude-sonnet-4-6", "max_tokens": 800, "system": [ { "type": "text", "text": "You are a support policy assistant..." }, { "type": "text", "text": "Refund policy manual v2026-02-01:\n\n...", "cache_control": { "type": "ephemeral" } } ], "messages": [ { "role": "user", "content": "Can I get a refund after 45 days?" } ] } ``` 具体请求结构因 API 版本和 SDK 而异,因此请将它视为结构示意,而不是可以直接复制粘贴的代码。关键是将较大的稳定内容块明确标记为可缓存,并使其在各次调用之间保持不变。 对于 OpenAI 式自动缓存,即使你不手动标记内容块,提示词结构也同样重要: ``` { "model": "gpt-5.5", "messages": [ { "role": "system", "content": "You are a support policy assistant...\n\nRefund policy manual v2026-02-01:\n\n..." }, { "role": "user", "content": "Can I get a refund after 45 days?" } ] } ``` 如果系统消息在每个请求中都会变化,就不应期待持续稳定地从缓存读取。 ## 一种实用的架构模式 对于生产系统,我喜欢在应用代码中明确定义缓存的提示词前缀。 一种简单的模式: ``` prompt_prefix/ support_policy_v17.txt tool_schema_v4.json examples_v9.md runtime_request/ user_question user_profile retrieved_recent_orders ``` 然后像这样组装请求: ``` from pathlib import Path POLICY_PREFIX = Path("prompt_prefix/support_policy_v17.txt").read_text() TOOL_SCHEMA = Path("prompt_prefix/tool_schema_v4.json").read_text() EXAMPLES = Path("prompt_prefix/examples_v9.md").read_text() def build_prompt(user_question: str, user_profile: dict) -> str: stable_prefix = f""" You are a support policy assistant. TOOLS: {TOOL_SCHEMA} POLICY: {POLICY_PREFIX} EXAMPLES: {EXAMPLES} """.strip() dynamic_suffix = f""" USER PROFILE: {user_profile} USER QUESTION: {user_question} """.strip() return stable_prefix + "\n\n--- DYNAMIC REQUEST ---\n\n" + dynamic_suffix ``` 实际上,我还会为前缀设置版本。如果政策手册发生变化,我希望这是一次有意触发的缓存失效事件: ``` sha256sum prompt_prefix/support_policy_v17.txt ``` 将这个哈希值与模型名称、缓存创建 token 数、缓存读取 token 数和请求路由一起记录到日志中。如果缺少这种可观测性,缓存问题会很难排查,因为响应仍然正常,只是成本更高。 一条有用的日志记录如下: ``` { "route": "support.answer", "model": "claude-sonnet-4-6", "prefix_hash": "9d3a4a...", "input_tokens": 932, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 42000, "output_tokens": 688, "latency_ms": 1840 } ``` 不要仅凭账单进行优化。应按调用路径记录缓存行为。 ## 延迟:通常会改善,但并非魔法 提示词缓存可以降低延迟,因为模型提供商能够复用缓存前缀的计算结果。但除非已在自己的工作负载上进行测量,否则我不会在制定 SLA 时假定延迟会降低一个固定值。 生产环境中的实际情况更复杂: - 短提示词可能不会带来明显的延迟变化。 - 较长的缓存前缀通常能缩短首个 token 的响应时间。 - 输出长度仍然是许多请求耗时的主要决定因素。 - 工具调用、网络延迟和排队可能掩盖缓存带来的收益。 - 如果写入比读取更慢,缓存未命中会导致尾部延迟骤增。 贴近实际情况的基准测试设置很简单: ``` # warm cache python run_eval.py --case support_refund --repeat 1 # measure cache reads python run_eval.py --case support_refund --repeat 50 --jsonl out.jsonl # inspect p50/p95 by cache_read_input_tokens > 0 python analyze_latency.py out.jsonl ``` 分别测量缓存命中和缓存未命中的请求。将它们混在一起,只会得到一个无法解释任何一种情况的数字。 ## 我在实际系统中见过的常见陷阱 ### 1. 动态内容放在稳定内容之前 这是最常见的问题。时间戳、请求 ID、功能开关配置快照、用户区域设置或实验分组信息被放在系统提示词的最前面。 把这些内容移到后面。 ### 2. “等价”的 JSON 并不等同 这两个片段在语义上完全相同,但在缓存层面未必相同: ``` {"name":"refund_lookup","timeout":3,"strict":true} ``` ``` { "strict": true, "timeout": 3, "name": "refund_lookup" } ``` 如果缓存前缀包含工具 schema 或配置,请使用确定性序列化: ``` import json def stable_json(data: dict) -> str: return json.dumps(data, sort_keys=True, separators=(",", ":")) ``` 这样可以避免因键顺序或空白字符变化而意外导致缓存未命中。 ### 3. 检索可能破坏可缓存性 RAG 系统经常这样构建提示词: ``` Retrieved documents: [doc A] [doc B] [doc C] Instructions: ... ``` 如果每次查询的检索结果不同,前缀也会随查询而变化。如果检索到的上下文确实是动态的,这样没有问题,但不要把稳定的指令放在它后面。 更好的做法: ``` Instructions: ... Long-lived product documentation: [stable docs] Query-specific retrieved documents: [doc A] [doc B] [doc C] ``` 对于请求量较大的系统,可以考虑将上下文拆分为: - 全局稳定的指令 - 租户内稳定的文档 - 针对用户或查询的检索内容 每一层的缓存行为都不同。 ### 4. 缓存 TTL 很重要 有些缓存条目会过期。如果流量呈突发性,一个前缀可能在十分钟内被大量复用,之后便无人使用。延长缓存存活时间可能有所帮助,但不同提供商和模型的定价或可用性可能不同。 这时,请求分布就很重要。“我们每天收到 10,000 个请求”不如“上午 9 点到 10 点之间,针对这个完全相同的前缀,我们每分钟收到 200 个请求”有用。 ### 5. 模型变化会让原有假设失效 不能假定为某个模型缓存的前缀可以供另一个模型复用。如果你在 Claude Opus 4.8、Sonnet 4.6、GPT-5.5、Gemini 3,或 Llama、Qwen、DeepSeek、MiniMax、Kimi 等开放模型之间进行路由,请按模型跟踪缓存行为。 多模型路由很有用,但会让成本摊销更复杂。如果流量分到五个模型,每个模型收到的重复前缀可能会更少。 ### 6. 提示词个性化可能适得其反 团队经常对系统提示词进行个性化处理: ``` You are helping {{customer_name}}, who is on plan {{plan_name}}. ``` 这看起来无伤大雅,但如果它出现在缓存内容之前,每个客户就会有不同的前缀。除非个性化内容确实会改变模型对稳定内容块的理解,否则应将其放在后面。 ## 提示词缓存何时能省钱 以下情况通常非常适合使用缓存: - 可缓存的前缀较大,通常包含数千甚至数万个 token。 - 完全相同的前缀被多次复用。 - 前缀在不同请求之间保持稳定。 - 流量在缓存存活时间内到达。 - 你能观测缓存创建和缓存读取的 token 数。 - 路由中的提示词组成可预测。 具体示例: - 编程助手反复包含相同的代码仓库结构图和风格指南。 - 客服机器人在回答每个退款问题时都包含相同的政策手册。 - 智能体在每次请求中都使用大型静态工具 schema。 - 内部研究助手为团队提供相同的语料库概览。 对于这些工作负载,提示词缓存可能是最有效的成本优化手段之一,因为它不需要降低上下文质量。你仍然保留长提示词,只是不必每次都按完整的输入价格付费。 ## 提示词缓存何时没有帮助 以下情况下,缓存收益有限,甚至可能产生负面效果: - 提示词很短。 - 前缀很少被复用。 - 大部分上下文都是针对特定查询的检索内容。 - 动态数据出现在稳定数据之前。 - 你在实验期间不断修改指令。 - 流量稀疏地分散在许多模型、租户或提示词变体之间。 - 缓存写入成本较高,而读取次数太少,无法摊销写入成本。 评估时的一个常见陷阱是:在 notebook 中,提示词缓存看起来效果不佳,因为每个案例只运行一次。到了生产环境,数千名用户共享同一个稳定前缀,效果就非常好。反过来的情况也会发生:演示程序循环运行同一个提示词 100 次,显示出显著的成本节省,但生产环境中的租户定制却阻碍了复用。 使用真实的请求分布进行测试。 ## 简单的缓存就绪检查清单 在启用或依赖提示词缓存之前,我会检查: - 是否至少有一个长度超过几千个 token 的稳定前缀? - 它是否位于所有动态内容之前? - 序列化是否具有确定性? - 是否对前缀进行了版本管理并计算了哈希? - 我能否记录缓存创建和缓存读取的 token 数? - 我是否知道缓存存活时间内的预期复用次数? - 我是否分别测量过缓存命中和未命中的延迟? 如果有三个或更多问题的答案是“否”,缓存可能仍然有效,但你是在凭猜测做判断。 ## 实践要点 提示词缓存不是神奇的压缩技术。它复用的是稳定的 token 前缀,其经济收益来自一次性支付创建成本,再多次支付较低的读取成本。 工程实践方法很直接: - 将大段稳定内容放在最前面:指令、工具、文档、示例。 - 将时间戳、用户元数据、检索结果和请求 ID 移到可缓存前缀之后。 - 跟踪 cache_creation 与 cache_read 按路由和模型统计的 token 数。 - 对 JSON 工具 schema 和配置使用确定性序列化。 - 根据实际提供商的价格和流量分布,建立盈亏平衡模型。 - 谨慎处理多模型路由、租户定制和短提示词。 - 分别测量缓存命中和未命中的延迟,不要仅凭平均值判断。 我见过最好的提示词缓存优化一点也不复杂:我们把 42,000 个稳定 token 移到了 12 个动态 token 前面。模型行为没有变化,账单却变了。 [推理](/zh/blog/tag/inference/)[提示词](/zh/blog/tag/prompts/)[检索增强生成](/zh/blog/tag/rag/)[API](/zh/blog/tag/api/) A [AI Prime Tech](https://aiprimetech.io/zh/) 本博客由 AI Prime Tech 发布。文章借助 AI 辅助起草;在据此采取行动前,请对照官方文档核实模型 ID、价格和限额等特定版本的细节。 以更低价格接入 Claude API 一个 API 密钥即可使用 Claude Opus 5.5、Sonnet 5、Haiku 4.5 和 Fable 5.1,以及 GPT-6 个模型。按量付费,无需订阅。 [获取 API 密钥 →](/register) AI Prime Tech 是独立的第三方 API 网关。Claude™ 和 Anthropic® 是 Anthropic, PBC 的商标。此处不暗示任何关联或背书。