在大模型平台(LLM MaaS / Enterprise AI Gateway)的商业化运营中,计费与账单系统是承载平台现金流与多租户隔离的核心基石。大模型调用计费与传统 SaaS 的按月订阅或微服务按请求次数计费完全不同,它具有极高的复杂性:
- 计量粒度极度碎片化:按 Prompt Token、Completion Token、缓存命中 Token(Prompt Cache Hit)、视觉多模态输入(Resolution / Pixels)等多个细分维度综合定价;
- 时效性与准确性的尖锐矛盾:
- 租户额度监控与欠费熔断必须在秒级完成,一旦租户账户欠费却未能在秒级切断 API 密钥,大模型高并发调用会在数分钟内产生数万元的坏账;
- 财务核算与大客户月结发票则要求绝对的准确性、幂等性、审计链可追溯性与阶梯折扣平账,容不得任何流计算由于节点重启、乱序事件产生的微小偏差。
单轨制的架构设计在这一场景下必然走向死胡同。单纯依赖 T+1 离线批处理,无法实现欠费实时拦截;单纯依赖流计算(如 Flink),在大规模长连接、网络分区、反压与节点漂移场景下,累加状态难逃微小误差累积,且难以应对跨周期的阶梯折扣重算。
解决该矛盾的工业级方案是构建“增量流计算预扣 + 离线全量对账修正”的双轨制账单架构。
双轨制整体架构拓扑
系统在数据链路设计上彻底解耦实时控制流与最终财务结算流:
+-------------------------------------------+ | 网关集群 (LLM API Gateway) | +-------------------------------------------+ | 异步发射 Token 审计日志 | v +---------------------------+ | Kafka 计量日志 Topic | +---------------------------+ / \ / \ (实时轨: 秒级预算与欠费拦截) (离线轨: 绝对一致性与审计) / \ v v +--------------------+ +--------------------+ | 实时流处理计算引擎 | | 归档落盘对象存储 | | (Flink / Go-Worker)| | (Parquet / Iceberg)| +--------------------+ +--------------------+ | | 秒级扣减租户 Redis 额度 T+1 离线批处理重算 | | v v +-----------------------+ +--------------------+ | 实时风控 (透支停机) | | 生产财务月结明细表 | +-----------------------+ +--------------------+ ^ | | 双轨平账对账 | +-----------------------+1. 实时轨(Speed Layer):增量流计算与额度预扣
网关在每次流式传输完成后,生成包含tenant_id、request_id、model、prompt_tokens、completion_tokens、cache_read_tokens的标准化计量事件。实时流计算引擎消费该消息,按租户为 Key 进行窗口内滑动聚合,计算出纳分级(Nano-cents,1元 = $10^9$ 纳分,彻底规避浮点数误差)的预扣金额,原子扣减租户在 Redis 集群中的可用信用池。一旦余额跌破安全阈值,立即通过分布式发布订阅机制广播网关阻断该租户的后续鉴权请求。
2. 离线轨(Batch Layer):全量审计与最终一致性结账
原始计量事件双写或分流持久化至对象存储(S3/OSS),转化为列式存储(Parquet)。每日凌晨由批处理作业(Spark / StarRocks)执行 T+1 结算任务。离线任务拥有完整的上下文信息,可以精确处理:
- 复杂的跨月阶梯折扣规则(例如月度消费突破 10 万元后后续 Token 享受 7 折);
- 跨周期异常请求冲正(如模型网关由于后端 GPU 亚健康中途断连而发起的 SLA 补偿返还);
- 严格的幂等去重校验,消除分布式网络中可能存在的少量重复计量消息。
3. 平账机制(Reconciliation Engine)
离线任务在清算完成后,生成标准的不可篡改账单快照,并与实时轨在过去 24 小时记录的账本流水进行差异比对。若存在微小偏离(例如因网络抖动实时轨多扣或少扣了极少量额度),对账引擎会生成一条类型为RECONCILIATION_ADJUSTMENT的单向对冲流水,平滑更新租户的可用余额,实现双轨闭环。
核心对账与双轨平账引擎实现
计费系统的核心技术挑战在于跨时钟周期的浮点精度控制与高并发幂等对冲。以下是基于 Go 1.27.1 实现的生产级双轨账单对账核心模块:
package billing import ( "context" "database/sql" "errors" "fmt" "time" ) // 货币单位采用纳分(1 CNY = 1,000,000,000 Nano-cents),杜绝 float64 精度泄漏 type NanoCents int64 // BillItem 代表离线核算出的单项租户精准账单 type BillItem struct { TenantID string Date time.Time ModelID string PromptTokens int64 CompTokens int64 ExactCost NanoCents TierDiscount NanoCents // 阶梯优惠减免 FinalPayable NanoCents } // ReconciliationEngine 双轨对账平账处理器 type ReconciliationEngine struct { db *sql.DB redisClient RedisCreditClient } type RedisCreditClient interface { GetTenantRealtimeUsage(ctx context.Context, tenantID string, date string) (NanoCents, error) ApplyAdjustment(ctx context.Context, tenantID string, delta NanoCents, reason string) error } // ReconcileDailyBilling 执行日终平账闭环 func (re *ReconciliationEngine) ReconcileDailyBilling(ctx context.Context, dateStr string) error { // 1. 从离线分析数仓中提取核算完备的精准账单 items, err := re.fetchBatchAuditedBills(ctx, dateStr) if err != nil { return fmt.Errorf("failed to fetch audited bills: %w", err) } for _, item := range items { // 2. 查询实时轨在 Redis 中记录的累计扣减流水 realtimeCost, err := re.redisClient.GetTenantRealtimeUsage(ctx, item.TenantID, dateStr) if err != nil { // 单个租户异常记录日志并报警,不阻断整体清算流程 fmt.Printf("Error fetching realtime usage for tenant %s: %v\n", item.TenantID, err) continue } // 3. 计算差异 Delta = 离线精准应付 - 实时轨已扣 delta := item.FinalPayable - realtimeCost // 4. 若存在偏离,发起平账事务 if delta != 0 { if err := re.applyReconciliationAdjustment(ctx, item, delta, realtimeCost); err != nil { return fmt.Errorf("reconciliation failed for tenant %s: %w", item.TenantID, err) } } // 5. 固化不可篡改日结账单入库 if err := re.persistFinalBill(ctx, item, delta); err != nil { return fmt.Errorf("failed to persist final bill: %w", err) } } return nil } func (re *ReconciliationEngine) applyReconciliationAdjustment(ctx context.Context, item BillItem, delta NanoCents, realtimeCost NanoCents) error { tx, err := re.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}) if err != nil { return err } defer tx.Rollback() // 插入对账调整审计流水 query := ` INSERT INTO billing_adjustment_ledger ( tenant_id, bill_date, model_id, offline_exact_cost, realtime_deducted_cost, delta_nano, status, created_at ) VALUES (?, ?, ?, ?, ?, ?, 'APPLIED', NOW()) ` _, err = tx.ExecContext(ctx, query, item.TenantID, item.Date, item.ModelID, item.FinalPayable, realtimeCost, delta) if err != nil { return err } // 同步调整 Redis 中的实时可用信用额度(补偿或追缴) reason := fmt.Sprintf("Daily reconciliation delta for %s", item.Date.Format("2006-01-02")) if err := re.redisClient.ApplyAdjustment(ctx, item.TenantID, delta, reason); err != nil { return fmt.Errorf("redis adjustment rollback trigger: %w", err) } return tx.Commit() } func (re *ReconciliationEngine) fetchBatchAuditedBills(ctx context.Context, dateStr string) ([]BillItem, error) { // 查询经由离线任务严格去重与阶梯定价后的精准结果集 query := ` SELECT tenant_id, bill_date, model_id, sum(prompt_tokens), sum(completion_tokens), sum(final_payable) FROM dw_offline_tenant_token_daily WHERE bill_date = ? GROUP BY tenant_id, bill_date, model_id ` rows, err := re.db.QueryContext(ctx, query, dateStr) if err != nil { return nil, err } defer rows.Close() var result []BillItem for rows.Next() { var item BillItem var pTokens, cTokens, payable int64 if err := rows.Scan(&item.TenantID, &item.Date, &item.ModelID, &pTokens, &cTokens, &payable); err != nil { return nil, err } item.PromptTokens = pTokens item.CompTokens = cTokens item.FinalPayable = NanoCents(payable) result = append(result, item) } return result, nil } func (re *ReconciliationEngine) persistFinalBill(ctx context.Context, item BillItem, adjustment Delta) error { // 归档财务正式账单 return nil } type Delta = NanoCents离线数仓高吞吐计费模型 SQL 实现
在底层离线数仓(ClickHouse / StarRocks)中,T+1 离线任务通过以下经过性能优化的聚合 SQL,在数分钟内完成数亿级 Token 日志的幂等清洗与多级阶梯清算:
-- 离线按租户、模型计算精确消费与去重明细 INSERT INTO dw_offline_tenant_token_daily SELECT tenant_id, toDate(event_time) AS bill_date, model_id, count(DISTINCT request_id) AS total_requests, sum(prompt_tokens) AS total_prompt_tokens, sum(completion_tokens) AS total_completion_tokens, -- 按不同模型精准定价计算原始费用(单位:纳分) sum( prompt_tokens * price_per_prompt_nano + completion_tokens * price_per_comp_nano ) AS raw_payable, -- 结合月度累计用量计算动态阶梯抵扣 multiIf( sum(prompt_tokens + completion_tokens) > 1000000000, sum(prompt_tokens * price_per_prompt_nano + completion_tokens * price_per_comp_nano) * 0.20, sum(prompt_tokens + completion_tokens) > 100000000, sum(prompt_tokens * price_per_prompt_nano + completion_tokens * price_per_comp_nano) * 0.10, 0 ) AS tier_discount, -- 最终应付金额 raw_payable - tier_discount AS final_payable FROM ods_llm_token_events_distributed WHERE event_time >= '2026-10-10 00:00:00' AND event_time < '2026-10-11 00:00:00' GROUP BY tenant_id, bill_date, model_id;避坑指南与生产权衡
- 绝对禁止在货币计算中使用 Float/Double:计算机二进制浮点数在长链路累加下会出现经典的
0.1 + 0.2 = 0.30000000000000004灾难。在单次请求计费只有几厘钱的大模型场景中,高频累加会导致日终累计出现无法平账的错账。必须全链路采用整型“纳分”(或标准 BigInt/Decimal)进行原子计算。 - 实时轨允许“微过冲”,防范“死锁假死”:实时额度扣减为了追求高性能,在 Redis 中使用 Lua 脚本做分布式原子减法即可,无需引入沉重的分布式两阶段提交(2PC)。即使在余额刚好归零的瞬时,并发请求导致账户透支了几分钱,相比于引入分布式事务锁带来的 50ms 网关延迟损耗,允许微小透支并在离线平账时追回是经过工业验证的最高性价比选择。
- 计量日志与计费流水的唯一幂等键设计:每笔计量记录必须以
request_id作为唯一键。离线任务执行前必须强制基于request_id做去重处理,防止因网络重试导致同一笔长推理请求被重复扣款。