
大模型平台的计费与成本架构——多租户的 Token 计量与配额管理一、计费是商业化的基石大模型平台的商业化运营绕不开一个核心问题如何精确计量每个租户的 Token 消耗并进行准确计费。与传统的 SaaS 产品按功能或用户数计费不同LLM 平台本质上是按算力消耗计费——而 Token 就是算力消耗的核心度量单位。一个成熟的计费架构需要同时支撑毫秒级的实时 Token 累计、多模型差异化定价、多租户预充值/后付费/混合配额管理以及对账级别的账单准确性。二、计费架构的全局设计graph TB subgraph 租户管理层 TENANT[租户注册与配置] QUOTA[配额管理] PRICE[定价规则引擎] end subgraph 实时计量层 GW[AI 网关] -- METER[Token 计量引擎] METER -- CACHE[(Redis 实时累计)] METER -- STREAM[消息流 Kafka] end subgraph 准实时聚合层 STREAM -- AGG[流式聚合 Flink] AGG -- OLAP[(ClickHouse)] STREAM -- ALERT[超限告警] end subgraph 离线计费层 OLAP -- BILL[账单计算引擎] BILL -- INVOICE[(账单表)] BILL -- DEDUCT[余额扣减] end subgraph 控制面 ALERT -- BLOCK{超限阻断} BLOCK -- GW CACHE -- GW end三、Token 计量的核心挑战3.1 不同模型的 Token 计量方式不同不同模型对 Token 的定义存在差异——GPT 系列使用 cl100k_base 分词器Claude 使用自己的 tokenizerLlama 系列使用 BPE tokenizer。平台需要支持多种模型的 Token 计量适配/** * Token 计量引擎——多模型 Token 计数适配 */ Service public class TokenMeteringEngine { /** 模型 → Tokenizer 映射 */ private final MapString, Tokenizer tokenizerCache new ConcurrentHashMap(); /** 缓存大小上限——LRU 淘汰 */ private static final int MAX_CACHED_TOKENIZERS 20; /** * 计量一次推理请求的 Token 消耗 * * param request 推理请求元数据 * return Token 使用量摘要 */ public TokenUsageSummary meter(InferenceRequest request) { String modelName request.getModelName(); try { // Step 1: 获取对应模型的 Tokenizer Tokenizer tokenizer getTokenizer(modelName); // Step 2: 计算 Prompt Token实际生产中优先使用 API 返回值 int promptTokens request.getUsage().getPromptTokens(); int completionTokens request.getUsage().getCompletionTokens(); // Step 3: 如果 API 未返回 Token 数本地估算 if (promptTokens 0) { promptTokens tokenizer.countTokens( buildPromptText(request.getMessages())); } if (completionTokens 0) { completionTokens tokenizer.countTokens( request.getResponseText()); } int totalTokens promptTokens completionTokens; // Step 4: 计算实际费用 long costMicroDollars calculateCost(modelName, promptTokens, completionTokens); TokenUsageSummary summary TokenUsageSummary.builder() .requestId(request.getRequestId()) .tenantId(request.getTenantId()) .userId(request.getUserId()) .modelName(modelName) .promptTokens(promptTokens) .completionTokens(completionTokens) .totalTokens(totalTokens) .costMicroDollars(costMicroDollars) .timestamp(System.currentTimeMillis()) .build(); // Step 5: 实时累计到 Redis accumulateRealtime(summary); // Step 6: 异步发送至 Kafka 供流式聚合 publishToKafka(summary); return summary; } catch (Exception e) { log.error(Token 计量失败requestId{}, model{}, request.getRequestId(), modelName, e); // 计量失败不应该阻塞用户请求异步补偿 compensationService.markForCompensation(request); return TokenUsageSummary.unknown(request); } } /** * 多模型差异化定价计算 * 单位微美元1/1000000 USD避免浮点精度问题 */ private long calculateCost(String modelName, int promptTokens, int completionTokens) { PricingRule rule pricingService.getRule(modelName); // prompt 和 completion 通常不同价 long promptCost promptTokens * rule.getPromptPricePerMillionTokens() / 1_000_000L; long completionCost completionTokens * rule.getCompletionPricePerMillionTokens() / 1_000_000L; return promptCost completionCost; } /** * Redis 实时累计——支持毫秒级的配额检查 */ private void accumulateRealtime(TokenUsageSummary summary) { String key String.format(token:usage:%s:%s:%s, summary.getTenantId(), summary.getModelName(), LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)); // Redis Lua 脚本保证原子性INCRBY 设置过期时间 String luaScript local current redis.call(INCRBY, KEYS[1], ARGV[1]) if current tonumber(ARGV[1]) then redis.call(EXPIRE, KEYS[1], ARGV[2]) end return current ; DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(luaScript); script.setResultType(Long.class); redisTemplate.execute(script, Collections.singletonList(key), String.valueOf(summary.getTotalTokens()), String.valueOf(86400 * 35) // 保存 35 天 ); } }四、配额管理与超限控制多租户平台需要支持预充值、后付费、试用额度等多种配额模型同时保证在配额耗尽时能及时阻断/** * 配额管理器——租户级 Token 使用额度检查与扣减 */ Service public class QuotaManager { private static final Logger log LoggerFactory.getLogger( QuotaManager.class); private final StringRedisTemplate redisTemplate; /** 配额检查的 Lua 脚本——原子化检查预留 */ private static final String QUOTA_CHECK_SCRIPT local quotaKey KEYS[1] -- 总配额 key local usedKey KEYS[2] -- 已使用 key local requested tonumber(ARGV[1]) -- 本次请求 Token 数 -- 1. 获取总配额 local totalQuota redis.call(GET, quotaKey) if not totalQuota then return -1 -- 配额未配置 end totalQuota tonumber(totalQuota) -- 2. 获取已使用量 local used redis.call(GET, usedKey) if not used then used 0 else used tonumber(used) end -- 3. 检查是否超限 if used requested totalQuota then return -2 -- 配额不足 end -- 4. 原子扣减 redis.call(INCRBY, usedKey, requested) return used requested -- 返回累计使用量 ; /** * 检查并扣减配额 * * param tenantId 租户 ID * param requestedTokens 请求消耗的 Token 数 * return 配额检查结果 */ public QuotaCheckResult checkAndDeduct(String tenantId, long requestedTokens) { String quotaKey quota: tenantId :total; String usedKey quota: tenantId :used: LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(QUOTA_CHECK_SCRIPT); script.setResultType(Long.class); Long result redisTemplate.execute(script, List.of(quotaKey, usedKey), String.valueOf(requestedTokens) ); if (result -1) { log.warn(租户 {} 无配额配置, tenantId); return QuotaCheckResult.notConfigured(); } if (result -2) { log.warn(租户 {} Token 配额不足当前使用已超限, tenantId); return QuotaCheckResult.exceeded(); } // 检查是否接近限额80%推送预警 Long totalQuota Long.valueOf( redisTemplate.opsForValue().get(quotaKey)); if (result totalQuota * 0.8) { alertService.sendQuotaWarning(tenantId, result, totalQuota); } return QuotaCheckResult.allowed(result, totalQuota); } }五、账单生成与对账账单需要同时保留两个维度的数据分钟级的时间序列数据供实时监控和告警和天级的聚合账单供结算和对账/** * 账单计算——基于 ClickHouse 的 OLAP 查询生成租户账单 */ Repository public class BillingRepository { private final JdbcTemplate clickHouseTemplate; /** * 生成指定租户的月度账单 * * param tenantId 租户 ID * param yearMonth 账单月份格式202607 * return 账单明细 */ public BillingStatement generateMonthlyBill(String tenantId, String yearMonth) { // 按模型汇总 Token 使用量 String sql SELECT model_name, sum(prompt_tokens) AS total_prompt_tokens, sum(completion_tokens) AS total_completion_tokens, sum(total_tokens) AS total_tokens, sum(cost_micro_dollars) / 1000000.0 AS total_cost_usd, count(DISTINCT user_id) AS active_users, count(DISTINCT request_id) AS total_requests FROM token_usage_hourly WHERE tenant_id ? AND toYYYYMM(timestamp) ? GROUP BY model_name ORDER BY total_cost_usd DESC ; try { ListModelBillingItem items clickHouseTemplate.query( sql, new Object[]{tenantId, Integer.parseInt(yearMonth)}, (rs, rowNum) - ModelBillingItem.builder() .modelName(rs.getString(model_name)) .totalPromptTokens(rs.getLong(total_prompt_tokens)) .totalCompletionTokens(rs.getLong(total_completion_tokens)) .totalTokens(rs.getLong(total_tokens)) .totalCostUsd(rs.getBigDecimal(total_cost_usd)) .activeUsers(rs.getInt(active_users)) .totalRequests(rs.getLong(total_requests)) .build() ); BillingStatement statement new BillingStatement(); statement.setTenantId(tenantId); statement.setBillingPeriod(yearMonth); statement.setItems(items); statement.setTotalCostUsd( items.stream() .map(ModelBillingItem::getTotalCostUsd) .reduce(BigDecimal.ZERO, BigDecimal::add) ); return statement; } catch (DataAccessException e) { log.error(账单生成失败tenant{}, period{}, tenantId, yearMonth, e); throw new BillingException(账单生成异常, e); } } /** * 账单对账——比对网关日志和计费系统的数据一致性 */ public ReconciliationReport reconcile(String date) { // 从网关日志统计 BigDecimal gatewayTotal getGatewayTotalCost(date); // 从计费系统统计 BigDecimal billingTotal getBillingTotalCost(date); BigDecimal diff gatewayTotal.subtract(billingTotal).abs(); BigDecimal diffPercent diff.divide(gatewayTotal, 4, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); ReconciliationReport report new ReconciliationReport(); report.setDate(date); report.setGatewayTotal(gatewayTotal); report.setBillingTotal(billingTotal); report.setDifference(diff); report.setDifferencePercent(diffPercent); // 差异超过 0.1% 触发告警 if (diffPercent.compareTo(BigDecimal.valueOf(0.1)) 0) { log.error(计费对账差异超阈值date{}, 差异{}%, gateway{}, billing{}, date, diffPercent, gatewayTotal, billingTotal); alertService.sendReconciliationAlert(report); } return report; } }六、成本优化视角计费系统不仅要关心收了多少钱还要与成本系统打通支持投入产出分析/** * 模型推理成本计算——将 GPU 使用时间折算为单次推理成本 */ Service public class InferenceCostCalculator { /** * 计算单次推理的实际成本 * * param modelName 模型名称 * param executionTimeMs GPU 执行时间毫秒 * param gpuType GPU 类型 * return 推理成本美元 */ public BigDecimal calculateInferenceCost( String modelName, long executionTimeMs, String gpuType) { // GPU 单价美元/小时——来自云服务商或自建机房核算 double gpuHourlyRate getGpuHourlyRate(gpuType); // GPU 利用率实际使用率通常 70%-85% double utilizationRate getGpuUtilizationRate(); // 实际 GPU 小时 执行时间 * 利用率修正 double gpuHours executionTimeMs / 3_600_000.0 / utilizationRate; // 推理成本 GPU 小时 * 单价 BigDecimal inferenceCost BigDecimal.valueOf(gpuHours * gpuHourlyRate); // 加上基础设施成本分摊网络、存储、运维人力 BigDecimal infrastructureOverhead inferenceCost.multiply(BigDecimal.valueOf(0.15)); // 15% 分摊 return inferenceCost.add(infrastructureOverhead); } }计费数据一致性的工程保障计费系统对数据一致性的要求极高——少计会导致收入损失多计会引发客户投诉和信任危机。我们的生产实践中计费链路的数据一致性问题主要出现在三个环节第一流式响应场景下的 Token 计数。SSE 流式输出时如果客户端中途断开连接LLM 可能已经生成了部分 Token 但未返回完整的 usage 信息。如果按 API 返回的 usage 计费会丢失这部分消耗。我们的解决方案是在 AI 网关层独立维护 Token 计数器——使用与模型一致的 tokenizer 对已发送的每个 chunk 进行本地计数即使客户端断开也至少能记录已生成的部分。这块的差异通常在 5%-10% 之间对账单准确性影响显著。第二分布式场景下的计费重复。当网关发送 Kafka 消息时at-least-once 语义可能出现同一请求的 Token 使用量被多次写入。我们在计费聚合层Flink使用requestId作为去重键配合 24 小时的去重窗口和 Bloom Filter 判定将重复率控制在 0.01% 以下。第三跨天计费的时间边界。部分长推理请求可能跨越 00:00 的日配额重置边界。我们的处理策略是以请求发起时间作为归属日在上层配额管理中按时间戳归日避免因跨天导致的配额计算偏差。此外多模型定价的差异化也对计费系统提出了挑战。同一个 Prompt 在不同模型上的 Token 化结果差异可达 15%-30%这会直接影响用户的成本感知。我们建议在管理面提供成本预览功能——用户在发送请求前可以预估各模型的 Token 消耗和费用帮助用户做出成本最优的模型选择也能有效降低事后的费用争议。七、计费架构的边界与 Trade-offs7.1 实时计费的延迟与一致性权衡Redis 实时累计方案的最大优势是毫秒级的配额检查响应但其代价是数据持久化的延迟。在我们的生产实践中Redis 异步写入 ClickHouse 的延迟通常在 3-8 秒这意味着用户在前端看到的今日已用 Token 量与实际情况之间存在时间窗差异。对于高频调用用户如每秒数十次推理请求这个时间窗内可能发生超额使用。解决方法是对高消耗租户单次请求超过 10 万 Token采用同步双写策略同时写入 Redis 和 ClickHouse确保实时账单的准确性但这会将高消耗请求的 P99 延迟增加约 15-20ms需要在网关层对这种请求单独处理。7.2 多租户场景下的资源隔离成本当多个租户共享同一套 GPU 推理集群时计费系统还需要处理资源隔离的问题。如果租户 A 的大量请求导致 GPU 显存占满会影响租户 B 的推理延迟。一种方案是为每个租户预留固定的 GPU 资源资源隔离但这样会大幅降低 GPU 整体利用率通常低于 40%。我们在生产中采用的方案是软隔离 优先级抢占默认所有租户共享资源池但当某个租户的 Token 消耗速率超过其购买配额的 2 倍时动态降低其请求优先级。这种方案的计费复杂度在于需要实时计算超额使用的惩罚费用需要在计费引擎中增加动态的单价调整逻辑。7.3 预付费与后付费的坏账风险预付费模式对平台现金流更友好但对用户的资金占用较高可能导致用户流失后付费模式用户体验更好但存在坏账风险。我们在生产中采用混合模式新注册用户默认后付费但当月消费超过 1000 美元时强制切换为预付费或提供信用卡担保。计费系统需要实时计算用户的信用风险评分基于历史付款及时性、账户余额趋势、调用量增长速度并在评分低于阈值时自动触发限额或暂停服务。这部分逻辑需要与计费系统深度集成在 Token 计量引擎中增加信用检查步骤通常增加的延迟在 5ms 以内基于 Redis 缓存的信用评分结果。八、总结大模型平台的计费架构需要同时兼顾实时性毫秒级配额检查、准确性微美元级费用计算和可靠性对账差异 0.1%。核心技术选型上推荐Redis Lua 脚本实现实时累计Kafka Flink实现准实时聚合ClickHouse存储历史账单数据。在业务层面至少需要支持预充值、后付费和混合配额三种模式并保持与成本系统的打通。数据一致性的保障需要从流式计数、幂等去重和时间边界三个维度同时治理。计费架构的设计不是一次性的工作而是需要随业务规模增长持续演进的系统工程。