费用涨得快于调用量时,先算单次成功任务成本,再看它的三个组成部分哪一项变了。
- 指标:统计期内全部请求费用(含失败和重试)除以验收通过的任务数。
- 拆分:指标等于每任务调用次数、单次请求平均费用、任务通过率三个乘数的组合。变化能定位到其中一项。
- 方法:每次请求写一行日志,用一个 Python 脚本汇总,再用平台用量导出对账。脚本在模拟数据上的输出与手算一致,本文没有用真实账单验证。
一、单次成功任务成本怎么算?
分母只数验收通过的任务,失败和重试的费用全部留在分子里,因此失败会直接拉高这个指标。
单次成功任务成本 = 统计期内全部请求费用(含失败、重试) ÷ 验收通过的任务数
用调用次数做分母会掩盖两件事:
- 失败和重试的费用被摊平在成功请求的单价里。
- 一个任务可能调用模型好几次,调用次数说明不了完成了多少工作。
二、费用变化来自三个乘数中的哪一个?
指标可以拆成三个乘数,每个乘数对应不同的排查方向。这个拆分由定义直接推出,与模型和价格无关。
单次成功任务成本 = 每任务调用次数 × 单次请求平均费用 ÷ 任务通过率
| 乘数 | 含义 | 偏高时先查 |
|---|---|---|
| 每任务调用次数 | 一个任务发出的请求数,含工具循环和重试 | Agent 循环是否失控,同一工具、同一参数是否反复出现 |
| 单次请求平均费用 | 输入、缓存、输出按单价折算后的平均值 | 输入是否变长,Top-K 是否过大,输出有没有上限,模型是否偏大 |
| 任务通过率 | 验收通过的任务占全部任务的比例 | 失败是否集中在某一类任务,校验规则是否变严 |
通过率下降时,即使每次请求的费用没有变,指标也会上升。按调用次数统计看不到这一点。用第七节的模拟数据代入:每任务调用次数 2.00,通过率 0.5,单次请求平均费用约 0.0564,乘积约为 0.2255,与脚本输出一致(手算,未实测)。
三、每次请求要记录哪些字段?
日志每行对应一次请求,包括失败和重试。字段要先归一化,才能跨模型相加。每个字段对应一个乘数:
| 日志字段 | 对应乘数 |
|---|---|
| input_tokens、cache_read_tokens、cache_write_tokens、output_tokens | 单次请求平均费用 |
| model | 单次请求平均费用(按模型拆分) |
| task_id(同一任务的行数) | 每任务调用次数 |
| task_passed | 任务通过率 |
input_tokens 只记未缓存输入,不含缓存读取和缓存写入。返回字段的命名因接口而异。本文参考的阿里云百炼上下文缓存文档示例中,缓存命中数写在 prompt_tokens_details.cached_tokens,缓存创建数写在 cache_creation_input_tokens。各家是否把缓存部分计入 prompt_tokens,口径不同,写入日志前要用一条请求核对。没有缓存的字段填 0,不留空。task_passed 在任务验收后写入,同一任务的每一行值相同。
对账时,平台侧的用量要按 request_id 和本地日志对齐。本文示例使用 灵眸 AI 的用量导出接口,选用它的原因是导出数据逐条包含 request_id、各类 Token 数和实扣费用 actual_cost,可以直接和本地日志核对。第七节说明了对账步骤。
四、为什么用日志文件加脚本,不建数据库?
日志文件加离线脚本就能回答"哪一步变贵",而且实现最少。下面是几种备选方案的取舍。
| 方案 | 结论 | 原因 |
|---|---|---|
| 只看调用次数和单价 | 放弃 | 失败重试的费用被摊平,多次调用的任务无法比较 |
| 只看账单月总额 | 放弃 | 没有任务维度,无法判断哪一步变贵 |
| 用平台导出做日常统计 | 只用于对账 | 导出数据没有任务 ID 和验收结果 |
| 写入数据库并接入监控告警 | 暂不做 | 统计是离线的,日志量小时文件更容易检查 |
| JSON Lines 加脚本 | 采用 | 每行独立,写入中断只影响一行;同一文件可以反复复算 |
日志采用 JSON Lines 格式。规范要求 UTF-8 编码,每行是一个合法的 JSON 值,适合逐行追加。脚本只依赖 Python 标准库中的 json 模块。文中的方案没有使用阿里云 ECS、日志服务或云监控。
五、怎么运行脚本算出单次成功任务成本?
脚本算出的是按单价估算的数字,不替代账单。它做三件事:
- 读取日志。
- 按任务汇总,统计通过数和调用次数。
- 按单价重算每条请求的费用。
验收结束后,同一任务的所有行要写入相同的 task_passed。回填方式可以是重写这些行,也可以单独记录验收结果,汇总时按 task_id 关联。脚本读取的是已填好的文件。
运行前先把 PRICES 换成所选型号官方价目表上的单价,并注明核对日期。价目表里没有的模型名会抛出 KeyError。
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},
}
USAGE_FIELDS = ("input_tokens", "cache_read_tokens", "cache_write_tokens", "output_tokens")
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 has_usage(rec):
# 任一用量字段缺失都不计费,避免按 0 计
return all(rec[k] is not None for k in USAGE_FIELDS)
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 not has_usage(rec):
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}
Windows PowerShell 中先设置编码,否则控制台可能出现中文乱码。
$env:PYTHONIOENCODING = "utf-8"
python cps.py usage.jsonl
六、费用偏高时先改哪一项?
先按第二节判断哪个乘数偏高,再只改对应的一项。下面的改动按成本从低到高排列。每次只改一项,并用同一统计期比较改前改后。
- 先看失败集中在哪。(任务通过率)按任务类型统计失败率。失败集中在某一类任务时,先修这类任务的输入或校验规则。增加重试解决不了这类问题,还会抬高分子。
- 给流程设边界。(每任务调用次数)Agent 设最大轮次、超时和预算。同一工具、同一参数重复执行时中止,并记录原因。能用代码校验的格式交给代码,重试集中在一处并设上限。
- 换模型,不动流程。(单次请求平均费用)分类、标签这类任务先试小模型。在同一批样本上比较准确率和完整任务费用,再小范围切换。
- 精简输入。(单次请求平均费用)删除重复的系统指令。历史保留关键事实和最近几轮,早期内容改成摘要,摘要本身也计费。RAG 先去重,再缩小 Top-K 并测试召回质量。时间范围、产品版本、排除项这类限制条件不要删。
- 调整缓存前缀。(单次请求平均费用)稳定内容(指令、文档、工具定义)放在前面,变化的问题放在后面。阿里云百炼的上下文缓存文档写明,显式缓存的创建按输入单价的 125% 计费,命中按 10% 计费,有效期 5 分钟;隐式缓存命中按 20% 计费,命中率不确定;缓存块的内容最少为 1024 Token。所以只有前缀在 5 分钟内被多次命中,缓存才会省钱。低频复用的前缀,写入成本可能高于节省的部分。
- 约束输出,但别卡死。(单次请求平均费用)各家接口对输出上限的参数名不同。Anthropic 的 Messages API 用
max_tokens,OpenAI 的 Responses API 用max_output_tokens。上限过紧会截断答案并触发重试,推理内容也占生成额度。 - 比较供应商时算完整任务价格。(单次请求平均费用)输入、输出、缓存读写和存储费一起算,不能只比最低输入单价。
【建议补上 EEAT|专业:写出你们遇到的例外,例如哪些分类任务换小模型后准确率下降,以及判断依据。】
七、脚本结果可信吗?能和平台账单对上吗?
脚本输出与手算一致,验证的是计算逻辑,不是账单。
验证环境为 Windows,Python 3.14.3。样本是 2 个虚构任务、4 次调用,其中 1 行用量缺失。手算如下:大模型两次调用为 (8000 × 10 + 6000 × 1 + 500 × 40 + 9000 × 10 + 700 × 40) ÷ 10⁶ = 0.224,小模型一次调用为 (1200 × 1 + 80 × 4) ÷ 10⁶ = 0.00152,合计 0.22552。脚本输出如下:
任务数 2,验收通过 1,调用次数 4,平均每任务 2.00 次
总费用(含失败与重试):0.2255
缓存命中率(缓存读取 / 全部输入):24.8%
单次成功任务成本:0.2255
警告:1 条记录缺少用量,未计入费用,上面的数字偏低,需对账
模拟任务的费用几乎全部来自大模型。两个大模型调用占总费用的 99.3%,小模型只占 0.7%(手算,未实测)。缓存命中率 24.8% 也全部来自 t1 的大模型调用。因此第六节的第三项(换小模型)应先评估。
判断用量缺失时要检查全部用量字段。只检查输入字段,遇到 output_tokens 为空的行,脚本会在计算费用时抛出 TypeError。另用一份数据测试,修正后的判断把这一行计入缺失警告:
任务数 2,验收通过 1,调用次数 2,平均每任务 1.00 次
总费用(含失败与重试):0.1000
缓存命中率(缓存读取 / 全部输入):0.0%
单次成功任务成本:0.1000
警告:1 条记录缺少用量,未计入费用,上面的数字偏低,需对账
【建议补上 EEAT|经验:放一次真实改动的前后对比,写明统计期、任务数和模型;没有真实数据就不写。】
对账步骤按接口说明编写,没有用真实账户执行。灵眸 AI 的用量导出接口需要账号鉴权,调用分两步:先用账号和密码请求登录接口换取 JWT,再用 JWT 按 start_date、end_date 和 timezone 分页调用用量接口。使用时要注意以下几点。
- 接口只接受账号 JWT,不接受
sk-开头的 API Key。接口说明把这一点列为有意的安全设计。 - 账号密码等同于完整的账户访问权限,应放在环境变量里,不写进脚本。
- 导出字段
actual_cost是按美元计的实际扣费,等于total_cost乘以rate_multiplier。第五节的脚本按单价重算,不能替代这个值。 - 导出字段里没有任务 ID 和验收结果,调用时要把
request_id与task_id一起记下。 - 接口说明没有写明
input_tokens是否包含cache_read_tokens。用一条已知缓存命中的请求核对后,再套用归一化规则。 - 日期参数要同时传
timezone,否则日期边界按 UTC 切分,会和本地日志差出一段时间。 - 调用元数据保留 90 天,跨季度对账前要定期导出存档。
八、这套方法有哪些限制?
这套方法能指出费用变化来自哪一个乘数,不能保证省钱。
- 省下的费用如果换来质量下降,任务通过率会变化,指标可能反而上升。
- 脚本只统计已记录的请求。平台返回但日志里没有的请求,要和账单逐条对账。
- 价格写死在
PRICES中,官方调价后要同步更新,并注明核对日期。阶梯价、时段价和批量折扣没有实现。 - 缺失的用量不能按 0 计。按 0 计时,总费用和指标都会偏低,容易误判优化已经见效。
- 自动重试只能处理部分临时错误,不能消除所有 429,也不能替代上游额度规划。
- 失败请求是否计费,以厂商文档为准。无法确认时按可能计费处理,计入分子。
- 缓存命中率的分母是全部输入,即未缓存输入、缓存读取和缓存写入之和,不含输出。只用未缓存输入做分母,比率会偏高。
九、上线前要检查哪些项?
上线前对照下面七项检查。每项都做到,指标才能用来判断优化是否有效。
- [ ] 每次请求都写一条日志,包括失败和重试
- [ ] 输入、缓存读取、缓存写入、输出分开记录
- [ ] 日志里有请求 ID 和任务 ID,导出后能关联
- [ ] 按任务统计验收通过数,计算单次成功任务成本
- [ ] Agent 设最大轮次和预算,分类类任务限定模型
- [ ] 导出明细时传入时区,并定期存档
- [ ] 每次优化只改一项,用同一统计期比较
费用上涨时,先算三个乘数里哪一个变了,再决定改哪一步。你们的单任务成本主要花在哪一个乘数上?
常见问题
没有明确验收标准的任务,怎么算成功?
没有验收标准,这个指标就算不出来。先定义可判定的结果,例如输出通过 JSON 校验、测试用例全部通过,或人工抽检通过,并把判定方式记在日志里。
为什么不直接用平台导出的 actual_cost 做统计?
actual_cost 是每次请求的实际扣费,但导出数据没有任务 ID 和验收结果,算不出单次成功任务成本。它适合核对总额,任务统计要靠本地日志按 task_id 汇总。
平台导出的用量和本地日志对不上,先查什么?
先查三项:导出时是否传了 timezone,两边的 request_id 能否对上,缓存部分是否包含在 input_tokens 里。三项都确认后仍有差异,再逐条比对。
价格调整后,历史数据怎么算?
每个统计期保存当时生效的价目表,分别运行脚本。本文的 PRICES 只有一套价格,直接替换后重算,会用新价格覆盖旧的结论。
参考资料
【发布前核对:Anthropic 与 OpenAI 的页面需在可访问的网络下逐个打开确认,本次核对环境无法打开。】
- 阿里云百炼:上下文缓存(2026-11-04 核对)
- JSON Lines 格式规范(2026-11-04 核对)
- Python 标准库:json 模块
- Anthropic:Messages API
- OpenAI:Responses API 创建接口