标题里那场"隐性成本之战",我这个季度深有体会。账单比上季度翻了一倍,查询量只涨了30%,排查到最后问题根本不在算力扩容,而是模型选错、重试风暴、上下文膨胀这些看不见的地方。后来我们做了三件事:把OpenTelemetry(OTel)的企业级遥测体系搭起来、引入多模型路由、把成本治理变成闭环机制。这篇文章把全景实战过程拆开讲,适合正在做企业级AI平台、或者被LLM API账单折磨的架构师和平台工程师。
1. 那张超出预期的账单:隐性成本到底藏在哪里
1.1 从一次月度账单复盘说起
上季度末,财务把LLM API账单甩过来的时候,我一度以为是系统被人刷了。查了用量明细,请求量环比只涨了三成,费用却涨了97%。用最粗暴的除法去估,平均单次请求成本几乎翻倍。直觉告诉我,这不是单纯的业务增长,而是系统内部出现了某种"成本泄漏"。
我把线上日志、网关调用记录、应用侧埋点全部拉出来,按功能模块做了聚合。结论很打脸:成本增长最大的模块,是一个只做关键词提取的小功能,它居然调用的是旗舰大模型;而另一个核心对话功能,因为上游超时设置太短,每条用户消息平均触发2.3次模型调用。这些都不是什么高深的技术问题,就是典型的"隐性成本"堆积。
很多人一提到AI成本治理,第一反应是"跟模型厂商谈折扣"。但谈折扣是财务的事,工程能做的其实更多——把每一次模型调用变成可观测、可追踪、可路由的单元。
1.2 四类看不见的成本来源盘点
我复盘之后,把隐性成本归成四类,每一类在代码里都有对应病灶:
| 成本类型 | 具体表现 | 典型根因 |
|---|---|---|
| 重试风暴 | 一次业务请求触发多次模型调用 | 超时设置过短、退避策略缺失、网关层无限重试 |
| 上下文膨胀 | prompt里塞入大量历史消息和参考文档 | 对话历史无截断、知识库拼接不做摘要 |
| 模型误配 | 简单任务调用高配模型 | 全系统共用同一个模型实例、无路由策略 |
| 无效输出 | 模型生成了远超需求的内容 | max_tokens不设上限、输出未做结构化约束 |
这四类问题最麻烦的地方在于:它们在单次调用里都"看不出问题",每次可能就差几分钱。但放到每天百万次调用的规模下,就是每月几十万的成本差异。
比如重试风暴,我们当时的网关配置是"失败就重试3次",却没有区分"业务失败"和"限流失败"。某次模型服务短暂抖动,限流错误让所有请求走了重试链路,一瞬间把所有配额打满,然后配额打满又触发更多重试,形成了恶性循环。这种问题如果不靠遥测数据,光看总账单是永远定位不到的。
2. 多模型路由:把合适的任务交给合适的模型
2.1 路由不是省钱的妥协,是系统架构的必需品
多模型路由这个词,听起来像是在做"价格战",但落地之后我发现,它真正解决的是"任务特征和模型能力匹配"的问题。企业里的大模型调用场景千差万别:有的任务是抽取结构化信息,准确率要求高;有的是闲聊,延迟敏感但质量容忍度高;有的是内部测试环境,根本不需要旗舰模型的能力。
把这三种流量全部打到一个模型上,结果必然是:该高质量的没上去,不该花钱的乱花钱,高峰期还把关键链路的配额占光了。路由层的作用,就是在请求入口处做一次"分流",让不同价值的请求走不同的模型通道。
我们当时的接入方式是做一个统一模型网关,对外暴露OpenAI兼容接口,内部维护模型列表和路由策略。上游业务方不需要关心自己调的是哪个模型,只需要声明任务类型和质量要求。
2.2 路由场景建模:三个关键维度
做路由之前,必须先定义清楚任务画像。我习惯用三个维度来给请求打标:
- 复杂度:输入文本长度、是否需要多步推理、是否涉及工具调用。关键词提取、摘要、分类这类任务复杂度低,问答和代码生成复杂度高。
- 质量红线:业务方是否接受偶尔的错误。比如合同信息抽取,错误代价极高;而标题生成,错了无所谓。
- 延迟预算:用户能等多久。流式输出的对话场景可以接受3-5秒,同步接口的语义搜索则要求1秒内返回。
这三个维度组合起来,基本能覆盖80%的场景。剩下的20%属于边界情况,需要靠降级机制兜底。
2.3 一套可落地的路由实现思路
路由策略不需要一开始就上机器学习,规则是最高性价比的方案。我们的第一版路由完全基于规则:
def route_request(request): # 1. 外部标注:业务方显式指定的模型等级 if request.model_hint == "cheap": return Model_Z # 2. 复杂度规则:输入超长或者需要工具调用,走旗舰 if len(request.messages) > 20 or request.tool_calls: return Model_X # 3. 质量要求:带结构化输出约束的任务走均衡模型 if request.response_format: return Model_Y # 4. 默认兜底:低成本模型 return Model_Z这套规则上线后,我们的成本立刻下降了一大截,原因是大量内部工具类和测试类流量都被导到了轻量模型。但很快暴露出新问题:规则是静态的,模型能力会变,业务分布也会变。某个模型如果突然降价或质量下降,规则完全感知不到。
第二版我们引入了"动态路由":在规则之上加了一个小型的分类模型,用过去一段时间的请求特征和结果评分做输入,预测"哪个模型在这个任务上性价比最高"。这个分类模型本身部署在轻量模型上,单次路由决策成本极低,但带来的收益是持续的。
2.4 路由降级的兜底策略
路由层最怕的是"主模型不可用导致全站瘫痪"。我们的设计是:每个路由目标都配了一个降级链。
- Model_X(旗舰)不可用 → 降级到Model_Y
- Model_Y(均衡)不可用 → 降级到Model_Z
- Model_Z(轻量)不可用 → 降级到本地小参数模型
- 本地模型都挂了 → 返回缓存结果或明确的错误信息,绝不无脑重试
降级链的切换判断完全依赖遥测数据——连续错误率超过阈值、p95延迟超过预算、限流指标触顶。这些信号都来自我们在第三节讲的可观测系统。没有遥测,降级链就是盲切;有了遥测,降级链才能做到"有依据地切"。
3. 用OpenTelemetry给AI系统装上"仪表盘"
3.1 为什么选OTel,而不是自己埋点
很多团队一提到可观测性,第一反应是"自己在代码里打日志、算指标"。我在上一个项目里就是这么干的,结果半年后维护成本爆棚:每个服务埋点格式不统一,指标口径对不上,排查跨服务问题要同时开三个监控面板。
OpenTelemetry(OTel)的价值在于:它是标准化的数据采集框架,同一套API可以同时产生指标、日志、链路追踪三类数据,而且语义约定是社区共同维护的。这意味着,新服务接入的成本极低,工具链生态丰富,数据格式不用自己造轮子。
具体到AI场景,OTel在2024年后有了专门的gen_ai语义约定,定义了LLM调用的标准span属性、指标项和事件结构。用这套约定埋完后,任何支持OTel的后端(Prometheus、Jaeger、云厂商的监控服务)都能直接识别和展示AI调用数据,不需要额外定制。
3.2 gen_ai语义约定:标准化是关键
gen_ai语义约定是我见过最实用的规范。它把一次LLM调用抽象成一个span,核心属性包括:
gen_ai.operation.name:操作类型,比如chat、complete、embedgen_ai.request.model:请求的模型名gen_ai.request.max_tokens:请求的最大输出长度gen_ai.usage.prompt_tokens:输入token数gen_ai.usage.completion_tokens:输出token数gen_ai.response.finish_reasons:停止原因(比如长度限制、停止词)
这些属性覆盖了"调用的是谁、花多少钱、生成了多少内容、为什么停止"四个关键问题。我们甚至在业务侧签发了自定义属性:team.id(哪个团队)、feature.id(哪个功能)、route.target_model(路由最终选了哪个模型)。
有了这些自定义维度,成本归因就能精确到团队和功能,而不是只看到一笔总账单。
3.3 生产环境采集链路怎么搭
OTel的落地链路并不复杂,核心是三层:
应用进程 → OTel SDK → OTel Collector → 后端存储/展示第一层是应用进程内的SDK,负责创建span、采集指标,并通过OTLP协议上报。第二层是关键,OTel Collector我们部署了独立集群,做三件事:接收、处理(过滤、采样、打标签)、导出。第三层是后端,指标的存到Prometheus,链路追踪的存到Jaeger或云上Trace服务,日志走原有日志平台。
生产环境的采集一定要设采样策略。我们的方案是:错误span全量采样,普通成功span按10%采样,但所有span里的gen_ai.usage类属性都不会丢,因为成本统计依赖这些数字。刚开始图省事全量采样,结果Collector内存顶上来了,后来调整成上述策略才稳定下来。
3.4 把路由决策和成本归属打进遥测数据
遥测数据如果只是"能看",价值有限;真正把账算清楚,靠的是把路由决策和业务信息绑定到span上。
我们在模型网关里做了一个埋点约定:每次路由决策都生成一个独立的route.span,属性包括route.request_id、route.input_dimension(复杂度、质量线、延迟预算)、route.result_model、route.rule_name(命中了哪条路由规则)。
然后把route.span和实际LLM调用产生的gen_ai.span通过request_id关联起来。这样排查问题时就有一条完整的调用链:
业务请求 → 路由决策(为什么选这个模型) → 实际模型调用(花了多少token) → 响应返回(是否成功)
成本归因变成了一个简单的SQL查询:按team.id分组,汇总gen_ai.usage.prompt_tokens + completion_tokens,再乘以对应模型的单价。我们每天的账单核对,就是这么跑出来的。
4. 一次真实的事故排查:遥测数据如何把隐性成本抓出来
4.1 现象:某功能成本突增50%
就在路由和遥测上线跑了一个月之后,我们遇到了一次典型的隐性成本事故。某天告警通知:某个核心功能的日成本突然比前一日涨了50%,但业务访问量几乎没变。放在以前,这种问题基本只能靠猜——可能是模型涨价了?还是有人改了prompt?效率极低。
这次我们直接打开追踪系统,按feature.id筛出当天的调用链数据,发现问题集中在某个会话功能上。单次调用token数没有明显变化,但每个"用户动作"对应的模型调用次数从1次涨到了3-4次。
4.2 通过trace定位到模型路由异常
顺着调用链往下看,发现链路里多了一个之前没见过的操作:每次用户发消息,系统会先调用一次"话题分类",再调用一次"意图识别",最后才走正式的对话生成。这两个小任务本来应该由轻量模型以极低成本完成,但查看路由span发现,它们无一例外全部命中了旗舰模型。
为什么?因为路由规则里写的是"输入长度超过20条历史消息就走旗舰模型",而会话功能在发消息时会把整个历史会话全部带上。业务量增长后,用户的会话长度自然变长,于是大量本应走轻量模型的请求,被这条规则连带拖进了旗舰模型。
这个案例特别典型:路由规则本身没有"错",但它没有区分"历史消息参与生成"和"仅作为上下文背景"两类场景,导致成本被系统性放大。
4.3 通过metric验证并修复
定位到原因之后,我们用指标数据做了量化验证:把route.result_model按模型维度聚合,发现旗舰模型占比从15%飙到40%,而按token量算占比更高——因为长上下文的prompt token本身就是大头。
修复方案分两步。第一步,把路由规则里的"历史消息长度"触发条件,改成"仅当历史消息参与关键推理时才使用旗舰模型",普通会话的上下文压缩后再走轻量模型。第二步,给轻量模型加一个"摘要前置"的config,超长上下文先由轻量模型生成压缩摘要,再作为输入传给旗舰模型。
修复上线后,旗舰模型占比回落到18%,成本恢复到了预期范围。整个定位过程从告警到修复,用了不到3小时,这在没有遥测数据时是难以想象的。
4.4 复盘:这次排查的价值不止省了钱
事后复盘,这次事故暴露的问题有两个:一是路由规则的"触发条件"写得过于粗粒度,没有考虑任务真正的质量需求;二是缺少"成本配额"的自动告警机制,要不是人工发现,可能还要多烧几天钱。
我们随后做了一件事:每次路由规则变更,都必须同步更新对应的成本预测指标,并在指标异常时自动触发告警。这让"路由策略"和"成本观测"进入了一个互相验证的循环,而不是各自独立地运转。
5. 企业级成本治理的闭环机制
5.1 预算与配额:把成本变成工程指标
成本治理不能靠月底看账单,必须前置到"配额"层面。我们建立了三层配额体系:团队级月度预算、功能级日配额、单请求cost上限。
团队级月度预算由技术负责人分配,按周滚动检查;功能级日配额防止某个功能异常吞噬全部预算;单请求cost上限则是最硬的一条线——如果某次调用的预估成本超过阈值,网关直接拒绝,返回成本超限错误。
这套配额的价值在于:它把"省钱"变成了可执行的工程约束。业务方开发时就会关注自己的请求是否符合成本预期,而不是上线后才被账单教育。
5.2 缓存与语义相似度:省掉重复计算
路由和配额管的是"怎么调用",缓存管的是"能不能不调用"。我们在线上了两层缓存:
- 精确缓存:完全相同的prompt直接命中缓存,返回历史结果。适合知识库问答、内容审核这类重复率高的场景。
- 语义缓存:用embedding向量做相似度检索,语义相近的prompt命中缓存。适合"用户反复问同一个问题但表述不同"的客服类场景。
语义缓存需要额外的embedding调用和向量存储成本,所以我们只对它设了很保守的相似度阈值,并在命中后才做收益核算。从实际效果看,精确缓存大概省了15%的调用量,语义缓存额外省了5%左右,两者都不算惊人,但叠加起来,抵掉了相当大一部分业务增长带来的成本压力。
5.3 降级与熔断:保护账单也保护体验
成本治理经常被误解为"能省就省",但在企业级系统里,稳定性比省钱优先。我们的做法是:把熔断机制和降级链绑定,并让它们感知成本指标。
具体来说,当某个模型通道的可用性指标或成本指标达到阈值时,系统会自动触发降级链。比如旗舰模型通道的p99延迟超过预算,或者日成本达到配额的80%,部分非核心流量就会被自动切到均衡模型。这个机制的目的不是牺牲体验,而是防止"一次异常拖垮所有核心流量"。
顺带提一个教训:熔断阈值不能定得太死。我们曾经把"成本配额80%切换"写得太激进,结果业务正常增长也触发了切换,导致核心功能体验明显下降。后来改成"80%告警、95%才切换",给业务增长留出余量。
5.4 组织协同:成本责任到团队
最后但同样重要:成本治理不是平台团队单方面的事。我们做了一次组织上的调整——每个业务团队的成本消耗直接进入该团队的技术负责人OKR,并且每月同步一次"成本报告"和"优化建议清单"。
这种责任下沉的效果特别明显:以前业务方只管功能上线,现在他们会主动考虑"这个功能能不能用轻量模型"、"这里是不是可以不传历史消息"。平台团队的角色从"警察"变成了"服务方",提供工具和最佳实践,让各团队更容易做正确的选择。
6. 落地节奏与常见坑
6.1 分阶段推进:先观测,再路由,后治理
如果让我给一个刚起步的团队规划路径,我建议按三步走。
第一步,先把OTel可观测性搭起来,重点就是gen_ai语义约定和路由span,这一阶段不改变任何线上行为,只增加数据透明度。第二步,在观测数据的基础上做"伪路由"——只计算路由决策结果、不实际切换,用历史数据回放验证路由规则的效果。第三步,灰度切换路由和配额机制,逐步把成本约束真正落到线上。
我见过不少团队一上来就想搞多模型路由,结果没有埋点数据,路由规则全靠拍脑袋,上线之后成本没降反而引入了新的不稳定因素。先观测、再路由,这套顺序不能乱。
6.2 容易踩的坑:埋点遗漏、采样率不合理、规则太刚性
总结踩过的坑,三个最值得说。
一是埋点遗漏。最容易漏的是错误路径——比如限流、超时、模型返回异常这些场景,很多团队只在正常调用链路上打了span,结果异常发生时无从排查。我们的做法是:网关层统一拦截所有LLM调用进出,无论成败都生成span,保证"调用即埋点"。
二是采样率不合理。为了省存储把采样率压得太低,导致成本聚合数据失真。尤其在流量较大时,你只采样1%的话,小体量功能的数据基本就消失了,成本归因根本做不准。建议:成本维度的usage属性必须全量保留,can对此单独配置,别和普通trace共用一套采样率。
三是路由规则太刚性。规则直接硬编码在业务逻辑里,一旦调整需要发版重启。一定要把路由策略独立成配置,甚至可以做成动态配置下发,这样调优的成本极低。我们一开始图省事写在代码里,后来发现模型能力变化时,改一次规则要等一次发版,非常痛苦。
6.3 我的经验总结
回到文章开头那个"账单翻倍"的问题,最终我们通过这套组合拳,在下个季度把成本涨幅控制在了业务增速的一半以内。当然这不是说我们的方案做到了极致,事实上模型能力和价格在快速变化,可观测性体系也需要跟着演进。但有一点是确定的:没有可观测性,就没有成本治理;没有多模型路由,成本治理就永远只是"事后解释"而非"事前控制"。
最后分享一个小技巧:把"每次模型调用的成本"当成一个虚拟指标,在预览环境也打上埋点。虽然预览环境流量小,但它的数据集非常干净,适合验证路由规则和成本模型的准确性。等路由规则在预览环境验证通过后再上生产,可以省掉很多不必要的线上试错。这套方法论我们已经沉淀成内部工具,当然,起步阶段不需要那么完善的工程平台,先把手头的埋点做好,把账单查清楚,就已经领先大多数团队了。