先说数字,再讲坑。
日均调用 1,000 → 100,Token 消耗降 87%,P95 延迟从 8.2s 降到 1.4s,月账单从 4,832 降到 461。
做了五件事:缓存、模型分级、Prompt 精简、批处理、流式输出。
听着都是常规操作,但每个都踩了坑。这篇主要讲坑,代码放最后。
坑一:语义缓存的阈值,抄来的都是错的
网上讲语义缓存的文章,阈值清一色写 0.95。我一开始也是这么设的,跑了三天,命中率 41%,看着挺好。
然后客服反馈来了:用户问"写个快速排序",返回的是归并排序的代码。
我以为是模型抽风,查了日志才发现是缓存命中------两条 query 的 embedding 相似度 0.951,超过阈值,直接返回了缓存。
问题在于,0.95 这个数字跟你的业务无关。它是别人在自己数据上标定出来的,你抄过来就是赌博。
正确的做法是拿自己的数据标:
我们的标定结果:
| 阈值 | 准确率 | 召回率 |
|---|---|---|
| 0.90 | 91.2% | 67% |
| 0.92 | 93.5% | 58% |
| 0.95 | 98.6% | 41% |
| 0.97 | 99.4% | 28% |
最后选 0.95。不是因为准确率高,是因为缓存的错误代价远大于未命中的代价。未命中无非多花点钱,命中错误是直接给用户错误答案。
这个道理跟 CDN 缓存不一样。CDN 缓存错了顶多内容旧一点,LLM 缓存错了是逻辑错误。
还有一点:TTL 一定要分级。我们一开始没设过期,公司改了报销制度,缓存还在返回旧答案,被财务找上门。现在制度类 7 天,操作类 30 天,实时数据不缓存。
坑二:模型分级,规则顺序能坑死人
我们 60% 的请求是意图分类、实体抽取、格式转换这类简单任务,全切到 mini 上,成本直接砍一半。
切之前我做了 A/B,抽 100 条对比两个模型的输出。人工看下来差异率 2% 左右,觉得没问题,上了。
上线一周,投诉集中在"代码生成"上。
我纳闷,代码生成明明没切啊。查日志才发现,问题出在分类器上。
有个需求是"帮我写个函数,把这段文本里的订单号提取出来",规则匹配到了 写.*函数,判成代码生成走 GPT-4o,没问题。
但另一个需求是"提取订单号并生成一个 JSON",规则先匹配到 提取.*订单,判成实体抽取走 mini,结果 mini 输出的 JSON 格式有问题,少了一层嵌套。
规则匹配是有顺序的,先匹配到的赢。 我的规则表里 entity_extract 排在 code_generation 前面,导致复合任务被误判。
改成按"任务复杂度"排序,复杂的规则优先:
改完之后差异率降到 0.8%。
教训:规则分类器看起来简单,但顺序设计是个真问题。要么按复杂度排,要么用打分制而不是首个匹配。
还有一个:max_tokens 也要跟着分级。很多团队只改模型不改输出上限,结果模型便宜了但输出还是那么长,省不了多少。意图分类的输出根本不需要 4096 token,128 够用。
坑三:Prompt 精简,删掉了一条安全指令
这条最不该犯。
我们的 prompt 模板里有这么一段:
你是客服助手。回答用户关于产品的问题。
不得提供医疗、法律、投资方面的专业建议。
不确定时如实说明。
精简的时候,我判断"不得提供医疗建议"跟我们产品无关(我们是做企业协作工具的),删了。
测试环境跑了两天没事。上线第三天,有用户问"我最近头疼,你们这个软件能不能帮我记录症状",模型真的开始给建议了,还说"建议服用布洛芬"。
虽然最后没出事,但这是个严重问题。
Prompt 精简的边界在哪?
我的判断标准现在是:涉及"不能做什么"的指令,一律不删。 不管看起来多无关。
因为模型不知道你的业务边界在哪,它只知道 prompt 里写了什么。你觉得无关,是因为你懂业务;模型不懂。
现在我们的精简规则是:
- 删:重复礼貌用语、空洞修饰词、冗余 few-shot
- 保留:所有否定式指令(不得、禁止、避免)、所有格式要求、所有拒答逻辑
- 加强:把保留的否定式指令移到 prompt 最前面
顺带说下精简的量:系统提示词从平均 420 token 压到 130 token,这一项单独贡献了约 12% 的成本下降。改一次永久生效,性价比最高。
坑四:批处理,不是所有任务都能批
Batch API 价格减半,看着很香。我们一开始把能批的都批了,包括一些"看起来是离线"的任务。
结果出问题了:有个功能是用户上传文档后生成摘要,我们判断这是异步任务,走了批处理。但用户上传完就在页面上等着,24 小时内出结果等于不可用。
判断能不能批,标准不是"是不是异步",是"结果晚 24 小时出来,业务能不能接受"。
我们的实际分类:
| 任务 | 能否批处理 | 原因 |
|---|---|---|
| 夜间文档摘要 | 能 | 用户第二天才看 |
| 商品批量打标签 | 能 | 后台任务,无用户等待 |
| 历史对话质检 | 能 | 周报用,不急 |
| 用户上传文档摘要 | 不能 | 用户在页面上等 |
| 客服实时回复 | 不能 | 秒级响应 |
| 合同条款提取 | 不能 | 用户等结果 |
真正能批的只占 30% 左右,但省下来的钱是实打实的。
还有个更划算的做法:把多次调用合成一次。
我们有个合同字段提取的需求,20 个字段,之前调 20 次。改成一次让它输出 JSON:
调用次数降到 1/20,准确率反而更高------因为模型能看到全局上下文,不会出现字段之间矛盾的情况。
坑五:流式输出,省的不是 token
流式输出本身不省 token,一分钱都不省。
但它解决了一个隐藏成本:用户等不及刷新页面导致的重复请求。
我们之前非流式调用,P95 延迟 8.2 秒。日志里能看到大量这样的记录:同一个 user_id,同一个 prompt,间隔 3-5 秒连续发两次、三次。
用户等 3 秒没反应,以为卡了,刷新页面,前端重新发请求。
改成流式之后,首 token 时间 0.8 秒,用户能立刻看到内容在往外蹦,没人刷新了。重复请求从 8% 降到 0.3%。
关键在 finally 里 close。用户关掉页面,生成立刻停,不再继续烧 token。
顺带一提,流式还能配合 max_tokens 做提前截断。有些场景用户根本看不完长回答,设个上限既省钱又提升体验。
两个没做成的尝试
尝试一:把 temperature 调到 0 提升缓存命中率。
想法是让输出确定,同样输入必然同样输出。实际效果一般,因为我们本来 temperature 就设的 0.3,调到 0 之后回答变得很死板,用户投诉变多,又调回去了。而且命中率只从 34% 涨到 37%。
尝试二:自建小模型替代部分调用。
部署了个 7B 的模型处理意图分类,理论上能省更多。实际跑下来,准确率比 mini 低 6 个点,而且 GPU 成本加上运维人力,算总账并不划算。最后放弃了,还是用 API。
API 的规模效应不是自建能比的。除非你的调用量大到能摊薄 GPU 成本,否则别折腾。
完整代码
五件事的实现,整合成一个模块:
最后
五件事的贡献度:
| 动作 | 降幅 | 难度 |
|---|---|---|
| 缓存(精确+语义) | 35% | 中 |
| 模型分级 | 28% | 低 |
| Prompt 精简 | 12% | 低 |
| 批处理 | 8% | 中 |
| 流式输出 | 4% | 低 |
有重叠,实际总降幅 90.5%。
说个反直觉的:这五件事里,没有一件是"换个更便宜的模型"。
我见过太多团队,成本高了第一反应是换模型。换一圈下来,账单没降多少,效果还掉了。
真正的大头在架构层面。我们那次梳理,1,000 次调用里 190 多次是前端重复触发的,120 多次是缓存该拦住没拦住的,70 多次是参数错误。光这三类就占 38%。
先看清楚钱花在哪,再想怎么省。顺序反了,白忙。
所有代码都在生产环境跑过,参数按我们业务调的,你用的时候记得拿自己数据重新标定。