缓存、限流与成本优化:让 LLM 应用活得下去 本节摘要:多数 AI 创业公司不是死于模型差,而是死于单位经济算不过账。一次 GPT-4o 调用几分之一美分,一万个用户每天调十次,光输入 token 每天就 250 美元——你还没收进一分钱。活下来的公司,把每次 API 调用当成一笔财务交易,而非一次函数调用。本节带你建立成本优化的全套武器:LLM 调用的五项成本拆解(系统提示是沉默杀手);厂商级提示缓存(Anthropic 显式 cachecontrol 命中省 90%、OpenAI 自动前缀省 50%);语义缓存(嵌入查询、算余弦、命中阈值 0.920.
本节摘要:多数 AI 创业公司不是死于模型差,而是死于单位经济算不过账。一次 GPT-4o 调用几分之一美分,一万个用户每天调十次,光输入 token 每天就 250 美元——你还没收进一分钱。活下来的公司,把每次 API 调用当成一笔财务交易,而非一次函数调用。本节带你建立成本优化的全套武器:LLM 调用的五项成本拆解(系统提示是沉默杀手);厂商级提示缓存(Anthropic 显式 cache_control 命中省 90%、OpenAI 自动前缀省 50%);语义缓存(嵌入查询、算余弦、命中阈值 0.920.95,抓住「换个说法的同一问题」);精确缓存(温度 0 的哈希命中);令牌桶限流(按用户分层配额);模型路由(简单问题送便宜模型,省 4070%);成本追踪与三档熔断(70% 警告、85% 降级、95% 只返回缓存)。一套组合拳把月账单从 22500 美元砍到 5000 美元。
对应原课程:Phase 11 · Lesson 11 ·
caching-cost(原英文phases/11-llm-engineering/11-caching-cost/docs/en.md)。本节讲应用层缓存;第 15 节讲厂商层提示缓存,两者叠加可降本 50~95%。
阅读完本节,你应当能够:
你做了个 RAG 聊天机器人,跑得很美,用户喜欢。然后账单来了。
GPT-5 输入 5 美元/百万 token、输出 15 美元;Claude Opus 4.7 输入 15 美元、输出 75 美元;Gemini 3 Pro 输入 1.25 美元、输出 5 美元;GPT-5-mini 输入 0.25 美元、输出 2 美元(价格为示例,务必查厂商当前价目)。
杀死创业公司的数学:
日输入成本:10000 × 10 × 1000 / 1000000 × 2.5 = 250 美元/天
日输出成本:10000 × 10 × 500 / 1000000 × 10 = 500 美元/天
月合计:22500 美元/月
这还只是 LLM。加上嵌入、向量库托管、基础设施,一个聊天机器人要 30000 美元/月。
更残酷的是:这些查询里 40~60% 是近似重复。用户用略不同的措辞问同样的问题;你的系统提示每次请求都一样,却每次都计费;RAG 检索的上下文文档在问同一主题的用户间重复。你在为冗余计算付全价。
每次 API 调用有五项成本组件。
系统提示是沉默杀手。一个 1500 token 的系统提示,随每次请求发送,光这个前缀每百万请求就 3.75 美元。日 10 万请求,每天 375 美元——每月 11250 美元——花在从不改变的文本上。
2026 年三大厂商都提供厂商侧提示缓存,但机制不同(详见第 15 节)。
| 厂商 | 机制 | 折扣 | 最低门槛 | 缓存时长 |
|---|---|---|---|---|
| Anthropic | 显式 cache_control 标记 |
命中省 90%(写入多付 25%) | 1024 token(Sonnet/Opus)、2048(Haiku) | 默认 5 分;扩展 1 小时(写入 2 倍溢价) |
| OpenAI | 自动前缀匹配 | 命中省 50% | 1024 token | 尽力而为,最长 1 小时 |
| Google Gemini | 显式 CachedContent API | 约省 75%(加存储费) | 4096(Flash)/32768(Pro) | 用户配置 TTL |
Anthropic 的方式是显式的。你用 cache_control: {"type":"ephemeral"} 标记提示段落,首次请求付 25% 写入溢价,后续相同前缀的请求拿 90% 折扣。一个 2000 token 系统提示,正常 0.005 美元,缓存命中时 0.000625 美元。10 万请求省 437.50 美元/天。
OpenAI 的方式是自动的。任何匹配先前请求的前缀拿 50% 折扣,无需标记。代价是折扣少、控制少,但零实现成本。
厂商缓存只对相同前缀生效。语义缓存处理更难的场景:不同查询、相同含义。
「退货政策是什么?」和「我怎么退货?」是不同字符串但同一意图。语义缓存嵌入两个查询,算余弦相似度,超过阈值(典型 0.92~0.95)就返回缓存响应。
嵌入成本可忽略。OpenAI text-embedding-3-small 每百万 token 0.02 美元。查缓存相比一次完整 LLM 调用,几乎不花钱。
对确定性调用(温度 0、同模型、同提示),精确缓存更简单更快。哈希整个提示,查缓存,命中就返回。完美适用于:系统提示 + 固定上下文 + 相同用户查询;带相同工具定义的函数调用;同一文档被多次处理的批处理。
限流不只关乎公平,更关乎生存。
令牌桶算法:每用户一个桶,容量 N,以每秒 R 速率补充。请求从桶里消耗 token,桶空就拒绝。这允许突发(一次用满整桶)同时强制平均速率。
按用户配额:按用户层设日/月 token 上限。
| 层级 | 日 token 上限 | 最大请求/分 | 模型访问 |
|---|---|---|---|
| 免费 | 50000 | 10 | 仅 GPT-4o-mini |
| Pro | 500000 | 60 | GPT-4o、Claude Sonnet |
| 企业 | 5000000 | 300 | 全部模型 |
不是每个查询都需要 GPT-4o。「门店几点关门?」不需要 10 美元/百万输出的模型,GPT-4o-mini 0.60 美元/百万完美胜任,Claude Haiku 1.25 美元也行。一个简单分类器把便宜查询送便宜模型、复杂查询送贵模型。
调校良好的路由器光模型成本就省 40~70%。
你优化不了你测不了的东西。每次 API 调用记录:时间戳、模型名、输入 token、输出 token、延迟(ms)、计算成本(美元)、用户 ID、缓存命中/未命中、请求类别。这数据揭示哪些功能贵、哪些用户重度消费、缓存哪里影响最大。
OpenAI Batch API 异步处理请求,打 50% 折扣。提交最多 5 万请求的批次,24 小时内返回结果。用于:夜间文档处理、批量分类、评估运行、数据富化管线。不用于:实时面向用户的查询(延迟要紧)。
熔断器在你触顶时停掉花费。没有它,一个 bug 或滥用能在几小时内烧穿月预算。设三档阈值:
按顺序叠加这些技术,每层在前一层基础上复利。
| 层 | 技术 | 典型节省 | 实现难度 |
|---|---|---|---|
| 1 | 厂商提示缓存 | 30~50% | 低(加缓存标记) |
| 2 | 精确缓存 | 10~20% | 低(哈希 + 字典) |
| 3 | 语义缓存 | 15~30% | 中(嵌入 + 相似度) |
| 4 | 模型路由 | 40~70% | 中(分类器) |
| 5 | 限流 | 保护预算 | 低(令牌桶) |
| 6 | 提示压缩 | 10~30% | 中(改写提示) |
| 7 | 批处理 | 符合条件者省 50% | 低(批次 API) |
一个应用 15 层的 RAG 应用,典型把成本从 22500 美元/月降到 40006000 美元/月。这是烧光跑道和做成生意之间的差别。
一个服务 1 万 DAU 的 RAG 聊天机器人真实账本:
| 指标 | 优化前 | 优化后 | 节省 |
|---|---|---|---|
| 月 LLM 成本 | 22500 美元 | 5200 美元 | 77% |
| 平均每次查询成本 | 0.0075 美元 | 0.0017 美元 | 77% |
| 缓存命中率 | 0% | 52% | — |
| 路由到 mini 的查询 | 0% | 65% | — |
| P95 延迟 | 2800ms | 900ms(缓存命中 50ms) | 68% |
| 月嵌入成本 | 0 美元 | 180 美元 | (新增成本) |
| 月总成本 | 22500 美元 | 5380 美元 | 76% |
语义缓存的嵌入成本(180 美元/月)在缓存命中开始的第一小时内就回本了。
完整代码见原课程 code/caching_cost.py,这里给出关键骨架。
import hashlib, time, json, math MODEL_PRICING = { "gpt-4o": {"input": 2.50, "output": 10.00, "cached_input": 1.25}, "gpt-4o-mini": {"input": 0.15, "output": 0.60, "cached_input": 0.075}, "claude-opus-4": {"input": 15.00,"output": 75.00, "cached_input": 1.50}, "claude-sonnet-4": {"input": 3.00, "output": 15.00, "cached_input": 0.30}, "claude-haiku-3.5":{"input": 0.80, "output": 4.00, "cached_input": 0.08}, "gemini-2.5-pro": {"input": 1.25, "output": 10.00, "cached_input": 0.3125}, "gemini-2.5-flash":{"input": 0.15, "output": 0.60, "cached_input": 0.0375}, } def calculate_cost(model, input_tokens, output_tokens, cached_input_tokens=0): if model not in MODEL_PRICING: return {"error": f"未知模型: {model}"} p = MODEL_PRICING[model] non_cached = input_tokens - cached_input_tokens input_cost = (non_cached / 1_000_000) * p["input"] cached_cost = (cached_input_tokens / 1_000_000) * p["cached_input"] output_cost = (output_tokens / 1_000_000) * p["output"] return {"input_cost": round(input_cost, 6), "cached_input_cost": round(cached_cost, 6), "output_cost": round(output_cost, 6), "total_cost": round(input_cost + cached_cost + output_cost, 6)}
class ExactCache: def __init__(self, max_size=1000, ttl_seconds=3600): self.cache, self.max_size, self.ttl = {}, max_size, ttl_seconds self.hits = self.misses = 0 def _hash(self, model, messages, temperature): key = json.dumps({"model": model, "messages": messages, "temperature": temperature}, sort_keys=True, ensure_ascii=False) return hashlib.sha256(key.encode()).hexdigest() def get(self, model, messages, temperature=0.0): if temperature > 0: # 非确定性,跳过缓存 self.misses += 1; return None key = self._hash(model, messages, temperature) if key in self.cache: entry = self.cache[key] if time.time() - entry["timestamp"] < self.ttl: self.hits += 1 entry["access_count"] += 1 return entry["response"] del self.cache[key] self.misses += 1 return None def put(self, model, messages, temperature, response): if temperature > 0: return if len(self.cache) >= self.max_size: # LRU 简化:踢最旧 oldest = min(self.cache, key=lambda k: self.cache[k]["timestamp"]) del self.cache[oldest] self.cache[self._hash(model, messages, temperature)] = { "response": response, "timestamp": time.time(), "access_count": 1}
def simple_embed(text): """简易词频嵌入(生产中换成真实嵌入 API)。""" words = text.lower().split() vocab = {} for w in words: vocab[w] = vocab.get(w, 0) + 1 norm = math.sqrt(sum(v*v for v in vocab.values())) return {k: v/norm for k, v in vocab.items()} if norm else {} def cosine_similarity(a, b): if not a or not b: return 0.0 return sum(a.get(k, 0) * b.get(k, 0) for k in set(a) | set(b)) class SemanticCache: def __init__(self, similarity_threshold=0.85, max_size=500, ttl_seconds=3600): self.entries, self.threshold = [], similarity_threshold self.max_size, self.ttl = max_size, ttl_seconds self.hits = self.misses = 0 def get(self, query): q_emb = simple_embed(query) now = time.time() best_match, best_sim = None, 0.0 for entry in self.entries: if now - entry["timestamp"] > self.ttl: continue sim = cosine_similarity(q_emb, entry["embedding"]) if sim > best_sim: best_sim, best_match = sim, entry if best_match and best_sim >= self.threshold: self.hits += 1 best_match["access_count"] += 1 return {"response": best_match["response"], "similarity": round(best_sim, 4), "original_query": best_match["query"]} self.misses += 1 return None def put(self, query, response): if len(self.entries) >= self.max_size: self.entries.sort(key=lambda e: e["timestamp"]) self.entries.pop(0) self.entries.append({"query": query, "embedding": simple_embed(query), "response": response, "timestamp": time.time(), "access_count": 1})
💡 阈值 0.85 是演示用,生产用 0.92~0.95。太低会把「退货政策」和「换货政策」混为一谈,返回错误答案;太高抓不住同义改写。要用你的真实查询集调阈值,在召回与精度间找平衡。
class TokenBucketRateLimiter: def __init__(self): self.buckets = {} self.tiers = { "free": {"capacity": 50_000, "refill_rate": 500, "max_rpm": 10}, "pro": {"capacity": 500_000, "refill_rate": 5_000, "max_rpm": 60}, "enterprise":{"capacity": 5_000_000,"refill_rate": 50_000,"max_rpm": 300}, } def _get_bucket(self, user_id, tier="free"): if user_id not in self.buckets: cfg = self.tiers.get(tier, self.tiers["free"]) self.buckets[user_id] = {"tokens": cfg["capacity"], "capacity": cfg["capacity"], "refill_rate": cfg["refill_rate"], "last_refill": time.time(), "request_timestamps": [], "max_rpm": cfg["max_rpm"], "tier": tier, "total_tokens_used": 0} return self.buckets[user_id] def _refill(self, b): now = time.time() elapsed = now - b["last_refill"] refill = int(elapsed * b["refill_rate"]) if refill > 0: b["tokens"] = min(b["capacity"], b["tokens"] + refill) b["last_refill"] = now def check(self, user_id, tokens_needed, tier="free"): b = self._get_bucket(user_id, tier) self._refill(b) now = time.time() b["request_timestamps"] = [t for t in b["request_timestamps"] if now - t < 60] if len(b["request_timestamps"]) >= b["max_rpm"]: return {"allowed": False, "reason": "rate_limit", "retry_after_seconds": 60 - (now - b["request_timestamps"][0])} if b["tokens"] < tokens_needed: wait = (tokens_needed - b["tokens"]) / b["refill_rate"] return {"allowed": False, "reason": "token_limit", "tokens_available": b["tokens"], "retry_after_seconds": round(wait, 1)} return {"allowed": True, "tokens_available": b["tokens"]} def consume(self, user_id, tokens_used, tier="free"): b = self._get_bucket(user_id, tier) b["tokens"] -= tokens_used b["request_timestamps"].append(time.time()) b["total_tokens_used"] += tokens_used
class CostTracker: def __init__(self, monthly_budget=1000.0): self.logs, self.monthly_budget, self.alerts = [], monthly_budget, [] def log_call(self, model, input_tokens, output_tokens, cached_input_tokens=0, latency_ms=0, user_id="anonymous", cache_status="miss"): cost = calculate_cost(model, input_tokens, output_tokens, cached_input_tokens) self.logs.append({"timestamp": time.time(), "model": model, "input_tokens": input_tokens, "output_tokens": output_tokens, "cached_input_tokens": cached_input_tokens, "latency_ms": latency_ms, "cost": cost.get("total_cost", 0), "user_id": user_id, "cache_status": cache_status}) self._check_budget() return self.logs[-1] def _check_budget(self): total = self.total_cost() pct = total / self.monthly_budget if self.monthly_budget > 0 else 0 levels = [(0.95, "stop"), (0.85, "throttle"), (0.70, "warning")] for threshold, level in levels: if pct >= threshold and not any(a["level"] == level for a in self.alerts): self.alerts.append({"level": level, "message": f"预算已消耗 {pct:.0%}: ${total:.2f}/${self.monthly_budget:.2f}"}) def total_cost(self): return round(sum(e["cost"] for e in self.logs), 6)
SIMPLE_KW = ["几点", "地址", "电话", "价格", "退货政策", "你好", "谢谢", "是的"] COMPLEX_KW = ["分析", "比较", "解释为什么", "写代码", "调试", "架构", "权衡", "评估"] def classify_complexity(query): q = query.lower() if len(q.split()) <= 5 or any(k in q for k in SIMPLE_KW): return "simple" if any(k in q for k in COMPLEX_KW): return "complex" return "medium" def route_model(query, tier="pro"): complexity = classify_complexity(query) table = { "simple": {"free": "gemini-2.5-flash", "pro": "gpt-4o-mini", "enterprise": "gpt-4o-mini"}, "medium": {"free": "gpt-4o-mini", "pro": "claude-sonnet-4", "enterprise": "claude-sonnet-4"}, "complex": {"free": "gpt-4o-mini", "pro": "gpt-4o", "enterprise": "claude-opus-4"}, } return {"query": query, "complexity": complexity, "model": table[complexity].get(tier, "gpt-4o-mini"), "tier": tier}
# import anthropic # client = anthropic.Anthropic() # resp = client.messages.create(model="claude-sonnet-5", max_tokens=1024, # system=[{"type":"text", # "text":"你是 Acme 公司的客服代理……(长系统提示)", # "cache_control": {"type":"ephemeral"}}], # messages=[{"role":"user","content":"退货政策是什么?"}]) # print(f"输入 token: {resp.usage.input_tokens}") # print(f"缓存写入 token: {resp.usage.cache_creation_input_tokens}") # print(f"缓存读取 token: {resp.usage.cache_read_input_tokens}")
首次调用写缓存(25% 溢价),之后每次同系统提示前缀的调用读缓存(90% 折扣),缓存 5 分钟,每次命中重置计时。
# from openai import OpenAI # client = OpenAI() # resp = client.chat.completions.create(model="gpt-4o", messages=[ # {"role":"system","content":"你是客服代理……(1024+ token)"}, # {"role":"user","content":"退货政策是什么?"}]) # print(f"提示 token: {resp.usage.prompt_tokens}") # print(f"缓存 token: {resp.usage.prompt_tokens_details.cached_tokens}")
OpenAI 自动缓存,1024+ token 的前缀匹配近期请求就 50% 折扣,无需改代码,查 prompt_tokens_details.cached_tokens 验证生效。
# from openai import OpenAI # client = OpenAI() # 请求写成 JSONL,每行一个 {custom_id, method, url, body} # batch_file = client.files.create(file=open("batch_input.jsonl","rb"), purpose="batch") # batch = client.batches.create(input_file_id=batch_file.id, # endpoint="/v1/chat/completions", completion_window="24h")
Batch API 全 token 平打 50% 折扣,24 小时内出结果,适合非实时工作负载:评估、数据标注、批量摘要。
# 生产中把线性扫描换成向量索引(Redis Vector Search、Pinecone、pgvector)。 # 线性扫描撑 <1000 条;再大用 ANN 做 O(log n) 查找。
GPTCache 是开源语义缓存库,支持多种嵌入后端、向量库、淘汰策略;Helicone 作为代理层提供成本追踪、缓存、限流、预算告警;Not Diamond、Martian 是 ML 路由器,从你的流量模式学习,跨厂商优化成本/质量权衡。本节的从零实现让你看清每一层;生产里这些工具能省去重复造轮子。
本节产出两个可复用文件(位于原课程 outputs/):
prompt-cost-optimizer.md:一个元提示,分析你的 LLM 应用并推荐具体成本优化,附 projected savings(预测节省)。skill-cost-patterns.md:一个决策框架,按你的用例选择合适的缓存策略、限流配置、模型路由规则。Python 代码(code/caching_cost.py)是一套独立成本优化层,把 simulate_llm_call 换成真实 API、simple_embed 换成真实嵌入,即可投产;成本计算、缓存、限流、路由、熔断逻辑均无需修改。
语义缓存 LRU 淘汰:把最旧优先淘汰换成最久未用(LRU)。跟踪每条目最后访问时间,缓存满时淘汰最久未用的。在 100 条查询上对比两种策略的命中率。
成本预测工具:给定 API 调用日志(CostTracker logs),按近 7 天均值预测月成本,考虑工作日/周末模式。若预测月成本超预算 20% 触发告警。
分层语义缓存:用两个相似度阈值:0.98 高置信命中(直接返回)和 0.90 中置信命中(带免责声明返回「基于一个类似的过往问题……」)。跟踪每档命中来源,测量用户满意度差异。
嵌入式模型路由分类器:把基于关键词的分类器换成基于嵌入的。嵌入 50 条标注查询(简单/中等/复杂),新查询按最近邻分类。在 20 条测试集上测分类准确率。
带降级档的熔断器:70% 预算记警告;85% 自动把所有路由切到最便宜模型(gpt-4o-mini);95% 只返回缓存、拒新查询。用 1000 次请求对 1 美元预算模拟,验证每档阈值正确触发。
下一节,我们转向护栏与安全——当 LLM 应用面向真实用户,如何拦住越狱、提示注入、有害输出、隐私泄露,把质量和成本之上的「不出事」底线守住。