多模型统一接入后,如何追清真实成本
统一 API 密钥并不能让不同模型供应商的成本数据自动可比,需围绕请求生命周期记录不可变事件。通过区分请求与尝试,保留重试、流式响应和工具调用等上下文,才能解释成本与延迟差异。
AI 深度解读
同一把 API Key 能调用 OpenAI、Claude 和 Gemini,并不代表它们的用量与成本已经可以直接比较。原文给出的办法是:沿着一次请求的完整生命周期记录事件,再分别计算实际执行的工作、最终交付的结果,以及结果是否可用。
标题谈价格,正文重点却是「账该怎么记」。作者没有提供价格表或实测省钱比例,而是在解释:重试、流式输出和自动路由,为什么会让一个看似准确的成本数字失去解释力。接入多个模型的团队,值得先把这层基础理清。
一次请求成功,背后可能已经执行了三次
用户发出一次请求,模型第一次超时,第二次失败,第三次才返回结果。产品侧看到一次成功,运行时却执行了三次。两个统计都成立,但回答的是不同问题。
作者把执行过程分成三层:
- Request 对应用户可见的一次操作。
- Attempt 对应这次操作的一次执行,重试会产生新的 Attempt。
- Span 对应执行中的具体步骤,例如排队、模型调用、工具执行和响应组装。
这套层级的价值,是保留「谁导致了谁」的关系。模型调用可能返回多个流式片段,工具执行也可能单独重试。如果全部压成一行请求记录,汇总容易,排查就困难了。
因此,作者要求区分两种工作量:Delivered work 是产生用户可见结果的那次执行所做的工作;Performed work 则包含所有执行,包括失败尝试和取消的流。
原文举例:第一次调用输入为 1800 单位,输出为 0,状态是 timeout;第二次输入仍是 1800,输出为 290,状态是 ok。交付工作只对应第二次,实际工作要包含两次输入。
用户只拿到一个结果,不代表系统只做了一次工作。
不过,执行过多少工作,与供应商最终收多少钱仍有距离。失败调用是否计费、用量是否完整,原文没有逐家说明,不能直接把上述工作量当成账单。
统一字段之前,先统一字段的含义
作者提供了一份紧凑的事件示例,用来连接请求身份、执行步骤、耗时和用量:
{
"request_id": "req_01J7Q4M2",
"attempt_id": "att_01J7Q4N8",
"span_id": "spn_01J7Q4P1",
"operation": "model.generate",
"model": "runtime-model-id",
"status": "ok",
"started_at": "2026-10-26T08:10:12.104Z",
"ended_at": "2026-10-26T08:10:13.991Z",
"input_units": 1840,
"output_units": 276,
"retry_count": 0,
"streamed": true
}
要留意的是 input_units 和 output_units:作者刻意使用「单位」,没有承诺所有接口都按同一种 token 口径计量。一个来源提供估算值,另一个来源提供供应商确认值,即便字段名相同,也不能直接混算。
这也是原文最实用的提醒:统一 JSON 结构,只完成了接入的一部分。还需要说明单位定义、数据来源、是否完整,并为口径保留版本信息。否则换了模型或运行时实现,数字的意义可能已经变了。
事件还需要记录每次 Attempt 实际选择的路由及原因。一次请求走 OpenAI,另一次因策略检查转到 Gemini,如果只保存最初请求的模型配置,路由变化就可能被误判成模型退化。
存储方式也影响可追查性。作者建议追加不可变事件,或保留修订历史,避免不断覆盖一个「当前成本」字段。先保存可测量的用量,以后再结合费率、分摊规则计算金额,历史结果才能重算。
指标标签则要克制。原始提示词、生成文本、请求 ID 和任意用户元数据,不适合全部放进指标标签,否则会产生大量不同的时间序列。这类高基数字段更适合进入受访问权限和保留策略约束的 trace 或日志。
流式响应结束了,用量也未必完整
流式输出最容易出现的错误,是把每个 chunk 都算成一次完整响应。作者要求将所有片段挂在同一个 Span 下,等流进入终态,再结束这次调用。
下面的代码展示了如何把片段观测与调用结束分开:
const stream = startModelSpan({ requestId, attemptId });
try {
for await (const chunk of modelStream) {
stream.observeChunk({ bytes: chunk.length });
yield chunk;
}
stream.end({ status: "completed", usage: getFinalUsage() });
} catch (error) {
stream.end({ status: "failed", errorType: classify(error) });
throw error;
}
关键在 stream.end(...):最终用量在流结束时记录,逐片段观测没有被当成完整用量。chunk.length 也不能据此换算成 token 或费用,具体单位仍取决于接口。
这只是生命周期示意。代码展示了 completed 和 failed,没有展开 cancelled 的处理;实际接入时,需要补齐运行时支持的取消路径。如果供应商没有返回累计用量,就应标明数据只有一部分,不能把估算包装成确认总量。
记录执行还不够,作者还建议把结果质量接进来。比如必要工具是否调用完成、响应是否通过校验、人工是否接受结果,再用稳定的关联键连接运行事件。低用量响应如果无法使用,单看成本下降没有多少价值。
采样也会改变结论。如果失败和慢请求更容易被丢弃,平均耗时、重试率都会显得过于乐观。作者主张优先保留失败、取消和高延迟事件,并在看板旁展示时间窗口、统计人群、采样方式、重试范围、估算状态和运行时版本。
我的判断
我认同原文先建立执行关系,再汇总成本的顺序。对带有重试、工具调用和流式响应的 AI 应用,单个请求级成本字段确实很难解释异常。保留原始事件,也比只存最终金额更方便追查和重新计算。
但它是一套设计建议,不能当成经过对照实验验证的优化方案。原文没有提供生产数据、存储开销或故障定位时间的比较。正文提到日常复盘表格和示意百分比,所给材料里却没有实际表格,不能据此得出任何模型更便宜的结论。
它也没有彻底回答标题里的价格比较:没有供应商费率、模型版本价格,也没有完整的计费映射。更准确的定位是,为后续核算准备可追查的数据。
统一路由是否划算,同样有前提。它能集中接入和策略管理,也会增加协调成本与故障边界。流量简单、模型固定的小团队,可以先直接集成,并保留必要的请求与执行记录。供应商用量适合辅助账单核对,应用侧 trace 适合解释工作为何发生,两者需要关联起来。
能拿来做什么
- 在 Node AI 接口中,为用户操作生成稳定的
request_id,每次重试单独生成attempt_id,记录父级尝试、重试原因和实际路由,让一次成功背后的额外执行可以被找到。 - 给流式接口补齐 completed、cancelled、failed 终态,并区分确认用量、估算用量和部分用量,避免用户中断后留下无法解释的记录。
- 为 AI 编程或工具调用流程增加结果记录,例如必要工具是否成功、生成结果是否通过校验,再按模型和操作比较结果通过率、耗时及实际工作量。
- 调整日常看板,同时展示交付工作与实际工作,并注明统计窗口、重试和采样口径;两者差距变大时,结合超时设置、重试策略和输入增长排查。