AI 网关计费系统实现方法详解(Token 计量与实时扣费)

企业级 AI 网关的计费系统核心是四条:按 Token 计量、输入输出分开计价、原子扣减防超卖、流式请求也能完成计费闭环。我们在网关里把”读余额、判断、扣减”压成一条 SQL 原子更新,并发下不会出现余额被扣成负数;SSE 对话即使客户端中途断开,也坚持读完整上游尾部事件拿到 usage 再落账,避免”上游消耗了、平台没记账”的缺口。下面给出完整实现链路。

一、先定计费维度

记账前先想清楚按什么计量,选错后面全部推倒重来。

计费维度 适用场景 坑点
Token(输入/输出分开) 主流文本对话模型 输入输出单价常差 2~5 倍,混算严重失真
按请求次数 图像生成、embedding 长短 Prompt 同价,成本大户被平均
按时长 语音合成、实时流式 网络重试时长不该算进用户成本

我们的策略是:上游响应带 usage 字段就按 Token 计费,不带才退化为按次,两种模式在账本里用 billing_unit 字段区分。

二、计价公式与价格表

金额用整数最小单位(厘)存储,禁用浮点数,否则小额单价乘多维度再累加,对账时全是解释成本。

cost = input_tokens × input_price + output_tokens × output_price

价格表按模型维护,支持热更新,方便上游调价后即时同步:

CREATE TABLE model_price (
    id            BIGINT PRIMARY KEY AUTO_INCREMENT,
    model_name    VARCHAR(64) NOT NULL,
    input_price   BIGINT NOT NULL,   -- 每 1K Token,单位:厘
    output_price  BIGINT NOT NULL,   -- 每 1K Token,单位:厘
    updated_at    DATETIME NOT NULL
);

缓存 Token 折扣单独处理:上游返回 cached_tokens 时该部分单价按折扣计算,不能让缓存命中拉低整体成本导致毛利失真。

三、实时扣费的原子性

高并发下最常见的故障是”超卖”:两个请求同时读到余额足够,同时放行,最后余额被扣成负数。解决办法不是加锁(加锁拖慢网关吞吐),而是把”读余额、判断、扣减”合并成一条原子 SQL:

UPDATE account_balance
SET balance = balance - #{cost}
WHERE account_id = #{accountId}
  AND balance >= #{cost};

执行后影响行数为 0,说明余额不足,直接返回 402 并带上本次费用明细。这里必须用 balance >= cost 做条件判断,而不是先 SELECT 再 UPDATE,两条语句之间存在窗口期。

四、流式请求的计费闭环

SSE 场景计费有个反直觉点:客户端断开连接,不代表上游请求结束,也不代表 usage 已返回。如果客户端一断就停止计费,会出现”上游已经烧钱、平台没有落账”的缺口。

处理方式是断线后继续读上游尾部事件,拿到 usage 再落账:

  1. 请求进入网关,先按预估 Token 做余额预检与预留;
  2. 转发到上游模型,SSE 边收边转给客户端;
  3. 客户端断开时,网关不立即结算,继续消费上游流直到出现 usage 或明确终止标记;
  4. 拿到最终 usage 后重算费用,完成实扣;
  5. 预留金额与实际费用差额做”多退少补”或释放预留。

五、预扣与实扣

请求前必须做预算预检,否则用户并发发 20 个请求,每个都看到余额足够但都不预留,结果集体透支。预留不是扣款,是锁住一部分预算防止同一份余额被重复使用。请求结束后用最终 usage 实扣,多退少补。

// 预留:返回本次预留金额,Redis 或 DB 原子实现
long reserved = quotaService.reserve(accountId, model, estimatedTokens);
try {
    Usage usage = upstream.call(messages);   // SSE 流式调用
    long cost = billingService.settle(accountId, usage); // 实扣
    quotaService.release(accountId, reserved - cost);    // 多退少补
} catch (Exception e) {
    quotaService.release(accountId, reserved);           // 失败全额释放
}

六、账本设计

只记一个总金额字段不够,财务、客服、对账全都说不清。至少落四类记录:

  • usage_log:请求级账本,记录谁调的、用的哪个模型、总 Token 与总费用;
  • charge_line:维度级明细,输入、输出、缓存各用多少、单价多少、金额多少;
  • wallet_transaction:钱包流水,记录每次扣款与充值;
  • provider_evidence:上游账单原始数据,用于对账。

usagelog 与 chargeline 要做一致性校验:明细加总必须等于请求总费用,防止计费规则改版后历史数据对不上。

七、扣费幂等与重试

上游重试、网关重发会导致同一请求被计两次费。用请求的唯一 log_id 做幂等键,落账前先检查是否存在:

INSERT INTO usage_log (log_id, account_id, model, total_tokens, cost)
VALUES (#{logId}, #{accountId}, #{model}, #{totalTokens}, #{cost})
ON DUPLICATE KEY UPDATE updated_at = NOW();

log_id 建唯一索引后,重复提交只会更新更新时间,金额不会叠加。

八、完整扣费流程

  1. 请求解析出可计费模型,查价格表拿输入输出单价;
  2. 按输入 Token 与 max_tokens 预估费用,做余额预检与预留;
  3. 转发上游,SSE 流式转发,边收边记;
  4. 拿到最终 usage(含 cached_tokens),按规则重算费用;
  5. 原子 SQL 实扣余额,扣不动返回 402;
  6. 写 usagelog、chargeline、wallet_transaction 三类账本;
  7. 预留差额多退少补,结束。

九、常见坑

价格表只配输入输出两个价,遇到缓存、长上下文、图像输出就崩,条件计价规则要独立成表。折扣直接改模型单价,账单里就无法解释原价、套餐抵扣、等级优惠各是多少,折扣应作为独立权益层。请求结束后才查余额,高并发下平台先垫付上游成本,风险积累很快。上游账单不能直接当客户账单,客户价、折扣、套餐抵扣是平台侧逻辑,上游只有消耗证据。

常见问题(FAQ)

Q1:余额扣减为什么必须用一条原子 SQL?

分开读判断扣三步有窗口期,并发下会超卖扣成负数。

Q2:SSE 流式请求怎么计费?

断线后继续读上游尾部事件拿 usage 再结算,预留金额多退少补。

Q3:金额为什么不能用浮点数?

小额单价乘多维再累加误差放大,对账困难,一律用整数厘存储。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部