大模型 API 账单越来越高?用单次成功任务成本反推多花在哪一步
账单涨得快于调用量,原因通常是下面四种之一,或几种叠加:输入变长,输出变长,一个任务里的请求变多,或者模型选得偏大。只看调用次数分不出是哪一种。更有用的指标是 单次成功任务成本:统计期内全部请求的费用(含失败和重试),除以验收通过的任务数。这个数字变了,就能指出先改哪一环。
一、为什么用成功任务做分母
按调用次数算单价,会掩盖两件事。一是失败和重试的费用被摊平在成功请求里。二是一个任务可能调用好几次模型,调用次数多少不能说明完成了多少工作。把验收通过的任务作分母,失败会直接拉高指标。
单次成功任务成本 = 统计期内全部请求费用(含失败、重试) ÷ 验收通过的任务数
二、先看五个信号
| 信号 | 包含什么 | 偏高时先查 |
|---|---|---|
| 每次请求的输入 Token | 历史轮次、检索片段、系统指令 | 历史是否每轮重发,Top-K 是否过大,系统指令是否重复 |
| 每次请求的输出 Token | 回答正文和推理内容 | 是否有长度约束,推理类参数是否一直开着 |
| 每个任务的调用次数 | Agent 循环、重试、校验 | 同一工具、同一参数是否反复出现 |
| 按模型拆分的费用占比 | 每个模型承担了多少费用 | 分类、标签这类简单任务是否用了大模型 |
| 缓存读取占全部输入的比例 | 前缀是否稳定 | 前缀里是否有时间戳或随机 ID,变化的内容是否放在了前面 |
三、用请求日志算指标
日志每行记录一次请求,用 JSON Lines 格式保存。字段先归一化,才能跨模型相加:
input_tokens:未缓存输入,不含缓存读取和缓存写入。OpenAI 的prompt_tokens包含缓存命中部分,写入前要减去cached_tokens。cache_read_tokens、cache_write_tokens:没有就填 0,不要留空。task_passed:任务是否通过验收。同一任务的每一行填相同的值。
下面的脚本读取这样的日志,输出指标。
import json
import sys
# 示例单价:元 / 百万 Token,发布前替换为所选型号官方价目表的数字
PRICES = {
"model-large": {"input": 10.0, "cache_read": 1.0, "cache_write": 12.5, "output": 40.0},
"model-small": {"input": 1.0, "cache_read": 0.1, "cache_write": 1.25, "output": 4.0},
}
def record_cost(rec):
# 字段已归一:input_tokens 是未缓存输入,不含缓存读取和缓存写入
p = PRICES[rec["model"]]
return (rec["input_tokens"] * p["input"]
+ rec["cache_read_tokens"] * p["cache_read"]
+ rec["cache_write_tokens"] * p["cache_write"]
+ rec["output_tokens"] * p["output"]) / 1e6
def summarize(path):
passed = {} # 任务 ID -> 验收是否通过,同一任务的每条记录填相同值
calls = missing = 0
cost_all = input_all = cache_all = 0.0
with open(path, encoding="utf-8") as f:
for line in f:
rec = json.loads(line)
calls += 1
passed[rec["task_id"]] = rec["task_passed"]
if rec["input_tokens"] is None:
missing += 1 # 用量缺失时不按 0 计,单独统计
continue
cost_all += record_cost(rec)
input_all += rec["input_tokens"] + rec["cache_read_tokens"] + rec["cache_write_tokens"]
cache_all += rec["cache_read_tokens"]
ok = sum(passed.values())
print(f"任务数 {len(passed)},验收通过 {ok},调用次数 {calls},平均每任务 {calls / len(passed):.2f} 次")
print(f"总费用(含失败与重试):{cost_all:.4f}")
if input_all:
print(f"缓存命中率(缓存读取 / 全部输入):{cache_all / input_all:.1%}")
if ok:
print(f"单次成功任务成本:{cost_all / ok:.4f}")
if missing:
print(f"警告:{missing} 条记录缺少用量,未计入费用,上面的数字偏低,需对账")
if __name__ == "__main__":
summarize(sys.argv[1])
示例日志 usage.jsonl 的数据为虚构,只用于验证脚本:
{"task_id":"t1","model":"model-large","input_tokens":8000,"cache_read_tokens":6000,"cache_write_tokens":0,"output_tokens":500,"task_passed":true}
{"task_id":"t1","model":"model-small","input_tokens":1200,"cache_read_tokens":0,"cache_write_tokens":0,"output_tokens":80,"task_passed":true}
{"task_id":"t2","model":"model-large","input_tokens":9000,"cache_read_tokens":0,"cache_write_tokens":0,"output_tokens":700,"task_passed":false}
{"task_id":"t2","model":"model-large","input_tokens":null,"cache_read_tokens":0,"cache_write_tokens":0,"output_tokens":null,"task_passed":false}
运行命令是 python cps.py usage.jsonl。Windows 控制台出现中文乱码时,先设置 PYTHONIOENCODING=utf-8。输出如下:
任务数 2,验收通过 1,调用次数 4,平均每任务 2.00 次
总费用(含失败与重试):0.2255
缓存命中率(缓存读取 / 全部输入):24.8%
单次成功任务成本:0.2255
警告:1 条记录缺少用量,未计入费用,上面的数字偏低,需对账
这个脚本的限制:
- 单价写死在
PRICES里,官方调价后要同步更新,并注明核对日期。 - 只统计已记录的请求。平台没有返回的请求,要和账单逐条对账。
- 阶梯价、时段价、批量折扣没有实现,需要时自行扩展。
四、按改动成本从低到高处理
- 换模型,不动流程。 分类、标签这类任务先试小模型。在同一批样本上比较准确率和完整任务费用,再小范围切换。
- 精简输入。 删掉重复的系统指令。历史保留关键事实和最近几轮,早期内容改成摘要,但摘要本身也计费。RAG 先去重,再缩小 Top-K,并测试召回质量。时间范围、产品版本、排除项这类限制条件不要删。
- 调整缓存前缀。 稳定内容(指令、文档、工具定义)放在前面,变化的问题放在后面。缓存命中按各家规则计价:OpenAI 的缓存写入规则随型号变化,Anthropic 区分缓存写入和缓存读取。命中不等于免费,低频复用的前缀,写入成本可能高于节省的部分。
- 给流程设边界。 Agent 设最大轮次、超时和预算。同一工具、同一参数重复执行时中止并记录原因。能用代码校验的格式(JSON 字段、日期)交给代码,不再让模型复看。重试集中在一处,并设上限。
- 约束输出,但别卡死。 Anthropic Messages 用
max_tokens,OpenAI Responses 用max_output_tokens。OpenAI Chat Completions 的max_tokens已弃用,应核对max_completion_tokens的支持情况。上限过紧会截断答案并触发重试,推理内容也占生成额度。 - 比较供应商时算完整任务价格。 输入、输出、缓存读写和存储费一起算,不只比最低输入单价。对批量、非实时任务,看厂商是否提供异步批量接口。
每次只改一项,改前改后用同一统计期比较。同时改多项,就无法判断哪一项起了作用。
五、平台侧的护栏与对账
灵眸 AI(渠道入口)的 Token 设置里可以配置总额度、每分钟请求数和模型白名单。给 Agent 用的 Token 设上限,可以给失控的循环设一个开销边界;分类类任务的 Token 只允许调用小模型,能防止配置被改错后误用大模型。超出上限后的具体表现,以控制台说明为准。
对账可以使用用量导出接口 GET /api/v1/usage。它返回逐条明细,包括 request_id、各类 Token、原价 total_cost 和实扣 actual_cost。使用时注意以下四点:
- 导出接口用账号登录换取 JWT 鉴权,不接受
sk-开头的 API Key。登录密码放在环境变量里,不要写进脚本。 - 导出数据里没有任务 ID,也没有验收结果。调用时把响应里的请求 ID 和任务 ID 一起记下,导出后按
request_id关联。 - 文档没有写明
input_tokens是否包含cache_read_tokens。用一条已知缓存命中的请求核对后,再套用第三章的归一化规则。 - 平台侧元数据保留 90 天。跨季度对账前要定期导出存档。
核对平台扣费时,直接汇总 actual_cost。它是美元金额,已乘渠道倍率,即 total_cost × rate_multiplier。第三章的脚本按单价重算,适合比较模型和功能,不能替代账单。
六、方案边界
- 换小模型、压缩上下文、调整缓存,都不能保证节省。省下的费用如果换来质量下降,单次成功任务成本可能反而上升。
- 自动重试只能处理部分临时错误。重试和网关都不能消除所有 429,也不能替代上游额度规划。
- 聚合平台的计费口径要逐项核对:币种、渠道倍率、失败请求是否计费。把这些写进验收清单,不要只比较单价。
- 缺失的用量不能按 0 计。按 0 计时,总费用和单次成功任务成本都会偏低,容易误判优化已经见效。
七、常见问题
缓存命中率的分母怎么定?
分母取全部输入 Token,包括未缓存输入、缓存读取和缓存写入,不包括输出。只用未缓存输入做分母,比率会偏高。
失败的请求要计入成本吗?
要。失败和重试的请求如果已经在服务端完成,通常会计费。统计时,失败请求的费用计入分子,分母只数验收通过的任务。具体计费规则以厂商文档为准,无法确认时按可能计费处理。
平台导出的用量和自己的日志对不上怎么办?
按顺序核对三项。第一,导出时是否传了 timezone,否则日期边界按 UTC 切分,和本地日志会差一段时间。第二,两边的请求 ID 能否对上。第三,Token 字段是否按同一口径归一化,尤其是输入是否包含缓存读取。
八、检查清单
- 每次请求都写一条日志,包括失败和重试
- 输入、缓存读取、缓存写入、输出分开记录
- 日志里有请求 ID 和业务任务 ID,导出后能关联
- 按任务统计验收通过数,计算单次成功任务成本
- Agent 设最大轮次和预算,分类类任务限定模型
- 导出明细时传入时区,并定期存档
- 每次优化只改一项,用同一统计期比较
账单涨得快,先别急着换模型。先算清每个成功任务花了多少钱,再判断该改哪一步。你们的单任务成本主要花在长上下文、Agent 循环还是失败重试上?欢迎在评论区说说。
参考资料
以下资料的核对日期待发布前补写。发布前需逐个打开当日页面确认。
- 灵眸 AI 官方文档:导出 Usage 使用明细、如何使用 Token、合规与备案(元数据留存)。核对日期待补。
- OpenAI API 定价:https://platform.openai.com/docs/pricing
- OpenAI Prompt Caching:https://platform.openai.com/docs/guides/prompt-caching
- OpenAI Responses 参数(max_output_tokens):https://platform.openai.com/docs/api-reference/responses/create
- OpenAI Chat Completions 参数(max_completion_tokens):https://platform.openai.com/docs/api-reference/chat/create
- Anthropic API 定价:https://docs.anthropic.com/en/docs/about-claude/pricing
- Anthropic Prompt Caching:https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
- Anthropic Messages 参数(max_tokens):https://docs.anthropic.com/en/api/messages
- Google Gemini API 定价:https://ai.google.dev/gemini-api/docs/pricing
- Google Gemini 上下文缓存:https://ai.google.dev/gemini-api/docs/caching
更多推荐



所有评论(0)