在生产环境中运行 LLM 应用,Token 费用往往是最大的成本黑洞。一个日均 10 万次 API 调用的 RAG 系统,仅 System Prompt 的重复计算每月就可能浪费 $2,000 以上。Prompt Caching(提示缓存) 是 2025-2026 年三大 LLM 厂商(Anthropic、OpenAI、Google)相继推出的核心优化特性,能在不改变任何业务逻辑的前提下,将 API 调用成本降低 50%-90%,同时将首 Token 延迟(TTFT)压缩 60%-85%。如果你正在为 LLM 账单发愁,这篇文章会告诉你如何用最少的改动获得最大的收益。
💰 一、Prompt Caching 的工作原理与厂商对比
1.1 为什么 LLM 调用这么贵?
理解 Prompt Caching 的价值,首先要理解 LLM 推理的成本结构。当你向 GPT-4o 发送一个包含 2000 Token 的 System Prompt 加上 200 Token 的用户问题时,模型需要对全部 2200 个 Token 执行前向传播(Forward Pass)。KV Cache(Key-Value Cache)虽然避免了重复计算注意力权重,但首次计算仍需完整执行。
关键问题在于:大多数 LLM 应用中,80% 以上的输入 Token 是重复的。你的 System Prompt、Few-shot Examples、RAG 检索到的文档片段——这些内容在每次请求中几乎一模一样,却每次都重新计算。
📌 **记住:**Prompt Caching 的本质是「前缀匹配缓存」——如果多个请求共享相同的前缀部分,缓存命中后只需计算差异部分。这与 HTTP 缓存的 ETag 机制异曲同工,但工作在 Token 级别。
1.2 三大厂商方案全面对比
截至 2026 年 6 月,三大厂商的 Prompt Caching 方案各有特色:
| 维度 | Anthropic Prompt Caching | OpenAI Automatic Caching | Google Context Caching |
|---|---|---|---|
| 启用方式 | 显式标记 cache_control |
自动生效,无需配置 | 显式创建 CachedContent |
| 缓存粒度 | 消息级别,精确控制 | 前缀自动匹配 | 完整上下文对象 |
| 价格折扣 | 缓存写入 +25%,缓存读取 -90% | 缓存命中 -50%(自动) | 缓存存储费 + 读取 -75% |
| 最小缓存长度 | 1024 Tokens(Haiku)、2048(Sonnet) | ~1024 Tokens(自动判断) | 32768 Tokens |
| 缓存有效期 | 5 分钟(最后访问后) | 5-10 分钟(自动) | 自定义(TTL) |
| 支持模型 | Claude 3.5/4 系列全部 | GPT-4o、GPT-4.1、o1/o3 | Gemini 1.5/2.0/2.5 系列 |
| 适用场景 | 精确控制缓存内容 | 零配置快速优化 | 长上下文、多轮对话 |
⚡ **关键结论:**OpenAI 的方案最省心(零配置),Anthropic 的方案最灵活(精确控制),Google 的方案最适合长上下文场景(最低 32K Token 门槛)。在生产环境中,建议根据你的主要供应商选择对应方案,而非追求跨平台统一。
1.3 成本节省实测数据
以一个典型的 RAG 应用为例——System Prompt 800 Token、Few-shot Examples 1200 Token、RAG 文档 3000 Token、用户问题 200 Token:
# 成本计算:假设日均 10 万次调用
# 输入 Token 构成:System(800) + Few-shot(1200) + RAG(3000) + User(200) = 5200 Token
# 其中可缓存部分:5000 Token(96%)
# ❌ 无缓存(以 Claude Sonnet 4 为例,$3/MTok 输入)
# 月输入成本 = 100,000 × 30 × 5200 / 1,000,000 × $3 = $46,800
# ✅ Anthropic Prompt Caching(缓存命中率 95%)
# 缓存写入费 = 100,000 × 30 × 5000 × 0.05 / 1M × $3.75 = $2,813
# 缓存读取费 = 100,000 × 30 × 5000 × 0.95 / 1M × $0.30 = $4,275
# 非缓存费 = 100,000 × 30 × 5200 × 0.05 / 1M × $3 = $2,340
# 月总成本 = $9,428(节省 80%)
# ✅ OpenAI Automatic Caching(以 GPT-4o 为例,缓存命中率 90%)
# 月输入成本 = 100,000 × 30 × (5000×0.9×$1.25 + 5200×0.1×$2.5) / 1M
# = 100,000 × 30 × (5,625 + 1,300) / 1M = $20,775(节省 55%)
⚠️ **警告:**以上数据基于理想缓存命中率。实际命中率取决于你的请求模式——如果每次请求的 System Prompt 完全相同,命中率可达 95%+;如果包含动态内容(如时间戳、用户 ID),命中率会显著下降。
🔧 二、三大厂商 Prompt Caching 实战代码
2.1 Anthropic Prompt Caching:精确控制缓存边界
Anthropic 的方案需要显式标记哪些内容需要缓存。核心是通过 cache_control 参数在 Message 的特定位置设置缓存断点:
// Anthropic Prompt Caching 实战 — Node.js SDK
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
async function cachedRAGQuery(userQuestion, ragDocuments) {
const response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: [
{
type: 'text',
text: `你是一个专业的技术文档助手。请基于以下文档回答用户问题。
如果文档中没有相关信息,请明确说明。
回答要求:准确、简洁、引用原文。`,
// ✅ 标记 System Prompt 为缓存目标
cache_control: { type: 'ephemeral' }
}
],
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: `## 参考文档\n\n${ragDocuments.join('\n\n---\n\n')}`,
// ✅ 标记 RAG 文档为缓存目标(通常变化频率低)
cache_control: { type: 'ephemeral' }
},
{
type: 'text',
text: `\n\n## 用户问题\n\n${userQuestion}`
// ❌ 用户问题不标记缓存(每次不同)
}
]
}
]
});
// 检查缓存命中情况
const usage = response.usage;
console.log(`缓存创建 Token: ${usage.cache_creation_input_tokens || 0}`);
console.log(`缓存读取 Token: ${usage.cache_read_input_tokens || 0}`);
console.log(`原始输入 Token: ${usage.input_tokens}`);
return response.content[0].text;
}
💡 **提示:**Anthropic 的
cache_control标记遵循「最后一个匹配前缀」原则——缓存断点之后的内容不参与缓存匹配。因此,把最稳定的内容(System Prompt)放在最前面,最不稳定的内容(用户问题)放在最后面,可以最大化命中率。
2.2 OpenAI Automatic Caching:零配置自动优化
OpenAI 的方案最为简单——只要你的请求前缀与最近 5-10 分钟内的某个请求完全相同,缓存就会自动命中。你不需要做任何代码改动,但可以通过日志监控缓存命中情况:
# OpenAI Automatic Caching 监控 — Python SDK
from openai import OpenAI
import time
client = OpenAI()
def cached_completion(system_prompt: str, context: str, question: str) -> dict:
"""发送带缓存优化的 Completion 请求"""
start_time = time.time()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
# ✅ 将稳定的 System Prompt 放在最前面
{"role": "system", "content": system_prompt},
# ✅ RAG 上下文放在中间(如果内容稳定)
{"role": "user", "content": f"参考信息:\n{context}"},
# ✅ 用户问题放在最后
{"role": "user", "content": question}
],
temperature=0.7,
max_tokens=1000
)
elapsed = time.time() - start_time
usage = response.usage
# OpenAI 在 cached_tokens 字段中返回缓存命中情况
cached_tokens = getattr(usage, 'cached_tokens', 0) or 0
total_input = usage.prompt_tokens
cache_hit_rate = cached_tokens / total_input if total_input > 0 else 0
print(f"⏱️ 延迟: {elapsed:.2f}s")
print(f"📊 输入 Token: {total_input} (缓存命中: {cached_tokens})")
print(f"📈 缓存命中率: {cache_hit_rate:.1%}")
print(f"💰 实际费用: ${calculate_cost(total_input, cached_tokens):.4f}")
return {
"answer": response.choices[0].message.content,
"cached_tokens": cached_tokens,
"total_tokens": total_input,
"latency": elapsed
}
def calculate_cost(total_tokens: int, cached_tokens: int) -> float:
"""计算实际费用(GPT-4o 2026 定价)"""
# GPT-4o: 输入 $2.50/MTok,缓存命中 $1.25/MTok
uncached = total_tokens - cached_tokens
return (uncached * 2.50 + cached_tokens * 1.25) / 1_000_000
📌 记住:OpenAI 的自动缓存有一个隐含要求——请求前缀必须完全相同(逐 Token 匹配)。这意味着如果你在 System Prompt 中嵌入了动态内容(如当前日期、请求 ID),会导致缓存完全失效。解决方案是把动态内容移到 System Prompt 之后的用户消息中。
2.3 Google Context Caching:长上下文的最优解
Google 的方案最独特——它允许你预先创建一个「缓存上下文对象」,然后在多次请求中复用。这对长上下文场景(如视频理解、长文档分析)尤其有效:
// Google Context Caching 实战 — Gemini TypeScript SDK
import { GoogleGenerativeAI } from '@google/generative-ai';
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
interface CachedContext {
name: string;
model: string;
createTime: string;
expireTime: string;
usageMetadata: { totalTokenCount: number };
}
async function createContextCache(
documentContent: string,
ttlSeconds: number = 3600
): Promise<CachedContext> {
// ✅ 预创建缓存上下文(只需计算一次)
const cacheManager = genAI.createCacheManager();
const cache = await cacheManager.create({
model: 'gemini-2.5-pro',
contents: [
{
role: 'user',
parts: [{ text: documentContent }]
}
],
ttlSeconds: ttlSeconds, // 缓存有效期(最长 48 小时)
systemInstruction: '你是一个文档分析专家,请仔细阅读文档并准确回答问题。'
});
console.log(`✅ 缓存已创建: ${cache.name}`);
console.log(`📊 缓存 Token 数: ${cache.usageMetadata.totalTokenCount}`);
console.log(`⏰ 过期时间: ${cache.expireTime}`);
return cache as CachedContext;
}
async function queryWithCache(cacheName: string, question: string) {
// ✅ 使用缓存上下文发起请求(无需重复发送完整文档)
const model = genAI.getGenerativeModelFromCachedContent(
cacheName as any
);
const result = await model.generateContent(question);
return result.response.text();
}
// 使用示例:对同一文档多次提问
async function analyzeDocument() {
const longDocument = '...'; // 100,000+ Token 的长文档
// 第一步:创建缓存(一次性成本)
const cache = await createContextCache(longDocument, 7200);
// 第二步:多次提问(复用缓存,大幅降低成本)
const questions = [
'这份文档的核心观点是什么?',
'文档中提到了哪些技术方案?',
'总结文档的结论和建议。',
];
for (const q of questions) {
const answer = await queryWithCache(cache.name, q);
console.log(`Q: ${q}\nA: ${answer}\n`);
}
}
⚡ **关键结论:**Google Context Caching 的最低缓存长度为 32,768 Tokens,这使它不适合短提示场景。但如果你的应用需要对同一份长文档(如技术手册、法律合同、代码仓库)多次提问,它是三者中最经济的选择——缓存存储费仅 $1.00/MTok/小时,读取费比正常输入便宜 75%。
🎯 三、最大化缓存命中率的工程实践
3.1 请求前缀标准化
缓存命中率的核心决定因素是请求前缀的一致性。以下是标准化请求前缀的实战模式:
// 请求前缀标准化模板 — TypeScript
interface CacheableRequest {
systemPrompt: string; // 最稳定:全局统一
toolDefinitions: string; // 较稳定:工具列表
ragContext: string; // 中等稳定:RAG 文档
userMessage: string; // 最不稳定:用户输入
}
function buildCacheableRequest(params: CacheableRequest): Message[] {
// ✅ 正确写法:按稳定性从高到低排列消息内容
return [
{
role: 'system',
content: params.systemPrompt // 稳定性:★★★★★
},
{
role: 'user',
content: [
{
type: 'text',
text: `## 工具定义\n${params.toolDefinitions}` // 稳定性:★★★★
},
{
type: 'text',
text: `## 参考文档\n${params.ragContext}` // 稳定性:★★★
},
{
type: 'text',
text: `## 用户问题\n${params.userMessage}` // 稳定性:★
}
]
}
];
}
// ❌ 错误写法:动态内容破坏缓存前缀
function buildBadRequest(userMessage: string): Message[] {
return [
{
role: 'system',
// ❌ 包含时间戳,每次请求都不同,导致缓存完全失效
content: `当前时间: ${new Date().toISOString()}\n你是助手。`
},
{ role: 'user', content: userMessage }
];
}
⚠️ **警告:**一个最常见的错误是在 System Prompt 中嵌入当前时间或请求 ID。这会导致每个请求的前缀都不同,缓存命中率降至 0%。把动态信息放到消息的最后部分,让缓存能覆盖前面的静态内容。
3.2 分层缓存架构
在复杂应用中,可以构建多层缓存来进一步优化成本:
# 分层缓存架构 — Python 实现
import hashlib
import json
from functools import lru_cache
from typing import Optional
class LayeredLLMCache:
"""三层缓存架构:本地语义缓存 → LLM Prompt 缓存 → 完整 API 调用"""
def __init__(self, llm_client, embedding_model: str = "text-embedding-3-small"):
self.client = llm_client
self.embedding_model = embedding_model
self.semantic_cache: dict[str, dict] = {} # 本地语义缓存
self.similarity_threshold = 0.95 # 语义相似度阈值
def _compute_embedding(self, text: str) -> list[float]:
"""计算文本的 Embedding 向量"""
response = self.client.embeddings.create(
model=self.embedding_model,
input=text
)
return response.data[0].embedding
def _cosine_similarity(self, a: list[float], b: list[float]) -> float:
"""计算余弦相似度"""
dot = sum(x * y for x, y in zip(a, b))
norm_a = sum(x ** 2 for x in a) ** 0.5
norm_b = sum(x ** 2 for x in b) ** 0.5
return dot / (norm_a * norm_b) if norm_a and norm_b else 0.0
def _check_semantic_cache(self, query: str) -> Optional[str]:
"""第一层:语义缓存检查"""
query_embedding = self._compute_embedding(query)
best_match = None
best_score = 0.0
for cached_query, cached_data in self.semantic_cache.items():
score = self._cosine_similarity(query_embedding, cached_data['embedding'])
if score > best_score:
best_score = score
best_match = cached_data
if best_score >= self.similarity_threshold and best_match:
print(f"✅ 语义缓存命中 (相似度: {best_score:.3f})")
return best_match['answer']
return None
async def query(
self,
system_prompt: str,
context: str,
question: str,
use_cache: bool = True
) -> dict:
"""带三层缓存的查询入口"""
# 第一层:语义缓存(本地,零 API 成本)
if use_cache:
cached_answer = self._check_semantic_cache(question)
if cached_answer:
return {"answer": cached_answer, "cache_layer": "semantic"}
# 第二层:LLM Prompt Caching(依赖供应商缓存机制)
# System Prompt + Context 作为稳定的前缀,自动触发 Prompt 缓存
response = await self.client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"参考信息:\n{context}\n\n问题:{question}"}
]
)
answer = response.choices[0].message.content
# 写入语义缓存
if use_cache:
query_hash = hashlib.md5(question.encode()).hexdigest()
self.semantic_cache[query_hash] = {
'question': question,
'answer': answer,
'embedding': self._compute_embedding(question)
}
return {
"answer": answer,
"cache_layer": "prompt" if getattr(response.usage, 'cached_tokens', 0) > 0 else "none",
"cached_tokens": getattr(response.usage, 'cached_tokens', 0)
}
💡 **提示:**语义缓存的相似度阈值建议设为 0.95 以上——宁可漏掉一些缓存命中,也不要返回不相关的缓存结果。对于精确查询(如代码生成、数学计算),建议完全禁用语义缓存,只依赖 Prompt Caching。
3.3 缓存监控与告警
在生产环境中,你需要持续监控缓存命中率和成本节省情况:
// 缓存监控中间件 — TypeScript
interface CacheMetrics {
totalRequests: number;
cacheHits: number;
cacheMisses: number;
totalCachedTokens: number;
totalInputTokens: number;
estimatedSavings: number;
}
class PromptCacheMonitor {
private metrics: CacheMetrics = {
totalRequests: 0,
cacheHits: 0,
cacheMisses: 0,
totalCachedTokens: 0,
totalInputTokens: 0,
estimatedSavings: 0
};
recordRequest(cachedTokens: number, totalInputTokens: number, pricePerToken: number) {
this.metrics.totalRequests++;
this.metrics.totalCachedTokens += cachedTokens;
this.metrics.totalInputTokens += totalInputTokens;
if (cachedTokens > 0) {
this.metrics.cacheHits++;
// 缓存读取节省 = 缓存 Token 数 × (原价 - 缓存价)
// Anthropic: 缓存读取价是原价的 10%
const savings = cachedTokens * pricePerToken * 0.9;
this.metrics.estimatedSavings += savings;
} else {
this.metrics.cacheMisses++;
}
}
getReport(): string {
const hitRate = this.metrics.totalRequests > 0
? (this.metrics.cacheHits / this.metrics.totalRequests * 100).toFixed(1)
: '0.0';
const tokenCacheRate = this.metrics.totalInputTokens > 0
? (this.metrics.totalCachedTokens / this.metrics.totalInputTokens * 100).toFixed(1)
: '0.0';
return [
`📊 Prompt Cache 监控报告`,
`━━━━━━━━━━━━━━━━━━━━━━`,
`总请求数: ${this.metrics.totalRequests.toLocaleString()}`,
`缓存命中: ${this.metrics.cacheHits.toLocaleString()} (${hitRate}%)`,
`Token 缓存率: ${tokenCacheRate}%`,
`累计节省: $${this.metrics.estimatedSavings.toFixed(2)}`,
`━━━━━━━━━━━━━━━━━━━━━━`,
hitRate < 30 ? '⚠️ 缓存命中率过低,请检查请求前缀一致性' : '✅ 缓存运行正常'
].join('\n');
}
}
⚠️ **警告:**如果缓存命中率持续低于 20%,大概率是请求前缀不一致导致的。最常见的三个原因:(1)System Prompt 包含动态内容;(2)RAG 文档排序不稳定;(3)工具定义的 JSON 字段顺序不一致。逐一排查即可解决。
📊 四、实战避坑与最佳实践
4.1 常见坑点总结
| 坑点 | 表现 | 解决方案 |
|---|---|---|
| System Prompt 含动态内容 | 缓存命中率 0% | 将时间戳、用户 ID 移到最后 |
| RAG 文档排序不稳定 | 缓存命中率波动大 | 对文档按 ID 排序后再拼接 |
| 工具定义 JSON 序列化不一致 | 偶尔命中偶尔不命中 | 使用 JSON.stringify(data, Object.keys(data).sort()) |
| 缓存长度不满足最低要求 | 缓存不生效 | 合并短消息,或在消息前填充固定前缀 |
| 跨区域部署 | 缓存不共享 | 确保同一用户的请求路由到同一区域 |
| 多语言混合 | 缓存碎片化 | 按语言分组处理,减少前缀变异 |
4.2 最佳实践清单
- ✅ 将 System Prompt 放在请求最前面,且保证每次请求完全相同
- ✅ 对 RAG 文档排序——按文档 ID 或哈希值排序,确保相同文档集合生成相同的前缀
- ✅ 合并短消息——将多个短消息合并为一个,满足最小缓存长度要求
- ✅ 监控缓存命中率——设置告警阈值,命中率低于 30% 时触发排查
- ✅ 使用
JSON.stringify的确定性序列化——确保 JSON 内容每次输出一致 - ❌ 不要在 System Prompt 中嵌入时间戳或请求 ID
- ❌ 不要频繁修改工具定义——每次修改都会导致缓存失效
- ❌ 不要对需要精确结果的查询使用语义缓存——如代码生成、数学计算
4.3 不同场景的缓存策略选择
| 场景 | 推荐方案 | 预期节省 |
|---|---|---|
| RAG 文档问答 | Anthropic 显式标记 + 前缀标准化 | 70%-85% |
| 多轮客服对话 | Google Context Caching | 60%-75% |
| 代码生成(Copilot 类) | OpenAI 自动缓存 | 40%-60% |
| 批量文档分析 | Google Context Caching | 80%-90% |
| 实时流式对话 | Anthropic 显式标记 | 50%-70% |
✅ 总结
Prompt Caching 是 2026 年 LLM 应用成本优化中投入产出比最高的技术手段——不需要改变业务逻辑,不需要更换模型,只需要调整请求结构和添加缓存标记,就能获得 50%-90% 的成本节省。
核心要点回顾:
- 请求前缀一致性是缓存命中率的决定因素——把最稳定的内容放在最前面
- 根据供应商选择对应方案——OpenAI 最省心、Anthropic 最灵活、Google 最适合长上下文
- 建立缓存监控体系——没有监控就没有优化,命中率低于 30% 需要立即排查
- 分层缓存架构效果最佳——语义缓存处理重复查询,Prompt Caching 处理前缀匹配
⚡ **关键结论:**在 LLM 应用中,Prompt Caching 不是「可选优化」,而是「生产必备」。任何日均超过 1 万次 API 调用的应用,都应该在上线前完成缓存优化。一个配置良好的缓存系统,每月可以为你的团队节省数千到数万美元。