AI 用量事件计量账本:幂等去重、迟到修正与可审计出账
网关重试、流式分段和供应商回调会制造重复或迟到用量。本文设计不可变 usage event、唯一去重键、冲突隔离、账期水位和三方对账流程。
本文要点
- 主题聚焦:AI、大模型、独立开发
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
AI 用量不是一个请求对应一条整齐记录。一次 Agent run 可能调用三个模型、命中缓存、重试工具并流式结算;网关写库成功但响应丢失后会重投,队列也可能重复交付。若消费端每收到一条消息就给客户加量,重试会变成重复收费;若为了避免重复只按 request_id 保留一条,又会丢掉同一请求内多个合法分段。
AI 成本归因 回答成本属于哪个用户和功能,LLM 限流与配额 在执行前保护容量。本篇只处理可出账证据:实际发生了多少计费单位、如何证明同一事实只入账一次,以及迟到修正如何不改写历史。
先定义最小且稳定的计量事件
producer 在能够确认实际用量的边界生成事件。模型调用记录供应商 usage、缓存类别和成功状态;自部署推理记录经过验证的 token、GPU 时间或产品定义单位;按结果收费则只由确定性的业务完成事件产生。估算值可以用于预留,但不能悄悄冒充最终账单。
{
"specversion": "1.0",
"id": "evt_01J...",
"type": "ai.usage.v2",
"source": "llm-gateway/prod",
"time": "2026-07-18T09:10:11Z",
"subject": "account/ac_42",
"data": {
"meter": "model.output_token",
"quantity": "842",
"unit": "token",
"operationId": "op_7f",
"segment": 3,
"providerRequestId": "req_vendor_9",
"occurredAt": "2026-07-18T09:10:09Z",
"schemaVersion": 2
}
}
数量使用整数最小单位或固定精度十进制字符串,不用浮点。事件同时有发生时间和接收时间,保留租户、计量器、操作、分段与 producer 版本。原始 Prompt、用户文本和无限标签不进入账本;产品套餐和价格也不复制成事实,计量与定价在后续按版本连接。
幂等键代表业务事实而不是一次投递
producer 重试必须复用同一个 event ID。分段事件使用稳定的 (operationId, meter, segment),供应商事件可绑定受信的 provider request ID 与 usage kind。数据库对 (source, id) 和业务幂等键建立唯一约束;同键同 payload hash 返回原接收结果,同键不同 payload 进入 CONFLICT 隔离,绝不能以后到者覆盖。
INSERT INTO usage_events(id, source, idempotency_key, payload_hash, payload)
VALUES ($id, $source, $key, $hash, $json)
ON CONFLICT (source, idempotency_key) DO NOTHING;
payload_hash 基于 schema 版本定义的 canonical bytes,不受 JSON 字段顺序影响。插入行数为零时读取已有事件:hash 相同返回原 receipt;不同则原子写冲突记录并告警,不能只靠 DO NOTHING 静默吞掉。
插入、更新 ingest cursor 和写入 outbox 必须处于同一事务。消息系统提供的是至少一次交付,账本通过唯一约束得到一次计费效果;不要宣称网络端到端“恰好一次”。事件驱动的投递和重放边界可参考 事件驱动架构。
账本只追加,错误通过反向事件修正
已 POSTED 的 quantity、account 和 occurredAt 不做原地 UPDATE。发现 tokenizer 版本错误或供应商最终 usage 不同,创建负向冲销,再追加正确事件。修正命令使用稳定 ID,并对 (originalEventId, correctionRevision) 建唯一约束;事务锁定修正链并校验上一 revision,两个操作者不能同时把同一版本冲销两次。后续再修正时针对当前有效版本追加下一 revision,而不是重复冲销最初事件。
修正事件保留 correctionOf、发现时间、理由和操作者,同时区分继承自原事件的 servicePeriod 与实际调整的 postingPeriod。开放账期可回到原草稿;已关闭账期只能关联 credit note、下一期 adjustment 或人工案件,不能把旧 occurredAt 再当成一条普通新用量发送。删除或覆盖原记录会让发票、审计和用户导出无法重建。
状态至少区分 RECEIVED、VALIDATED、POSTED、CONFLICT 和 REJECTED。Schema 不支持、单位未知、账户缺失或数量越界都进隔离队列,不自动按零处理。修复 parser 后从原始 payload 重放,仍使用原事件 ID,避免修复动作再次计量。
迟到事件需要明确账期水位
每个 meter 定义允许迟到窗口、账期截止和关闭规则。窗口内按 occurredAt 进入当前草稿;发票已关闭后到达的事件不得静默改写已出账单,而是按合同生成下一期 adjustment、credit note 或人工复核。用户界面要区分“当前估算”“已结算”和“迟到调整”,不能让数字无说明地回跳。
输出到 Stripe 或其他计费系统时使用账本 event ID 作为稳定外部标识,但不能假设供应商永久去重。delivery 记录 provider 的幂等作用域、保留期限、attempt、请求 hash、对象 ID 和响应;超时先标为 UNKNOWN,通过查询或账单对账确认结果。只在保证期内安全重放未确认 outbox,超过期限则冻结并人工处理,绝不盲目再发。配额预留与实际账本关联 operation ID,但 AI 平台租户配额 的准入拒绝不等于实际消费。
用故障注入和三方对账验收
测试同一事件投递一百次、同键不同 payload、分段乱序、producer 写库后崩溃、并发修正、跨账期迟到,以及 exporter 超时跨过幂等期限。断言账本净数量、外部计量和用户明细完全一致,冲突永不自动出账。按账户与 meter 比较网关原始 usage、内部 POSTED 净额和计费供应商接受净额,差异超过绝对/相对门槛即冻结该 meter 出账。
发布先影子写账本但不推发票,对一个完整账期重放并与旧汇总比较,再按 meter 切换。回滚时停止 exporter、恢复旧 consumer 版本和 cursor,但保留新账本事件;错误事件用冲销修正,不能删表或重置唯一键。可靠计量不是让消息永不重复,而是让任何重复、迟到和修正都得到同一份可重建的净结果。
延伸阅读
Related