一个批量任务一个月多花了两百刀,账单比报错先炸:给每个任务记一笔 token 账

上个月对账,批量导出流水线一天的成本从三毛涨到快一块钱。没人收到告警,因为没挂任何跟成本有关的告警。它不 crash、不超时、不降质,就是每一刀都比昨天厚一点。这种"故障"比 crash 难发现多了:crash 会喊疼,成本漂移只会安安静静地按月结账。

专属插画
一个批量任务一个月多花了两百刀,账单比报错先炸:给每个任务记一笔 token 账

一个批量任务一个月多花了两百刀,账单比报错先炸:给每个任务记一笔 token 账

上个月对账,批量导出流水线一天的成本从三毛涨到快一块钱。没人收到告警,因为没挂任何跟成本有关的告警。它不 crash、不超时、不降质,就是每一刀都比昨天厚一点。这种"故障"比 crash 难发现多了:crash 会喊疼,成本漂移只会安安静静地按月结账。

先说清楚那两百刀是怎么漏的。三个原因,都不是什么高深问题:

第一,一个任务把整份上下文塞进了 prompt。原本八千 token 能讲清楚的指令,我们贴了四万 token 的相关文档进去。贴的时候没人觉得有什么,反正单次也就几块钱。

第二,429 限流后的重试不智能。每次重试都把完整的对话历史重发一遍,三次重试就是三倍开销,而失败的调用照样计费。

第三,某个任务类型加了"顺手再总结一遍"的额外调用,没人记住当初为什么加,也没人记得删。

三个问题单独看都"还好",叠起来单价就翻了三倍。

所以这次做的修复只有一件事:**给每个任务记一笔账,账不平就动手**。

一、调用层记账,任务层结账

我们在 router 的调用封装上挂了个钩子,每次模型调用都落一行:


ledger.insert({
    "task_id": task_id,          # 每次运行唯一
    "task_type": "batch_export", # 预算挂在这个粒度
    "model": model_name,         # 钉死版本号,不带 latest
    "prompt_tokens": usage.prompt_tokens,
    "completion_tokens": usage.completion_tokens,
    "cost_usd": cost,
    "ts": now(),
})

一行不到二十个字段的 sqlite,够用。关键是 task_id:调用归属到哪个任务,任务结束时就按 task_id 把几十行调用汇成一行"这个任务总共花了几块钱",再写一列 `status`——成功还是失败。

二、分母要看"成功单价",不是"调用单价"

这一步最容易做歪。按调用算成本,一个任务调了 10 次花了五块,看似便宜;但其中 8 次是重试,真正成功的只有 2 个产物,每个产物实际成本两块五,而不是五毛。

所以报表里我们只盯一个数:**cost_usd 除以成功产物数**。失败的调用不算"浪费",算成本的一部分——因为正是重试本身让产物变贵。

三、预算是闸门,不是报表

只记账不动手,账还是白记。我们给每个 task_type 设了预算上限,比如 batch_export 单次运行 0.5 美元。执行逻辑很简单:

- 任务运行中,每次调用后查一下累计成本,超 80% 打日志,超 100% **直接停掉任务**并标 `status=over_budget`。

- 超预算的任务不重试。重试的前提是失败有原因,预算超了说明原因在任务设计里,重试只会花第二笔钱。

- 每天早上一条汇总:昨天 over_budget 的任务列表,带 task_id 和最终成本。

停掉超预算的任务只对过来说有一点代价:偶尔会误杀一个只要再多跑一步就能成功的任务。但误杀一次的成本,远低于放任它跑飞的成本,而且误杀会留下完整 ledger,复盘很快。

四、每周排一次"最贵任务"

账记上两周后,我们把报表做成一行查询:


SELECT task_type,
       COUNT(*)            AS runs,
       SUM(cost_usd) / SUM(CASE WHEN status='success' THEN success_count ELSE 0 END) AS cost_per_success
FROM task_summary
GROUP BY task_type
ORDER BY cost_per_success DESC;

最贵的那一行就是本周要改的对象。改完下周的报表里它应该往下掉,不掉就说明改到了别的地方。

账单比报错先炸这件事没法避免,模型按量计费就是这样的。能避免的是:等账单来了才第一次看见这个数字。账本的意义不在省钱本身,在于让"哪里在烧钱"从月底的一次惊吓,变成每天早上一眼能看见的一行数字。

留言区

欢迎分享你的想法!

发表留言

0/500

加载留言中…