账单异常通常只有八种原因,先用六个查询依次排除,确定是多请求还是单价高;若仍在烧钱,先加天花板再查根因,别同时改多个变量。
账单一夜翻倍:一份排查手册
一次 LLM 账单翻倍,大约有八种可能原因,找到最快路径的方法不是读代码,而是对你的请求日志跑六次查询,按顺序来,每一次都排除一个分支。第一个查询 30 秒出结果,决定你是遇到了更多请求,还是每次请求变贵了。
如果在调查过程中费用还在上涨,先给它设置一个天花板。提供商侧的支出限制、自己网关的限流降级、或者关掉最新的功能开关,都可以为你争取时间,而且都不需要知道原因。表停了,诊断更便宜。
忍住同时改好几样东西让它停下来的冲动。如果你同时关掉了三个嫌疑方,费用下降了,你解决了事故但什么都没学到,问题还会回来。
这份手册假设每行记录对应一次请求。如果你没有这样的记录结构,那把它建好是第一个要做的修复,这是一天的活儿,但遇到第一次这种事就回本了。
CREATE TABLE llm_requests (
ts timestamptz NOT NULL,
request_id text,
model text NOT NULL, -- from the RESPONSE, the resolved one
route text, -- which feature or endpoint
caller text, -- service, job, or user id
tenant text, -- customer, if multi-tenant
prompt_tokens int NOT NULL,
cached_tokens int, -- prompt tokens served from cache
completion_tokens int NOT NULL,
reasoning_tokens int,
cost_usd numeric(12,6), -- computed at write time
status int,
attempt int, -- 1 for the first try, 2+ for retries
duration_ms int
);
有两列起了决定性作用。attempt 使得重试风暴可见,而不是看起来像自然流量。而 model 取自响应而非请求,这使得别名迁移可见——请求里写的是一个,但提供商实际跑了另一个。
是量的问题还是单价的问题? 后续所有判断都依赖这个答案,一个查询搞定:
SELECT date_trunc('day', ts) AS day,
count(*) AS requests,
sum(cost_usd) AS spend,
sum(cost_usd) / count(*) AS cost_per_request
FROM llm_requests
WHERE ts > now() - interval '14 days'
GROUP BY 1 ORDER BY 1;
请求量翻倍但单次成本不变:流量问题或循环问题,跳到第 4 步。请求量不变但单次成本翻倍:每次调用本身的某些东西变了,跳到第 2 步。两者都变了:通常有两个原因,应该分开来追。
是哪个模型? 模型组合的转移是单价上涨最常见的原因,而且经常在没有发版的情况下发生:
SELECT date_trunc('day', ts) AS day, model,
count(*), sum(cost_usd) AS spend
FROM llm_requests
WHERE ts > now() - interval '14 days'
GROUP BY 1, 2
ORDER BY 1, 4 DESC;
在账单开始变动那天首次出现的模型名就是答案。一个熟悉的模型但份额突然上涨也是——某个兜底路由开始触发了,或者一个别名解析到了别的地方。
是哪种 token? input、output、reasoning 和 cached tokens 价格不同,比例变化能告诉你哪里变了:
SELECT date_trunc('day', ts) AS day,
avg(prompt_tokens) AS in_avg,
avg(completion_tokens) AS out_avg,
avg(reasoning_tokens) AS reasoning_avg,
avg(cached_tokens::numeric / nullif(prompt_tokens,0)) AS cache_hit_rate
FROM llm_requests
WHERE ts > now() - interval '14 days'
GROUP BY 1 ORDER BY 1;
Input 涨了:prompt 变大了——更多检索出来的文档、更长的历史、更多的工具。Output 涨了:响应变冗长或 max_tokens 调高了。Reasoning 涨了:某个 effort 设置变了。Cache hit rate 崩了:某些东西导致前缀变化了,这种问题在其他所有视角里都看不见。
是哪个调用方?
SELECT route, caller,
count(*) AS n,
sum(cost_usd) AS spend,
sum(cost_usd) FILTER (WHERE ts > now() - interval '2 days')
- sum(cost_usd) FILTER (WHERE ts BETWEEN now() - interval '4 days'
AND now() - interval '2 days')
AS delta
FROM llm_requests
WHERE ts > now() - interval '4 days'
GROUP BY 1, 2 ORDER BY delta DESC NULLS LAST LIMIT 20;
按变化量而不是按总额排序是关键。花钱最多的人通常每周都是花钱最多的;但移动了的那个人才是你要找的。
重试和循环。
SELECT date_trunc('hour', ts) AS hour,
count(*) FILTER (WHERE attempt = 1) AS first_tries,
count(*) FILTER (WHERE attempt > 1) AS retries,
sum(cost_usd) FILTER (WHERE attempt > 1) AS retry_spend
FROM llm_requests
WHERE ts > now() - interval '3 days'
GROUP BY 1 ORDER BY 1;
重试占比平时是 1% 现在变成 30% 就是重试风暴,底层触发原因通常是提供商的一个错误,你的代码把它当作可重试的但其实不应该。对于 Agent,还要统计每次会话的 step 数:一次会话跑了两百步是循环,不是用户。
昂贵的尾部。 费用几乎总是高度集中的,Top 1% 的请求常常就是全部:
SELECT request_id, route, caller, model,
prompt_tokens, completion_tokens, cost_usd
FROM llm_requests
WHERE ts > now() - interval '2 days'
ORDER BY cost_usd DESC
LIMIT 50;
读一下 Top 记录背后的实际请求。这一步往往直接结束调查,因为有问题的请求一眼就能认出来。
重试风暴。 上游抖动,重试没有上限也没有 jitter,每次重试都要为那个长 prompt 全额付费。
Agent 循环没有步数限制。 一次会话产生数百次调用,因为一个工具持续失败而模型持续重试。每会话成本是发现这个问题的指标;停止条件和 Agent 成本控制是预防手段。
Prompt 变大了。 检索 top-k 从 5 提高到 10,新增了一个工具,会话历史没有边界。每次请求乘以这个变化,就是涨幅。
Prompt Caching 停止命中。 时间戳、session ID 或打乱顺序的工具放在缓存前缀里,意味着每次请求都是缓存未命中。账单可以明显移动而其他什么都看不出——见 Prompt Caching 节省了什么以及缓存的上下文排序。
模型或 effort 变了。 别名迁移了,兜底触发了,或者有人为了质量调高了 reasoning effort 设置但没有算清楚成本。
数据回填或批处理任务。 有人重新处理了一个语料库。合理的、一次性的,值得确认一下然后停止继续排查。
Key 泄露。 比人们担心的少,但查一下:不熟悉的调用方、不寻常的时段、或者请求组合不匹配任何功能。如果有,立即轮换而不是继续调查。
在接受任何解释之前,先检查它在算术上是否可能。一个路由的最大支出受限于其请求数乘以最坏情况 token 数再乘以价格。
工作示例。假设每百万 input tokens 4 美元,每百万 output tokens 16 美元,
某路由每天处理 50,000 次请求:
每次请求上限 = (8,000 input × 4 / 1e6)
+ (2,000 output × 16 / 1e6)
= 0.032 + 0.032 = 0.064 USD
每天上限 = 50,000 × 0.064 = 3,200 USD
如果该路由的账单是 9,000 美元,那请求数不对,
token 数不对,或者存在你没有记录的请求。
差异本身就是发现。
那些价格只是算术占位符,不是报价。用你实际调用的模型的当前官方费率替代;方法论才是可以复用的,数字不是。
在提供商或网关侧设置硬性支出限制。 不是告警,是限制。告警依赖于有人在值班。
设置每会话和每租户成本上限。 每会话成本是这里最有用的派生指标,因为几乎所有失控行为都是一个会话表现得和其他不一样。
每日对单次请求成本做异常检查。 比率在总量之前移动,这争取到一天时间。
重试预算,不只是重试次数。 限制整个流程每分钟的总重试次数,这样糟糕的一分钟不会变成糟糕的一小时。
记录 attempt 和解析后的模型。 如果这次第 2 步或第 5 步无法回答,先修这个再结案。
这些控制手段在预算控制、成本归属和拒绝钱包(denial of wallet)中有更详细的展开。
第 2 步和第 4 步都依赖于在调用时刻记录每次请求的成本,而不是从月度账单反推。Multigrid 计算每次请求的成本并将其归属到某个 key 和 label,这使得"哪个调用方移动了"这个查询可以在当天回答,而不是等到月末。