那封邮件出现在凌晨3点14分,CFO抄送了整个AI平台组。标题是“大模型推理成本月度分析与降本要求”,附件里一个数字让我彻底清醒:过去30天,我们调用GPT-4o和Claude 4的API费用是13.7万美元,而整个Kubernetes集群加GPU节点的成本才5.8万。更打脸的是,这些钱花得稀里糊涂——没人能说清哪个业务团队、哪个应用场景消耗了多少token,账上只有一个服务账号的总计费。
作为一名被半夜叫醒过太多次的DevOps工程师,我第一反应不是想优化代码,而是必须把每一分钱都钉在具体的消费主体上。没有可观测性的成本管理就是闭着眼睛花钱。在接下来两周内,我搭建了一整套面向大模型推理的FinOps体系:token粒度的成本标记、按用户/场景归因的实时看板、基于排队论的动态流量路由,以及Spot实例的优雅抢跑方案。最终当月底账单一出,推理成本从13.7万降到8.2万,整整砍掉40%,而P99延迟只增加了不到8%。这篇文章就是那次半夜惊魂后的完整复盘,每一条规则都是生产环境拿血换来的。
30秒速览
- - 大模型推理成本黑洞源于缺乏token维度监控和标签归因,必须为每个API调用打上application/team/scenario标签
- - 使用Prometheus recording rules将token消耗实时换算为美元成本,并在Grafana中构建角色化看板,设置3倍突发告警
- - 动态路由将实时请求与批处理请求分离,批处理流向Spot实例集群,通过优先级队列和优雅中断处理,节省70%的计算成本
- - 优化后整体推理账单降低40%,batch任务延迟增加但仍在SLA内,配合异常检测规则防止泄漏和错误切换,实现可持续的FinOps管理
推理账单黑洞:为什么月度费用是K8s集群的2.4倍?
我们的大模型推理服务对外暴露统一的API网关,背后串接了多个模型供应商:OpenAI GPT-4o、Anthropic Claude 4、以及自建的vLLM集群跑Llama 3.1 70B。上线初期为了快速迭代,所有调用都通过同一个服务账号发起,监控只看了总调用次数和平均延迟。第一个月还好,第二个月成本直接翻倍。
我翻遍历时日志,发现三类典型的成本泄漏点:第一,某个内部测试脚本每天凌晨批量跑3万条评估用例,用的全是GPT-4o而不自知;第二,营销团队接入了十几个小程序,每次生成海报文案都调最强模型,实际上Claude 3.5 Haiku就能满足需求;第三,流量没有优先级,大量后台汇总任务和实时用户请求挤在同一批按需实例上,导致我们必须扩容更多昂贵的预留节点来保证延迟。(延伸阅读:仿真零摔倒,实测8km摔一次——我把人形机器人送上亦庄半马赛道后的运动控制复盘)
更致命的是,没有针对成本的反常检测。有一个API Key被泄露到前端客户端,某个用户疯狂调用vision接口解析高清图片,连续5天每天产生$800费用,而我们毫不知情。直到CFO的邮件发来,我们才被动排查。那次我深刻体会到:大模型推理的计量单位不是请求次数,是token,而且必须打上多维度的消费标签,否则账单就是黑洞。
为什么只看QPS和延迟是成本管理的灾难
传统的API监控——QPS、错误率、延迟——对大模型推理基本无效。一个请求可以消耗100个token,也可以消耗10000个token,价格相差几十倍。我们曾有一个报表生成接口,用户上传Excel文件后调用GPT-4o进行长篇分析,单次请求耗掉2.8万个token,成本$0.42。在QPS曲线上,它和极短的聊天补全请求没有区别。运维团队若只看请求量去估算成本,误差至少300%。
我强迫自己接受一个原则:所有大模型推理的监控,第一维度必须是token消耗量,第二维度是费用(美元或人民币),然后再看延迟和错误。Prometheus原生时间序列无法直接理解“钱”,必须通过recording rules和标签设计,把每次调用的token数和模型单价实时换算为成本指标。否则运维看板永远是一笔糊涂账,财务追责时也拿不出任何有效证据。(延伸阅读:放弃8张A100后,我把LLaMA 3 8B预训练成本从$0.12砍到$0.032/百万token——Trainium2迁移调优全记录)
成本数据采样的第一个坑:prometheus-client的直方图粒度
我们最初试图使用OpenTelemetry的metrics SDK直接暴露token消耗histogram,但在高并发下,prometheus-client的默认直方图桶边界根本无法匹配大模型token的分布——聊天补全通常在10-500 token,但文档分析经常超过4096个token,全落在最大桶里,导致分位数计算失灵。我不得不自定义桶边界为[50, 200, 500, 1000, 2000, 4000, 8000, 16000],并针对vision模型再设独立指标。这是第一个凌晨踩的坑:当你的histogram不能覆盖实际数据范围时,Grafana看板上的99分位费用数据是假的,直接导致我们低估了高消耗场景的占比。
成本归因模型:让每一个API Key都为它的token买单
我的目标很明确:必须能够回答“昨天‘售后服务’这个场景花了多少钱,是哪个团队调用的,用的是哪个模型”。这需要构建一个可扩展的标签体系,并将其注入到每一次大模型调用的metrics里。我们设计了三个层级的标签:application(对应业务应用,如 chatbot、report-gen)、team(成本归属部门,如 marketing、support、engineering)、scenario(细分场景,如 after-sales、ad-copy、code-review)。另外还加上了一个动态标签priority,用来标记请求是实时(real-time)还是批处理(batch),为后面的动态路由做准备。
实现上,所有客户端在调用统一网关时必须携带自定义Header x-consumer-*,网关负责将这些标签注入到请求上下文中。任何不携带标签的请求直接拒绝并返回403,强制上游业务方显式声明身份。我们内部有一个服务注册表,网关据此验证application和team的合法性。下面是网关配置的一段Lua代码,运行在APISIX插件中,负责提取标签并写入ngx.ctx供后续的metrics收集器使用:
local core = require("apisix.core")
local plugin_name = "cost-attribution"
local _M = {
version = 0.1,
priority = 1900,
name = plugin_name,
schema = {
type = "object",
properties = {
required_labels = {
type = "array",
items = { type = "string" },
default = {"application", "team", "scenario", "priority"}
}
}
}
}
function _M.check_schema(conf)
return core.schema.check(_M.schema, conf)
end
function _M.access(conf, ctx)
local headers = core.request.headers(ctx)
local labels = {}
for _, name in ipairs(conf.required_labels) do
local val = headers["x-consumer-" .. name]
if not val or val == "" then
return 403, {message = "Missing required label: " .. name}
end
if name == "priority" and val ~= "realtime" and val ~= "batch" then
return 400, {message = "priority must be realtime or batch"}
end
labels[name] = val
end
ctx.var.cost_labels = core.json.encode(labels)
end
return _M
从token计数到美元计价的Prometheus recording rules
有了标签还不够,我需要让Prometheus直接记录费用。我们在每个模型调用的出口点(无论是调用OpenAI API还是自建vLLM)都埋了一个summary指标:llm_token_usage_total,包含prompt_tokens和completion_tokens两个字段。然后通过recording rule将其和模型单价相乘,生成连续的cost计数器:
groups:
- name: llm_cost
interval: 30s
rules:
- record: llm:cost_per_second:rate1m
expr: |
(
label_replace(
rate(llm_token_usage_total{type="prompt"}[1m]), "unit", "prompt", "", ""
) * on(model) group_left model_cost_prompt_tokens
+
label_replace(
rate(llm_token_usage_total{type="completion"}[1m]), "unit", "completion", "", ""
) * on(model) group_left model_cost_completion_tokens
)
labels:
unit: dollars
- record: llm:cost_by_application:rate1m
expr: sum by (application, team) (llm:cost_per_second:rate1m)
- record: llm:cost_by_team_scenario:rate1m
expr: sum by (team, scenario) (llm:cost_per_second:rate1m)
这里模型单价维护在另一个静态metric model_cost_prompt_tokens里,由配置管理定时更新。一旦模型定价变化,我们只需要修改这个metric的target就能让所有cost系列指标自动更新,而不用改代码。凌晨那个血泪教训告诉我,任何硬编码的单价在下一个模型版本发布时都会成为定时炸弹。
归因模型上线后,CFO终于能在每周成本复盘时明确看到:营销团队的广告文案场景消耗了35%的预算,但带来的客户转化仅有5%;研发团队的代码审查助手用了25%的预算,而它的ROI是节省了8个人天。这直接促使业务部门主动降级模型选择,把很多场景从GPT-4o迁移到Claude 3.5 Haiku甚至开源的Llama 3.1 8B上。(延伸阅读:在Jetson Orin上跑LangChain安全护栏:512MB内存预算下,我把注入拦截延迟压到1.8ms)
把Grafana看板变成CFO和工程师的共同语言
可观测性的核心是让不同角色的人在同一块屏幕前达成共识。我设计了两套看板:一套面向工程团队,侧重token消耗趋势、模型分布、P95延迟与cost的相关性;另一套面向管理层(CFO、产品VP),展示按成本中心的费用排行、预算消耗比例、异常飙升告警。两者底层数据完全一致,只是聚合维度和可视化方式不同。
Grafana看板的关键配置是使用Prometheus的llm:cost_by_application:rate1m和llm:cost_by_team_scenario:rate1m作为数据源,配合变量筛选器让用户自由钻取。我特意把时间范围默认设为“过去7天”,因为月度看板对发现突发异常毫无帮助。某个周五下午,营销部门一个实习生误上线了批量生成功能,每小时费用从$120飙升到$2100。多亏看板左侧那个“Cost per second by application”时序图,我们在42分钟内就发现了异常,并直接通过网关屏蔽了那个application的调用。如果等到月度报表,这2.1万美元的额外支出就已成定局。
异常检测规则:不是等账单爆了才报警
基于Grafana自带的Alerting,我设定了三条硬规则:
- 任何application的10分钟cost均值超过其历史同时间段(上周同日同时段)均值的3倍,立即发送Slack告警和PagerDuty通知;
- 任何API Key(对应application+team)的每日总token数超过前一天200%,触发阻断并人工审核;
- 单个请求消耗token数超过9500时,记录到单独的“超大请求”日志,每周自动生成报告。
第2条规则曾经救了我们一次:一个被误提交到公开GitHub仓库的测试Key,在周六凌晨被外人扫描到并用来挖加密货币(通过大模型生成挖矿代码脚本?其实只是调用API进行文本生成,但频率极高)。因为日token数直接从前一天的3.2万暴涨到37万,告警在5分钟内激活,我直接吊销了该Key,避免了$6000的损失。
告警一定要关联runbook。我把每条告警的描述里直接附上操作文档链接,比如“如何吊销API Key”、“如何临时将高消耗应用降级到低费模型”。凌晨被叫醒时,我最讨厌的就是还要用迷糊的大脑搜索应急流程。(延伸阅读:我们在Optimus Gen-3上刷出了99.2%搬运精度,但仿真到实机的坑烧掉了三台关节电机)
动态路由:让低优先级流量滚到Spot实例上排队,高优请求永不降级
成本看板搭好后,我们获得了清晰的消费数据:60%以上的token消耗来自批量任务——每天生成产品描述、定期汇总用户反馈、凌晨预计算推荐理由。这些任务不要求低延迟,甚至可以容忍几分钟的排队。但之前它们和实时用户请求跑在同一批GPU实例上,导致我们必须按峰值容量预留昂贵的按需节点。
我设计了一套两级路由架构:API网关根据请求头x-consumer-priority将流量分为realtime和batch两类。realtime请求直接打入稳定集群(按需实例+预留实例的混合),batch请求先进入一个优先级队列,由控制器调度到Spot实例集群上。当Spot实例因价格波动被回收时,队列中的任务会自动重试或者降级到更便宜的模型。
这个设计参考了排队论中的非抢占式优先级队列(Non-preemptive Priority Queue)。realtime请求拥有绝对优先权,只要稳定集群有空闲槽位就立刻执行;batch请求则根据集群中Spot实例的可用容量和当前队列深度动态控制并发数。我们用Kubernetes的HPA(水平自动伸缩器)分别管理两个Deployment:llm-stable和llm-spot。后者使用带有spot-instance: "true"标签的节点,并通过nodeSelector和tolerations确保只调度到Spot节点上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: llm-batch-worker
spec:
replicas: 2
selector:
matchLabels:
app: llm-batch
template:
metadata:
labels:
app: llm-batch
spec:
nodeSelector:
cloud.google.com/gke-preemptible: "true"
tolerations:
- key: "cloud.google.com/gke-preemptible"
operator: "Equal"
value: "true"
effect: "NoSchedule"
containers:
- name: worker
image: myrepo/llm-router:latest
env:
- name: MODEL_NAME
value: "claude-3.5-haiku"
- name: QUEUE_ENDPOINT
value: "redis-queue:6379"
- name: PRIORITY
value: "batch"
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10 && kill -SIGTERM 1"]
为什么K8s的preStop钩子要提前10秒
Spot实例被回收时,GCP会提前30秒发送ACPI信号,K8s kubelet会给Pod发送SIGTERM。如果不设置preStop钩子或处理时间太短,Pod会直接被杀掉,正在处理的batch请求就会丢失。我的经验是必须给preStop至少10秒的延迟,并在应用内部捕获SIGTERM后,停止接收新任务、等待当前任务完成或checkpoint、最后正常退出。上面yaml里的sleep 10看起来粗暴,但确实给了业务代码足够时间做graceful shutdown。我们没有用复杂的sidecar,而是让业务进程自己响应信号并写入完成状态到Redis,然后由调度器检测超时重试。这套机制在线上运行了半年,未完成请求的丢失率控制在0.03%以下,完全可以接受。
Spot实例实战:被回收前2分钟,我把流量切走了
动态路由加上Spot实例后,成本立竿见影:batch集群的节点小时单价从$2.48(n1-standard-8 + V100)降到了$0.74,降幅70%。但因为Spot回收不确定,我们必须处理好任务中断。GCP的preemptible实例平均存活时间不到24小时,最极端的一次,一批8个节点在早高峰时段同时被回收,队列瞬间积压了4700个任务。
那次事件之后,我做了两件事:第一,在路由器中加入了Spot可用性检测探针,每隔10秒检查节点剩余寿命(通过metadat server获取)。如果剩余时间小于3分钟,立即将该节点标记为“draining”,不再分配新任务,并把待处理任务重新入队。第二,为batch任务设置了最大重试次数(3次)和指数退避,重试后仍失败的自动降级到更便宜的模型(如从Claude 3.5 Haiku降级到Gemini 1.5 Flash),成本几乎忽略不计。这些策略通过一个轻量级Python调度器实现,不到500行代码,却让batch集群的可用性从95%提升到99.7%。(延伸阅读:用Ollama + LangChain构建本地隐私聊天机器人,30行代码搞定!)
Spot实例还有一个隐形成本:API调用失败的retry也会消耗token(部分模型在输入验证阶段就计费)。我们在路由器层面开启了“dry-run”检查——在真正发送请求前,先向Spot节点发一个极短的health check请求(只消耗1个token),确保服务可达。若失败,立即换到另一个节点。这虽然增加了1个token的额外花销,但避免了整个prompt被吞掉的几十倍浪费。算下来每个月节省了约$1100。
优化前后成本对比与异常检测的最后一道防线
整个FinOps体系上线一个月后,财务和工程团队坐在同一张桌前看数据。下面是我们真实的成本对比(已脱敏):
| 指标 | 优化前(月) | 优化后(月) | 变化 |
|---|---|---|---|
| 总推理成本 | $137,200 | $82,300 | -40% |
| batch类任务成本 | $82,000 | $24,500 | -70%(迁移到Spot) |
| realtime任务成本 | $55,200 | $57,800 | +4.7%(因精细化路由开销) |
| API Key泄露导致异常损失 | $14,300 | $0 | -100%(自动阻断) |
| batch任务P99延迟 | 1.2s | 3.8s(可接受) | +216% |
| realtime任务P99延迟 | 820ms | 750ms | -8.5%(队列隔离后) |
realtime延迟下降是因为batch流量不再占用稳定集群的slot,队列压力得以释放。batch延迟增加三倍在预期内,因为任务要排队等待Spot节点可用;我们和业务方确认过,3.8秒完全在SLA内(原定5秒)。没有哪个老板嫌钱省得太多,只有对延迟要求苛刻的场景我们才保留了全部按需实例。
异常检测规则不能只盯着费用飙升
成本优化完成后,我新增了一类反向告警:当某个application的cost连续3天为零或低于历史均值的10%,同样触发通知。因为这可能意味着业务方偷偷停掉了调用却不告知,或者网关故障丢掉了流量。曾经一个客服机器人应用因为前端升级不再发请求,但负责人以为在正常使用,直到月底review KPI才发现响应率暴跌。现在我们能在几小时内察觉,避免了业务影响。
另一个必须添加的规则是“模型切换追踪”。任何application昨天主要用GPT-4o,今天突然90%流量切到Claude 4,系统自动标记并通知。这防止了开发人员私自更换更高价模型(逆向降本)或者第三方依赖偷偷升级模型版本导致费用突变。在大模型API迭代如此迅速的今天,模型版本和定价的变动比传统IaaS频繁得多,可观测性必须覆盖到这个维度,否则所谓的成本管理就是刻舟求剑。
我还在Grafana上做了一个预算燃尽图(Burndown),把月度预算除以当月剩余天数,得到每日允许的最大成本线,再与实际消耗叠加。每天凌晨0点,脚本自动把前一天的数据推送到CFO的邮件。这种“倒计时”式的可视化比静态报表直观十倍:当曲线突破红线,根本不需要写报告,所有人打开看板就知道必须立刻降本。最终,我们成功把推理支出控制在了月预算$9万以内,并且第一次有了数据支撑去和业务方谈判——谁用得多,谁就承担对应部分的成本。
那天半夜CFO的邮件已经过去8个月,现在大模型推理的成本体系成了我们平台稳定性之外的又一条生命线。每个新接入的应用必须携带标签、必须有成本归属,否则根本进不了生产环境。可观测性不仅仅是监控CPU和内存,在大模型时代,它必须监控“钱”。而且我坚信,没有token粒度的成本看板,任何FinOps都是纸上谈兵。