做了快两年大模型应用开发,我最大的感受不是模型能力不够强,而是账单长得太快。很多团队第一次看到月度API费用时,第一反应都是“我们真的调了这么多次吗”,第二反应才是“能不能少花点”。这个问题其实可以拆成两件事:一是怎么让每次调用都不白花钱,二是怎么在源头选对平台、选对模型。我这次想聊的,就是围绕“大模型API缓存策略”和“平台选型”这两个核心动作,把企业团队和个人开发者的成本优化路径讲透。
先说结论:缓存策略解决的是“重复请求别重复花钱”,平台选型解决的是“不同请求花不同档位的钱”,两者叠加,通常能把账单压到原来的三分之一甚至更低。本文会从成本构成、缓存方案设计、平台评估框架、可落地的代码实现,到上线后的排障技巧,完整走一遍。无论你是给公司做AI中台,还是一个人维护几个小应用,里面大部分思路都能直接照搬。
1. 先把账算明白:大模型API的钱到底花在哪
很多人的降本动作起点就错了,一上来就问“哪家平台便宜”,却连自己的钱主要烧在哪个环节都没搞清楚。所以第一步不是选平台,而是拆账单。
1.1 成本构成的四个“黑洞”
大模型API的费用表面上只有一行“Token消耗”,实际拆开来,至少有四个地方在悄悄放大开销。
第一个黑洞:输出Token的定价放大。几乎所有主流平台都是输入和输出分开计价,而输出Token的价格通常是输入的三到五倍。流式接口尤其容易浪费——用户没看完就关闭页面,但已经生成的Token照样计费。我见过不少应用,用户只是随手打开对话框看一眼,成本就默默记上了。
第二个黑洞:并发重复。同样的一个问题,十个用户问,就要付十次钱。这在客服问答、知识库检索、内容审核场景里尤其明显。很多请求只是换了人名、日期或几个无关紧要的修饰词,模型输出的核心内容几乎一致,但钱是全额扣的。
第三个黑洞:失败重试的重复计费。平台限流(常见的就是返回“繁忙”或“请求过多”)时,你的客户端通常会自动重试。重试意味着再次计费,如果重试逻辑写得不讲究,一次高峰期的接口抖动,能把成本放大两三倍。
第四个黑洞:峰值冗余。为了撑住某个瞬间的流量,企业会让API这把“水龙头”一直开着最大。平时用不到这么多并发,但费用是按“可用额度”或“并发上限”占用的,这部分属于结构性浪费。
1.2 用一次真实场景估算成本与缓存收益
为了把问题说具体,我模拟一个典型的企业内部知识库问答应用。假设日均调用10万次,每次请求平均输入1200 Token、输出300 Token。按一个常见的中高精度模型定价粗算:输入每百万Token约5元,输出每百万Token约20元。
| 项目 | 日调用量 | 单次输入Token | 单次输出Token | 日成本估算 |
|---|---|---|---|---|
| 原始调用 | 10万次 | 1200 | 300 | 约1.2万元 |
| 精确缓存命中50% | 5万次直返,5万次真实调用 | 1200 | 300 | 约6000元 |
| 精确缓存50% + 语义缓存20% | 7万次直返,3万次真实调用 | 1200 | 300 | 约3600元 |
这里的算法很直观:每次真实调用成本是输入单价×(输入Token/100万) + 输出单价×(输出Token/100万),也就是5×0.0012 + 20×0.0003 = 0.006 + 0.006 = 0.012元。10万次就是1200元/日——注意,我这里把价格压得很保守,实际很多场景的输入更长、输出更长,账单更惊人。
加上缓存后的逻辑是:命中的请求直接从缓存拿结果,一分钱Token费用都不产生。上面的表里,精确缓存命中率做到50%,语义缓存再补20%,日成本就能从1.2万降到3600元,降幅70%。这个数字不是理论值,是我在实际系统里观测到的一个很典型的结果。所以“先缓存、再选型”这个顺序不能反。
2. 缓存策略怎么设计:从精确命中到语义召回
缓存不是简单地“把结果存起来”,核心问题有三个:缓存什么、怎么命中、何时失效。这一节我按从简单到复杂的顺序展开。
2.1 三类真正值得缓存的请求
不是所有大模型请求都适合缓存。有些请求天然变化,比如“帮我写一封给张三的邮件,主题是续约”,里面的人名和主题每次都在变,缓存价值有限。适合缓存的请求有三个特征:
确定性强。同样的输入应该有同样的或高度一致的输出。典型场景包括:文本分类、情感判断、信息抽取、实体识别、关键词提取、摘要生成。这些任务本质上是“把结构从非结构里挖出来”,结果基本稳定,缓存命中后质量几乎无损。
频率高且重复。最常见的例子是FAQ问答、产品规则查询、系统提示词拼接后的大量固定业务请求。比如一个电商助手,用户反复问“退换货政策是什么”,模型每次生成的答案大同小异,这种请求如果每次都买Token,就是在给平台送钱。
流式拼接型。很多应用用流式输出做打字机效果,但流式结果在服务端其实是完整的一整段文本。如果业务上允许,可以把流式结果完整落盘,相同问题再次出现时直接回放整段内容,连打字机效果都能模拟。这个技巧被很多人忽略,它对用户体验几乎没有影响,却能省下一大笔重复流式输出的钱。
2.2 精确缓存:最快见效的第一板斧
精确缓存的技术门槛最低,效果立竿见影。它的逻辑就是“完全相同的请求,直接返回之前的答案”。最简实现是:
import hashlib import json import time class ExactCache: def __init__(self, ttl=86400): # 简单演示用dict,生产环境换成Redis self._store = {} self._ttl = ttl def _normalize_prompt(self, messages): # 核心:按规则把输入转成稳定字符串 # 1. 去掉时间戳、随机数、无关空行 # 2. 系统提示词和用户消息按固定顺序拼接 # 3. 对文本做strip和压缩空白 parts = [] for msg in messages: parts.append(f"{msg['role']}:{msg['content'].strip()}") return "\n".join(parts) def _build_key(self, model, temperature, messages): payload = json.dumps({ "model": model, "temperature": temperature, "prompt": self._normalize_prompt(messages) }, ensure_ascii=False) return hashlib.sha256(payload.encode("utf-8")).hexdigest() def get(self, model, temperature, messages): key = self._build_key(model, temperature, messages) item = self._store.get(key) if not item: return None if time.time() - item["ts"] > self._ttl: self._store.pop(key, None) return None return item["content"] def set(self, model, temperature, messages, content): key = self._build_key(model, temperature, messages) self._store[key] = {"content": content, "ts": time.time()}这段代码最容易被忽略的就是_normalize_prompt。很多人缓存命中率上不去,不是缓存机制本身有问题,而是请求里的杂质太多——某个时间戳、某个随机UUID、系统提示词里自动附加的当前日期,都会让同样的语义请求变成不同的字符串。
2.3 语义缓存:用向量召回解锁更多命中
精确缓存只能救“一模一样”的请求,但真实业务里用户换个说法、加个语气词就完全不同了。这时候需要语义缓存。
语义缓存的核心思路:请求文本先转成向量(embedding),然后在向量存储里检索,如果找到一条历史请求与当前请求的相似度超过阈值,就把那条请求对应的答案返回。
这里有两个关键参数:向量维度和相似度阈值。向量维度取决于你用的embedding模型,常见的有几百到上千维不等。相似度阈值则是缓存系统的灵魂——太高则命中太少,太低则风马牛不相及的结果会被当成答案返回。
我实践下来的建议是:
| 业务类型 | 推荐阈值 | 原因 |
|---|---|---|
| FAQ问答 | 0.92 ~ 0.95 | 答案必须精确,容错低 |
| 内容摘要 | 0.88 ~ 0.92 | 小幅改写不影响结果 |
| 闲聊陪伴 | 0.82 ~ 0.88 | 语义漂移容忍度较高 |
| 代码生成 | 0.95以上 | 代码错误代价高 |
语义缓存的工程实现也不复杂,流程是:请求进来 → 用embedding模型生成向量 → 向量检索取Top1 → 算相似度 → 超阈值则返回缓存。这里有个容易被忽略的成本问题——embedding本身也要花钱。如果每次请求都做一次embedding,你的成本反而变高了。所以实务上通常只在精确缓存未命中后,才进入语义缓存判断,而且embedding模型选择小型低价的就够用。
2.4 缓存Key设计与失效策略
Key的设计决定了命中的上限。我的经验是,缓存Key至少包含四个维度:
- 模型标识:不同模型的能力和风格不同,必须隔离。
- 生成参数:temperature、top_p等参数直接影响输出分布,最好纳入Key,或者在取缓存时做校验。
- 请求内容归一化后的哈希:这是核心。
- 系统版本号或“提示词版本号”:业务升级了系统提示词后,旧缓存必须能一键作废。
失效策略上,最怕的是“一个错误答案被缓存了一整天”。我的建议是三条规则:
- 非正常响应的结果一律不缓存,比如限流提示、安全拒答、空结果。
- 业务数据源更新时,通过版本号机制主动清空相关缓存。
- 给不同场景设置不同的TTL。摘要类可以缓存一整天,信息抽取类可以缓存一周,带时效性的问题(如“今天的天气”)TTL压到几分钟甚至不缓存。
3. 平台选型:企业团队和个人开发者的两套打法
缓存救了重复请求的钱,但“同一个请求在不同平台花不同的钱”这件事,只有选型才能解决。很多人的选型误区是只比单价,结果综合成本反而更高。这里我给出两套完全不同的评估体系。
3.1 选型前必须理解的定价模式差异
大模型API的平台定价,表面上都是“按Token计费”,实际差异很大。
按输入输出分别计费是最主流的模式。输入便宜、输出贵。适合“短输入长输出”任务多的团队,你要重点压输出Token数量。
统一计费的模式省心,但通常单价偏高,适合调用量小的个人项目,算钱简单。
订阅套餐或按量阶梯模式,适合调用量相对稳定的企业。要警惕的是“阶梯价比单量价更贵”的情况。很多平台阶梯的单价其实是累进的,不是递减的,别被“阶梯价”三个字骗了。
自研/开源模型的托管服务,价格通常只有商业模型的三分之一甚至更低,但能力上限、并发稳定性都要评估。
除了Token价格,还有一个隐藏成本:限流与并发费用。有的平台低价模型并发极低,高峰期排队严重,你的业务就得买高并发包,综合下来并不便宜。所以在对比价格时,不能只看单价,要把“达到业务所需并发下的总成本”作为对比口径。
3.2 个人开发者的选型三原则
个人项目没有专职运维,没有预算冗余,选型逻辑和企业完全不同。我给个人开发者的建议是三句话。
第一,用“按量计费 + 免费额度”起步。不要一上来就买大套餐。个人应用冷启动阶段调用量很低,按量计费可能一个月就几块钱,买套餐反而浪费。
第二,能用低档模型绝不轻易升档。同一个平台通常提供多个档位的模型,低价档的模型做摘要、抽取、闲聊完全够用,不值得每个请求都跑顶配。我见过不少个人应用,把情感分析这种简单任务也丢给顶配模型,纯属烧钱。
第三,优先选有“结果缓存”或“离线批量”服务的平台。某些平台提供了服务端语义缓存、内容去重、批量异步处理等功能,能让开发者不写一行代码就省下大量Token。这种事前选型的好处,远大于事后自己去搭缓存系统。
3.3 企业团队的评估框架
企业选型要考虑的不是“哪个便宜”,而是“哪个可靠、可控、可治理”。我把评估维度整理成一张表:
| 评估维度 | 权重建议 | 考察重点 |
|---|---|---|
| 模型能力与场景匹配度 | 30% | 在同一批测试集上跑分,看准确率和稳定性 |
| 综合成本模型 | 25% | 含单价、并发费、阶梯费、缓存免费额度 |
| SLA与限流政策 | 20% | 限流阈值、故障补偿、高峰期表现 |
| 数据安全与合规 | 15% | 数据是否用于训练、私有化部署选项 |
| 生态与工具链 | 10% | SDK成熟度、监控、函数调用支持 |
这套框架里,我最想强调的是双源或多源策略。企业不应该把全部业务绑在单一平台上。哪怕某个平台当前价格最优,也要至少准备一个备选平台,平时保持小流量灰度。原因很简单:大模型平台的定价和政策变动非常频繁,今天的最优解可能下个月就不存在了。有备选,你才有谈判和迁移的底气。
此外,企业必须做预算治理。具体做法是给不同业务线设置配额和告警阈值,比如“每个应用每天API费用上限500元,超了就自动熔断降级到低档模型或返回缓存”。这个机制的重要性,甚至高于选型本身。
3.4 平台之外的三种“软降本”手段
选完平台,还有三个不换平台就能省钱的手段。
模型分级路由:请求进来先做个简单的规则判断或小模型分类,简单任务走低价模型,复杂任务才调度高档模型。我见过一个内容审核系统,把60%的请求都路由到了低档模型,整体成本下降了近一半。
异步批处理:非实时的任务(比如历史数据清洗、离线文档总结、批量埋点分析)不需要走在线API。用异步批量接口或者本地部署小模型处理,单价按量计费还便宜,又不会挤占在线业务的并发。
输出裁剪:限制max_tokens能显著压低输出成本。很多应用默认放开输出上限,模型就“滔滔不绝”,但这些多出来的内容用户未必需要。根据业务设定合理的输出长度上限,是把成本细节抠出来的重要一步。
4. 实操落地:搭一个统一的大模型API缓存代理层
前面讲了策略和选型,这一节我给出一个完整的落地参考:一个位于业务和模型API之间的“缓存代理层”。它统一接收请求、查缓存、转发模型、回写缓存、上报指标。
4.1 整体架构与模块划分
缓存代理层的核心价值是对上层业务透明——业务方不需要关心缓存逻辑,只需要按原样调用模型API。整个代理层分五个模块:
- 请求归一化模块:负责把原始请求转化为标准化缓存键,去掉干扰项。
- 精确缓存模块:基于Redis的KV缓存,TTL按场景区分。
- 语义缓存模块:负责向量化、相似度检索和阈值判定。
- 转发模块:真正的模型API调用,包含重试、超时、熔断逻辑。
- 指标模块:记录缓存命中率、平均延迟、Token节省量等。
部署形态上,小团队可以直接用Flask或FastAPI起一个独立服务;大团队可以把它做成微服务,嵌入API网关。核心思想是,它不能和任何单一业务耦合,否则复用不起来。
4.2 一个可运行的语义缓存代理骨架
下面这段代码展示最核心的链路,精确缓存查不到时,进入语义缓存判断,再不行才转发上游API。
import hashlib import numpy as np from fastapi import FastAPI, Request import redis import httpx app = FastAPI() cache = redis.Redis(host="localhost", port=6379, decode_responses=True) UPSTREAM_URL = "https://your-model-api.example/v1/chat/completions" API_KEY = "your-api-key" def get_embedding(text: str) -> list: """调用平台的embedding接口,注意选择低价embedding模型""" # 这里只是骨架,生产环境需要加缓存embedding结果,避免重复计费 resp = httpx.post( "https://your-embedding-api.example/v1/embeddings", headers={"Authorization": f"Bearer {API_KEY}"}, json={"model": "cheap-embedding-model", "input": text} ) return resp.json()["data"][0]["embedding"] def get_cache_key(model: str, messages: list) -> str: # 归一化:去掉常见噪声字段 normalized = [] for m in messages: content = " ".join(m["content"].split()) normalized.append(f"{m['role']}:{content}") raw = f"{model}|{'|'.join(normalized)}" return hashlib.sha256(raw.encode("utf-8")).hexdigest() def semantic_search(project: str, embedding: list, threshold: float): # 在向量数据库中检索,返回相似度最高的历史答案 # 这里用伪代码代替实现,可按项目用某种向量库或带向量能力的数据库 top_score = 0.86 top_answer = None if top_score >= threshold: return top_answer return None def call_upstream(model: str, messages: list, params: dict) -> dict: # 转发生成请求,带超时、重试和熔断 payload = { "model": model, "messages": messages, "temperature": params.get("temperature", 0.7), "max_tokens": params.get("max_tokens", 500) } resp = httpx.post( UPSTREAM_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json=payload, timeout=30 ) resp.raise_for_status() data = resp.json() return { "content": data["choices"][0]["message"]["content"], "usage": data.get("usage", {}) } @app.post("/v1/chat/completions") async def chat(request: Request): body = await request.json() model = body.get("model", "default-model") messages = body.get("messages", []) project = body.get("project", "default") params = body.get("params", {}) # 第一步:精确缓存 exact_key = get_cache_key(model, messages) cached = cache.get(exact_key) if cached: return {"content": cached, "from_cache": True} # 第二步:语义缓存 full_text = " ".join(m["content"] for m in messages) emb = get_embedding(full_text) sem_answer = semantic_search(project, emb, threshold=0.90) if sem_answer: # 命中后也同步回填精确缓存,加速后续相同请求 cache.set(exact_key, sem_answer, ex=86400) return {"content": sem_answer, "from_cache": True} # 第三步:调用上游 result = call_upstream(model, messages, params) content = result["content"] # 写缓存:只有成功结果才写,并且限制单条容量 if content and len(content) < 2000: cache.set(exact_key, content, ex=3600) # 同时写入向量库,代码省略 return {"content": content, "from_cache": False}代码里两个容易被踩的坑,我单独说:
一是embedding结果也要缓存。同一个prompt第一次做语义检索时生成过向量,第二次又生成一遍,这是纯浪费。我的做法是把“请求文本哈希 → embedding向量”也存进Redis,有效期设长一点,比如七天。
二是回填双缓存。语义缓存命中后,同样的问题下次还会来,直接把它回填到精确缓存里,下次就能走最快的路径,连embedding都省了。这个小优化,能让整体命中率再提升几个点。
4.3 关键参数怎么定:TTL、容量与淘汰策略
参数配置我建议用“分场景分优先级”的思路,而不是全局一套配置。
| 参数 | 推荐默认值 | 调整策略 |
|---|---|---|
| 精确缓存TTL | 1~24小时 | 时效性要求高则缩短,稳定知识可拉长 |
| 语义缓存阈值 | 0.90 | 按业务线的反馈数据动态微调 |
| 单条缓存内容上限 | 2000字符 | 避免把超长输出塞满缓存 |
| 缓存总容量 | Redis内存的30%以下 | 防止缓存挤占其他业务内存 |
| 淘汰策略 | LRU | 缓存击穿时用互斥锁保护上游 |
关于容量,经常有人忽略:大模型输出文本较长,一个缓存条目动辄几百到上千字符,如果不上限,Redis内存会被撑爆。我的做法是,超过2000字符的结果不进缓存,只给它短期TTL。这样既保证了高价值长回复能命中,又不会拖垮存储。
4.4 上线后看什么指标:把省钱这件事量化
缓存系统上线后,必须盯四个指标,不然你无法判断策略是有效还是失控。
缓存命中率是基础指标。精确缓存和语义缓存要分开统计,因为两者的成本收益完全不同。Token节省量直接对应账单下降。平均响应延迟用来确认缓存没有带来明显的体感劣化。缓存污染率则是最容易被忽视的风险指标,一旦某个错误被缓存,影响会是几何级数放大。
一个值得持续追踪的隐性收益是上游API可用性压力下降。缓存命中的请求根本不触达上游,限流风险、并发费用都会同步下降。这个指标虽然不是直接省钱,但对系统的稳定性贡献很大。
5. 常见问题与排查技巧实录
方案讲得再漂亮,落地时一定会遇到各种实际问题。这里整理几个我踩过的坑,以及对应的排查思路。
5.1 缓存命中率为什么一直上不去?
这是最常见的求助帖,原因通常是“归一化做得不到位”。我帮某个业务排查时发现,他们每条请求里都带着一个UUID,这个UUID被业务方当成日志追踪ID拼进了系统提示词,结果每次请求的缓存Key都不一样,缓存等于没做。
排查思路:先打开缓存日志,看看未命中的请求Key长什么样,重点关注“本该一样但实际不一样”的地方。常见的干扰项有时间戳、随机数、用户身份标识、会话历史拼接顺序。处理方式是,把这些干扰项从缓存Key中剔除,或者把业务里真正变化的“槽位”单独抽出来,不参与缓存键计算。还有一种特殊情况必须注意:如果某个业务就是强个性化的,比如“根据我的购物车推荐商品”,那它本质上就不适合缓存,硬做只会徒增embedding成本。
5.2 语义缓存阈值怎么调才不翻车?
阈值太高和太低都会出事。我见过一个团队把阈值设成0.99,结果命中率只有百分之几;也见过阈值设成0.75,结果用户问“怎么退货”返回了“怎么下单”的答案。
这里没有“万能阈值”,只有“动态调优”。我的做法分了四步:
- 先采集1000条真实请求,加上人工标注的“语义是不是一回事”,跑一个相似度分值的分布。
- 看分布后,寻找“同义分高分”和“异义分低分”的分界带在哪里。
- 初始阈值设在分界带偏低一侧,牺牲一点精确性换取命中率。
- 上线后监控两个指标:缓存命中结果的“点赞率”和“纠错率”。如果某个业务频繁被用户纠正,说明阈值该往上调了。
另外,不同业务线不能共用一个阈值。FAQ类要保守,闲聊类可以放开。
5.3 缓存污染:一个错误答案被缓存了一整天
缓存系统最怕的不是命中率低,而是“把错误答案缓存得很成功”。造成这个问题的典型场景有两个:
一是上游模型返回了安全拒答或空内容,这种结果混进缓存后,用户问什么都会收到“我无法回答这个问题”。解决方式是:凡是非正常完成状态的结果一律不缓存,一定只看内容质量。二是业务方在上游做了自定义提示词后,输出格式变了但缓存键没变,导致旧格式结果被当成新格式用。解决方式是:上一条也提过,把提示词版本号纳入缓存Key。
更稳妥的一招是延迟缓存——新结果不立刻写缓存,而是等它被用户或系统验证为有效后,再写入缓存。这个策略牺牲了一点命中率,但能极大降低污染风险。
5.4 429限流与重试的隐性成本怎么控?
接口返回429后,如果客户端立刻重试,不仅大概率继续429,还会产生新的计费请求。更麻烦的是,重试请求会刷新缓存中的“最近一次调用时间”,让本来可以缓存的请求变得不可缓存。
我的实践是三层防护:
- 指数退避:第一次429等1秒,第二次等2秒,第三次等4秒,最多三次。不要无脑重试。
- 熔断:一个平台连续1分钟错误率超过30%,自动熔断10分钟,切到备用平台或直接返回缓存。
- 缓存击穿保护:高并发下同一个key同时失效,会有大量请求同时穿透到上游。用分布式锁或“单飞”机制,只让一个请求去上游,其他请求等待结果后共享缓存。这个保护既是省钱手段,也是稳定性手段。
5.5 成本数据怎么汇报给老板?
最后补充一个很实际的问题:技术把事情做了,老板怎么判断价值?我的经验是,汇报时不要讲“我们做了缓存”,要讲“本月相比上月在相同业务量下,API费用下降了百分之多少”。如果可能,把Token节省量折算成具体金额。这不仅是汇报技巧,也能迫使自己把成本治理当成一个持续迭代的系统,而不是一次性的优化项目。
6. 一些掏心窝的建议
拿我自己来说,最初做缓存是因为一个账单把我看懵了,当时只想赶紧堵住漏洞。但做到后面我发现,成本治理这件事不是“一锤子买卖”,它是一个持续的节奏。最稳妥的推进路线是这样:第一周,只做精确缓存,把重复请求挡住,这一步通常就能省下三成以上;第二周,加上语义缓存,把同义不同文的请求也兜住;第三周开始,把精力放到平台选型和模型分级上,让每种请求都匹配到最合适价位的模型。
如果只让我留一条建议,那就是别在还没量数据的时候去谈平台选型。先把缓存做起来,把请求记录下来,你很快就能看清自己的调用画像:哪些是高频低价值,哪些是低频高价值,哪些根本不该走大模型API。有了这些数据,选型才不是拍脑袋。
最后一个小技巧:把缓存命中情况写进业务日志,形成每周一次的“成本review”习惯。时间一长,你对哪些业务能缓存、哪些业务不能缓存、哪家平台的调价让你最难受,会形成非常敏锐的感觉。这种对成本细节的掌控感,和写出一个漂亮功能带来的成就感一样高。