账单涨得快于调用量,原因通常是下面四种之一,或几种叠加:输入变长,输出变长,一个任务里的请求变多,或者模型选得偏大。只看调用次数分不出是哪一种。更有用的指标是单次成功任务成本:统计期内全部请求的费用(含失败和重试),除以验收通过的任务数。这个数字变了,就能指出先改哪一环。
一、为什么用成功任务做分母
按调用次数算单价,会掩盖两件事。一是失败和重试的费用被摊平在成功请求里。二是一个任务可能调用好几次模型,调用次数多少不能说明完成了多少工作。把验收通过的任务作分母,失败会直接拉高指标。
单次成功任务成本 = 统计期内全部请求费用(含失败、重试) ÷ 验收通过的任务数
二、先看五个信号
| 信号 | 包含什么 | 偏高时先查 |
|---|---|---|
| 每次请求的输入 Token | 历史轮次、检索片段、系统指令 | 历史是否每轮重发,Top-K 是否过大,系统指令是否重复 |
| 每次请求的输出 Token | 回答正文和推理内容 | 是否有长度约束,推理类参数是否一直开着 |
| 每个任务的调用次数 | Agent 循环、重试、校验 | 同一工具、同一参数是否反复出现 |
| 按模型拆分的费用占比 | 每个模型承担了多少费用 | 分类、标签这类简单任务是否用了大模型 |
| 缓存读取占全部输入的比例 | 前缀是否稳定 | 前缀里是否有时间戳或随机 ID,变化的内容是否放在了前面 |
三、用请求日志算指标
日志每行记录一次请求,用 JSON Lines 格式保存。字段先归一化,才能跨模型相加:
input_tokens:未缓存输入,不含缓存读取和缓存写入。OpenAI 的prompt_tokens包含缓存命中部分,写入前要减去cached_tokens。cache_read_tokens、cache_write_tokens:没有就填 0,不要留空。task_passed:任务是否通过验收。同一任务的每一行填相同的值。
下面的脚本读取这样的日志,输出指标。
importjsonimportsys# 示例单价:元 / 百万 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},}defrecord_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"])/1e6defsummarize(path):passed={}# 任务 ID -> 验收是否通过,同一任务的每条记录填相同值calls=missing=0cost_all=input_all=cache_all=0.0withopen(path,encoding="utf-8")asf:forlineinf:rec=json.loads(line)calls+=1passed[rec["task_id"]]=rec["task_passed"]ifrec["input_tokens"]isNone:missing+=1# 用量缺失时不按 0 计,单独统计continuecost_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}")ifinput_all:print(f"缓存命中率(缓存读取 / 全部输入):{cache_all/input_all:.1%}")ifok:print(f"单次成功任务成本:{cost_all/ok:.4f}")ifmissing: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