系列全称:AI 原生基础设施与大模型推理平台工程实践
传统接口通常使用 QPS、RPM 和并发数限制流量,但对于大模型请求,一次调用可能只输入几十个 Token,也可能携带十几万 Token 的上下文,并持续输出数千个 Token。
两个请求在 QPS 统计中完全相同,对模型容量和企业账单的影响却可能相差数百倍。
因此,大模型治理不能只回答“每秒允许调用多少次”,还必须回答:
- 每分钟允许消耗多少输入和输出 Token;
- 同时允许多少条生成流;
- 一个租户、项目、用户和 Agent 各有多少额度;
- 请求开始前如何预占未知的输出成本;
- 流结束后如何按真实 Usage 结算;
- 供应商限流、平台容量和企业预算如何统一;
- 如何避免一个错误循环在几分钟内烧掉整月预算。
引言:100 次请求,为什么可能比 10 万次普通接口更贵
一个普通查询接口:
GET /api/orders/ORD-001
通常具有相对稳定的资源消耗:
- SQL 次数大致可预测;
- 响应体较小;
- 请求持续时间较短;
- 单次成本变化有限。
因此可以采用:
每个用户每秒 10 次
每个接口每分钟 1000 次
进行流量控制。
大模型请求则完全不同。
请求 A:
输入:100 Token
输出:50 Token
请求 B:
输入:100,000 Token
输出:8,000 Token
二者在 RPM 中都只记作一次请求,但请求 B 会占用更多:
- 上下文处理时间;
- GPU Prefill 资源;
- KV Cache;
- 输出生成时间;
- 供应商 Token 配额;
- 企业成本预算。
如果平台仍只采用:
rateLimit:
requestsPerMinute: 100
一个用户只需发送少量超长请求,就可能耗尽整个租户的模型额度。
这也是为什么主流模型服务通常同时采用 RPM、输入 Token、输出 Token、总 Token、并发和消费额度等限制。Anthropic 的 API 限额区分 RPM、输入 TPM 和输出 TPM;Amazon Bedrock 也以模型级 Token 配额约束推理容量,部分运行时接口已经主要由 Token 配额而不是 RPM 治理。Envoy AI Gateway 则专门提供基于 Token Usage 的限流能力。
真正的生产方案应该是:
QPS / RPM
+ Input TPM
+ Output TPM
+ Concurrent Streams
+ Daily Token Quota
+ Cost Budget
+ Provider Capacity
先看一个失控场景:为什么一个 Agent 循环就能烧穿整月预算
假设我们有一个 Spring Boot 编排层,负责接入客服 Agent、营销 Agent 和报表生成 Agent;底层同时连接云端大模型和私有化推理集群。某天一个营销 Agent 的 Prompt 模板出现缺陷,进入“生成不满意就继续追问”的循环。
表面上看,接口 QPS 并不高:
- 每秒只有 3 到 5 个请求;
- 每个用户也没有明显超频;
- HTTP 层连接数和线程池都很平稳。
但真实资源消耗已经失控:
- 每次请求都携带数万 Token 的上下文和历史消息;
- 每次失败重试都会再次消耗输入 Token 与输出 Token;
- 流式响应持续数十秒,占住并发槽位;
- 多个 Agent 并发运行,租户预算在几分钟内被快速拉满;
- 云模型额度耗尽后,流量被动切到私有集群,GPU 队列瞬间堆积。
这类事故说明,AI 平台中的“限流”不能只盯着入口请求数,而要同时盯住流量速度、资源占用和财务边界。下面给出一个简化治理闭环:
如果这一套闭环缺少任何一环,平台都会暴露明显短板:
- 没有事前估算,请求可能在模型已经执行后才发现预算不足;
- 没有原子预占,并发高峰下会出现超卖;
- 没有流结束结算,取消和失败请求就会让账本失真;
- 没有预算联动路由,高价值请求会和低价值批处理一起争抢最贵模型。
因此,Token、配额和成本治理的本质,不是“把调用卡死”,而是给 AI 基础设施建立一套容量与财务的双重控制面。
一、先分清四个容易混淆的概念
1.1 Rate Limit:速率限制
限制单位时间内的消耗速度。
例如:
每分钟 100 次请求
每分钟 200,000 输入 Token
每分钟 50,000 输出 Token
它主要保护:
- 模型供应商;
- AI Gateway;
- 自托管 GPU;
- 公平使用;
- 突发流量。
1.2 Quota:用量配额
限制一个较长周期内允许消耗的总量。
例如:
每个用户每天 100 万 Token
每个租户每月 2 亿 Token
每个项目每周 10,000 次模型调用
它主要用于:
- 多租户资源分配;
- 产品套餐;
- 部门成本控制;
- 防止长期滥用。
1.3 Budget:财务预算
限制可以产生的金额。
例如:
单次请求不超过 0.10 美元
项目每天不超过 100 美元
租户每月不超过 5000 美元
Token 相同,模型价格可能完全不同,因此 Token Quota 不能替代 Cost Budget。
1.4 Provider Limit:供应商限额
供应商对账号、项目、模型或 Region 设置的外部上限。
可能包括:
- RPM;
- 输入 TPM;
- 输出 TPM;
- 并发请求;
- 每日 Token;
- 月度消费;
- Batch 队列;
- 专属容量。
平台必须在自己的租户配额之外,继续遵守供应商限额。
1.5 四层治理关系
只要任意一层不足,请求就不能无条件继续。
二、为什么 QPS 不够用
2.1 请求大小差异巨大
QPS 只统计请求个数,不关心:
- Prompt 长度;
- 历史消息;
- RAG 证据;
- Tool Schema;
- 最大输出;
- 多模态内容。
2.2 请求持续时间差异巨大
一个简单分类可能 300 毫秒完成。
一次深度推理可能持续几分钟。
同样的 QPS 下,活跃连接和并发生成数量完全不同。
2.3 输入和输出消耗不同
许多模型的:
Input Token Price
Output Token Price
Cached Input Price
并不相同。
输出 Token 往往比输入 Token 更昂贵,也更占用持续 Decode 资源。
因此,单一 Total Token 仍然不够精确。
2.4 自托管推理还受显存和 KV Cache 约束
对于 vLLM 等推理服务:
长输入
→ Prefill 压力增加
长输出
→ Decode 时间增加
高并发
→ KV Cache 占用增加
即使总 Token 尚未超过日配额,当前 GPU 也可能无法接纳更多请求。
2.5 成本并不只由 Token 构成
一次 AI 请求还可能产生:
- Web Search;
- Rerank;
- Embedding;
- 图片生成;
- 音频处理;
- Code Interpreter;
- Tool API;
- 重试;
- 审核模型;
- 缓存写入。
因此,最终仍需要统一成本账本。
三、建议建立六维限额模型
请求频率
+ 输入 Token
+ 输出 Token
+ 并发
+ 周期总量
+ 金额预算
| 维度 |
主要用途 |
| RPM/QPS |
防止高频小请求 |
| Input TPM |
控制 Prompt、RAG 和 Prefill 压力 |
| Output TPM |
控制生成负载和输出成本 |
| Concurrent Streams |
控制长连接和 Decode 并发 |
| Daily/Monthly Tokens |
产品套餐和部门分配 |
| Cost Budget |
控制真实财务风险 |
四、配额应该按哪些主体分层
生产平台通常至少需要以下 Scope:
Organization
Tenant
Department
Project
Application
Agent
User
API Key
Model
Provider
请求实际可用额度应取多层交集:
例如:
租户还有 100 万 Token
项目只剩 5 万 Token
用户只剩 1 万 Token
当前请求最多只能使用用户剩余的 1 万 Token。
4.1 为什么不能只设租户总额度
如果只有租户级配额:
- 单个用户可能耗尽全部额度;
- 测试脚本可能影响生产应用;
- 一个 Agent 死循环可能拖垮整个部门;
- 无法区分项目成本。
因此,至少需要:
租户总额度
+ 项目额度
+ 用户或 API Key 防滥用限额
五、请求前为什么需要 Token 估算
输入 Token 可以在发送前计算或估算,但输出 Token 尚未生成。
如果平台等到执行完成后才检查:
请求已经花钱,
拒绝已经没有意义。
因此,需要在请求开始前预占:
成本预占:
5.1 输入 Token 估算
输入应覆盖:
System Prompt
Messages
Tool Definitions
RAG Evidence
Images / Audio 的计费换算
Provider-specific Metadata
不同模型可能使用不同 Tokenizer,因此推荐:
- 使用目标模型官方 Tokenizer;
- 无法精确计算时使用兼容估算;
- 添加安全系数;
- 记录估算值和供应商实际值差异。
5.2 输出预留不能总用 max_tokens
假设:
max_tokens = 16,000
但历史上此类任务的 P95 输出只有 2,000 Token。
始终按 16,000 预占会造成大量额度被临时锁定。
推荐结合:
任务类型历史分布
模型历史分布
用户显式 max_tokens
平台安全上限
计算:
预留输出 = min(max_tokens, 该场景 P95 输出 × 安全系数, 平台安全上限)
对于高风险预算,可以使用更保守的 P99。
六、核心流程:预占、执行、结算
完整状态:
CREATED
RESERVED
CONSUMING
SETTLED
RELEASED
EXPIRED
RECONCILING
七、为什么预占必须原子化
假设某用户只剩:
10,000 Token
两个并发请求分别准备预占:
请求 A:8,000
请求 B:8,000
如果流程是:
查询余额
→ 判断足够
→ 扣减余额
两个请求可能同时通过,最终超额使用 6,000 Token。
因此,多层额度检查和扣减必须是原子的。
可选方案:
- Redis Lua;
- Redis Function;
- 数据库行锁;
- 乐观锁 CAS;
- 专用配额服务。
高并发在线限流通常更适合 Redis 或内存数据面,长期账本则写数据库。
八、Token Bucket 仍然适用,但单位变成 Token
传统 Token Bucket:
- 桶容量控制突发;
- 固定速率补充;
- 请求消耗一个或多个令牌。
AI 场景中,一个请求消耗的令牌数量不是 1,而是预估 Token 数。
Bucket Capacity:200,000 Token
Refill Rate:每秒 3,333 Token
Request Cost:本次预计 12,000 Token
判断:
当前可用 Token >= 请求预计 Token
→ 接纳
否则
→ 返回 Retry-After
8.1 输入和输出建议分桶
Input Token Bucket
Output Token Bucket
Total Token Bucket
原因:
- 供应商可能分别限制输入和输出;
- 输出价格和生成资源不同;
- 超长 Prompt 与超长输出的风险不同。
Envoy AI Gateway 的 Usage-based Rate Limiting 也支持基于捕获到的输入、输出和总 Token 用量建立不同限流桶。
九、并发限制不能缺失
TPM 只控制单位时间总量,不能控制瞬时并发。
假设:
TPM = 1,000,000
100 个长请求同时进入,仍可能:
- 占满连接池;
- 耗尽 GPU KV Cache;
- 触发供应商并发限制;
- 导致 TTFT 急剧升高。
因此还需要:
Max Concurrent Requests
Max Concurrent Streams
Max Concurrent Requests per Model
Max Concurrent Requests per Tenant
9.1 并发槽位应在流结束后释放
流式请求的并发占用从:
上游调用开始
持续到:
正常完成
客户端取消
超时
上游错误
任何终止路径都必须释放,否则会产生“幽灵并发”。
十、预算不只是硬拒绝
成本预算可以分为多个阶段。
绿色区间
使用率 < 70%
正常执行。
黄色区间
70%~90%
- 告警;
- 优先使用低成本模型;
- 减少最大输出;
- 暂停非关键批任务。
红色区间
90%~100%
- 仅允许高优先级任务;
- 强制低成本路由;
- 禁止实验流量;
- 要求人工批准。
耗尽
>= 100%
拒绝或使用明确配置的应急额度。
10.1 软预算路由
预算接近上限时,可以将信号传给多模型路由:
{
"budgetPressure": "HIGH",
"remainingCost": 12.50,
"preferredTier": "LOW_COST"
}
但数据安全和质量底线仍不能被突破。
十一、成本模型必须版本化
价格不是模型永久不变的属性。
它可能随以下条件变化:
- 模型版本;
- Provider;
- Region;
- Service Tier;
- Batch;
- Cached Input;
- Prompt Cache Write;
- 生效时间;
- 企业折扣。
建议保存价格快照:
CREATE TABLE ai_model_price (
id VARCHAR(64) PRIMARY KEY,
endpoint_id VARCHAR(64) NOT NULL,
price_type VARCHAR(32) NOT NULL,
unit VARCHAR(32) NOT NULL,
currency VARCHAR(16) NOT NULL,
price NUMERIC(24, 12) NOT NULL,
effective_from TIMESTAMPTZ NOT NULL,
effective_to TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
历史价格不能直接覆盖,否则无法重算历史账单。
十二、成本账本应该记录什么
CREATE TABLE ai_usage_ledger (
id VARCHAR(64) PRIMARY KEY,
request_id VARCHAR(128) NOT NULL,
reservation_id VARCHAR(64),
tenant_id VARCHAR(64) NOT NULL,
project_id VARCHAR(64),
application_id VARCHAR(64),
agent_id VARCHAR(64),
user_id VARCHAR(64),
logical_model VARCHAR(128) NOT NULL,
model_version_id VARCHAR(64) NOT NULL,
endpoint_id VARCHAR(64) NOT NULL,
provider_request_id VARCHAR(256),
input_tokens BIGINT NOT NULL DEFAULT 0,
output_tokens BIGINT NOT NULL DEFAULT 0,
cached_input_tokens BIGINT NOT NULL DEFAULT 0,
reasoning_tokens BIGINT NOT NULL DEFAULT 0,
estimated_cost NUMERIC(20, 8),
actual_cost NUMERIC(20, 8),
currency VARCHAR(16) NOT NULL,
usage_source VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
started_at TIMESTAMPTZ NOT NULL,
settled_at TIMESTAMPTZ,
UNIQUE(request_id, endpoint_id)
);
12.1 为什么保存 Provider Request ID
用于:
- 与供应商账单核对;
- 排查重复计费;
- 处理 Usage 缺失;
- 审计重试和 Fallback;
- 成本争议定位。
十三、配额策略数据模型
CREATE TABLE ai_quota_policy (
id VARCHAR(64) PRIMARY KEY,
policy_code VARCHAR(128) NOT NULL,
version INTEGER NOT NULL,
scope_type VARCHAR(32) NOT NULL,
scope_id VARCHAR(128),
model_selector JSONB NOT NULL DEFAULT '{}'::jsonb,
rpm_limit BIGINT,
input_tpm_limit BIGINT,
output_tpm_limit BIGINT,
total_tpm_limit BIGINT,
concurrent_limit INTEGER,
daily_token_limit BIGINT,
monthly_token_limit BIGINT,
daily_cost_limit NUMERIC(20, 8),
monthly_cost_limit NUMERIC(20, 8),
currency VARCHAR(16),
status VARCHAR(32) NOT NULL,
effective_from TIMESTAMPTZ NOT NULL,
effective_to TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(policy_code, version)
);
十四、预占记录
CREATE TABLE ai_quota_reservation (
id VARCHAR(64) PRIMARY KEY,
request_id VARCHAR(128) NOT NULL UNIQUE,
tenant_id VARCHAR(64) NOT NULL,
policy_snapshot JSONB NOT NULL,
reserved_input_tokens BIGINT NOT NULL,
reserved_output_tokens BIGINT NOT NULL,
reserved_cost NUMERIC(20, 8) NOT NULL,
actual_input_tokens BIGINT,
actual_output_tokens BIGINT,
actual_cost NUMERIC(20, 8),
status VARCHAR(32) NOT NULL,
expire_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
settled_at TIMESTAMPTZ
);
预占需要 TTL。调用方崩溃后,过期任务会进入对账流程,而不是永久锁住额度。
十五、Spring Boot 请求模型
public record QuotaReservationCommand(
String requestId,
String tenantId,
String projectId,
String applicationId,
String userId,
String modelVersionId,
String endpointId,
long estimatedInputTokens,
long reservedOutputTokens,
BigDecimal estimatedCost
) {}
public record QuotaReservation(
String reservationId,
Instant expiresAt,
long reservedInputTokens,
long reservedOutputTokens,
BigDecimal reservedCost
) {}
十六、配额服务接口
public interface AiQuotaService {
QuotaReservation reserve(
QuotaReservationCommand command
);
SettlementResult settle(
String reservationId,
ActualUsage usage
);
void release(
String reservationId,
ReleaseReason reason
);
}
关键要求:
requestId 幂等;
- 多层 Scope 原子校验;
- 重复结算不重复扣费;
- 预占可过期;
- 实际使用超过预占时支持补扣;
- 补扣失败进入对账或预算超限状态。
十七、预占服务参考实现
@Service
@RequiredArgsConstructor
public class DefaultAiQuotaService
implements AiQuotaService {
private final QuotaPolicyService policyService;
private final QuotaCounterStore counterStore;
private final QuotaReservationRepository repository;
private final CostCalculator costCalculator;
@Override
public QuotaReservation reserve(
QuotaReservationCommand command
) {
Optional<QuotaReservation> existing =
repository.findByRequestId(
command.requestId()
);
if (existing.isPresent()) {
return existing.get();
}
List<ResolvedQuotaPolicy> policies =
policyService.resolveHierarchy(
command
);
ReservationDemand demand =
ReservationDemand.from(command);
AtomicReserveResult result =
counterStore.tryReserve(
policies,
demand,
Duration.ofMinutes(15)
);
if (!result.accepted()) {
throw new QuotaExceededException(
result.rejectedScope(),
result.rejectedDimension(),
result.retryAfter()
);
}
QuotaReservation reservation =
QuotaReservationFactory.create(
command,
policies,
result
);
try {
repository.insert(reservation);
return reservation;
} catch (RuntimeException ex) {
counterStore.rollback(result);
throw ex;
}
}
@Override
public SettlementResult settle(
String reservationId,
ActualUsage usage
) {
QuotaReservationEntity reservation =
repository.lockById(reservationId);
if (reservation.isSettled()) {
return reservation.toSettlementResult();
}
BigDecimal actualCost =
costCalculator.calculate(
reservation.endpointId(),
usage,
reservation.startedAt()
);
SettlementDelta delta =
SettlementDelta.between(
reservation,
usage,
actualCost
);
counterStore.settle(
reservation.counterSnapshot(),
delta
);
reservation.settle(usage, actualCost);
repository.update(reservation);
return reservation.toSettlementResult();
}
@Override
public void release(
String reservationId,
ReleaseReason reason
) {
QuotaReservationEntity reservation =
repository.lockById(reservationId);
if (!reservation.canRelease()) {
return;
}
counterStore.release(
reservation.counterSnapshot()
);
reservation.release(reason);
repository.update(reservation);
}
}
数据库和 Redis 很难天然处于同一事务中,因此生产实现还应增加:
- Outbox;
- 对账任务;
- 幂等状态;
- Reservation TTL;
- Counter Repair。
十八、Redis Key 设计
ai:quota:rpm:{scopeType}:{scopeId}:{model}:{minute}
ai:quota:input-tpm:{scopeType}:{scopeId}:{model}
ai:quota:output-tpm:{scopeType}:{scopeId}:{model}
ai:quota:concurrent:{scopeType}:{scopeId}:{model}
ai:quota:daily-token:{scopeType}:{scopeId}:{date}
ai:budget:monthly:{scopeType}:{scopeId}:{month}
高基数用户量很大时,需要:
- 合理 TTL;
- 避免无界 Key;
- 分片;
- 热点租户隔离;
- Lua 执行时长控制。
十九、Redis Lua 原子预占思路
-- KEYS:
-- 1 input token bucket
-- 2 output token bucket
-- 3 concurrent counter
--
-- ARGV:
-- 1 required input
-- 2 required output
-- 3 max concurrent
-- 4 ttl seconds
local input_available = tonumber(
redis.call('GET', KEYS[1]) or '0'
)
local output_available = tonumber(
redis.call('GET', KEYS[2]) or '0'
)
local concurrent = tonumber(
redis.call('GET', KEYS[3]) or '0'
)
if input_available < tonumber(ARGV[1]) then
return {0, 'INPUT_TPM'}
end
if output_available < tonumber(ARGV[2]) then
return {0, 'OUTPUT_TPM'}
end
if concurrent >= tonumber(ARGV[3]) then
return {0, 'CONCURRENT'}
end
redis.call('DECRBY', KEYS[1], ARGV[1])
redis.call('DECRBY', KEYS[2], ARGV[2])
redis.call('INCR', KEYS[3])
redis.call('EXPIRE', KEYS[3], ARGV[4])
return {1, 'ACCEPTED'}
该示例只表达原子思想。完整实现还需要:
- 桶补充;
- 多层 Scope;
- Reservation ID;
- 回滚;
- 幂等;
- 时间窗口;
- Redis Cluster 同 Slot;
- 脚本版本管理。
二十、流式请求如何结算
流式响应存在三个结果。
正常完成
供应商返回最终 Usage:
按真实 Token 结算
→ 释放未使用预占
客户端取消
可能已经生成部分 Token:
使用已观测 Usage
→ 若供应商后续提供账单,再补账
流中断且 Usage 未知
状态标记为 RECONCILING
→ 通过 Provider Usage API、账单或估算补账
不能直接释放全部预占,否则故障请求会变成“免费调用”。
二十一、供应商 429 怎么处理
供应商返回 429 可能表示:
- RPM 超限;
- 输入 TPM 超限;
- 输出 TPM 超限;
- 加速限制;
- 共享容量不足;
- 账户消费额度不足。
AI Gateway 应将其转换成统一错误:
{
"error": {
"code": "PROVIDER_RATE_LIMITED",
"dimension": "INPUT_TPM",
"provider": "provider-a",
"retryable": true,
"retryAfterSeconds": 8
}
}
处理策略:
- 遵守
Retry-After 或供应商限额信息;
- 使用带抖动的指数退避;
- 首 Token 前可以切换备用供应商;
- 不要立即并发重试;
- 将供应商剩余配额反馈给多模型路由;
- 区分用户超额和供应商超额。
二十二、为什么重试也要消耗预算
错误设计:
原始请求计费
重试不计入用户预算
这会让不稳定模型把成本转嫁给平台。
更合理:
所有实际产生的 Provider Usage
→ 都进入成本账本
但产品层可以根据责任定义:
- 用户可见额度是否扣减;
- 平台是否承担故障重试;
- 是否计入 SLA 赔偿;
- 是否向供应商申请核对。
财务账本与产品配额可以是两套视图,但底层 Usage 不能丢失。
二十三、自托管 GPU 如何换算成本
自托管模型没有供应商 Token 账单,但仍有成本:
GPU 租用
CPU
内存
存储
网络
闲置
运维
可采用:
GPU 成本 / (单位时间 × Token 产出) = 每 Token 成本
更精细时区分:
- Prefill Token;
- Decode Token;
- GPU 型号;
- 模型规模;
- 量化方式;
- 批处理效率;
- 利用率。
不要因为“服务器已经购买”就把自托管推理成本记为零。
二十四、成本治理不应阻塞每个请求写数据库
高并发场景中,不能每输出一个 Token 就更新 PostgreSQL。
推荐:
Gateway 内存累计 Usage
→ 请求结束生成 Usage Event
→ MQ
→ Usage Consumer
→ Ledger
→ 分钟 / 小时聚合
强一致的配额预占走 Redis 或配额服务,账单分析走异步数据链路。
二十五、完整架构
(架构图)
二十六、可观测性
限流指标
ai_quota_reservation_total
ai_quota_rejected_total
ai_quota_rejected_by_dimension
ai_quota_reservation_latency
ai_quota_active_reservations
ai_quota_expired_reservations
Token 指标
ai_input_tokens
ai_output_tokens
ai_cached_input_tokens
ai_reserved_tokens
ai_reservation_utilization
ai_token_estimation_error
成本指标
ai_estimated_cost
ai_actual_cost
ai_cost_variance
ai_daily_budget_utilization
ai_monthly_budget_utilization
ai_cost_by_tenant
ai_cost_by_model
ai_cost_by_agent
供应商指标
provider_remaining_rpm
provider_remaining_input_tpm
provider_remaining_output_tpm
provider_rate_limited_total
provider_retry_after_seconds
二十七、重要告警
- 租户月预算超过 80%;
- 单小时成本异常增长;
- Token 估算偏差持续过高;
- 大量 Reservation 过期;
- 输出 Token P95 突然增长;
- 某 Agent 单次任务循环调用过多;
- 供应商 429 比例升高;
- 自托管 GPU 队列持续增长;
- 实际成本长期高于预估;
- 同一 Request ID 出现重复账单。
二十八、常见错误实践
1. 只按 QPS 或 RPM 限流
看不见请求实际资源和成本。
2. 所有 Token 使用同一个桶
输入、输出和缓存 Token 的价格及资源特征不同。
3. 请求完成后才检查预算
钱已经花掉,无法阻止。
4. 永远按 max_tokens 全量预占
会造成大量额度虚假占用。
5. 只按 Token 控制,不限制并发
瞬时长连接仍可能压垮 GPU 和网关。
6. Redis 只做查询再扣减
并发下会超卖额度。
7. 失败请求不记成本
供应商可能已经实际计费。
8. 将自托管 GPU 视为零成本
无法比较云模型与私有模型。
9. 修改价格时覆盖旧记录
历史账单失去依据。
10. 配额数据全部同步写数据库
在线链路吞吐和延迟难以承受。
11. 一个 Agent 没有独立预算
循环错误可能耗尽整个项目。
12. 只告警,不设置硬上限
异常脚本仍会持续产生账单。
二十九、什么时候简单 QPS 已经够用
以下场景可以先保持简单:
- 调用量很小;
- 只有一个内部用户;
- 单一模型;
- Prompt 长度稳定;
- 无多租户;
- 成本风险很低;
- 仍处于 Demo 阶段。
但至少应记录真实 Token 和成本,为后续治理准备数据。
三十、演进路线
阶段一:Usage 可见
记录输入、输出 Token 和模型成本
阶段二:Token 限流
增加:
- Input TPM;
- Output TPM;
- 并发;
- 用户限额。
阶段三:多层配额
增加:
- 租户;
- 项目;
- Agent;
- API Key;
- 日/月配额。
阶段四:预算和路由联动
增加:
- 预占;
- 真实结算;
- 软预算;
- 低成本路由;
- 阶梯告警。
阶段五:FinOps 闭环
增加:
- 云模型和 GPU 统一成本;
- 部门分摊;
- 预算预测;
- 异常检测;
- 自动优化建议。
三十一、关键设计决策
| 设计问题 |
推荐方案 |
原因 |
| 是否保留 QPS |
保留 |
防止高频小请求 |
| 核心计量单位 |
输入、输出 Token 分开 |
价格和资源不同 |
| 请求前如何控制 |
估算并预占 |
输出事前未知 |
| 请求后如何处理 |
按真实 Usage 结算 |
释放多余额度 |
| 多层额度如何计算 |
取最严格交集 |
防止子级超用 |
| 在线计数存哪里 |
Redis/内存配额服务 |
低延迟原子操作 |
| 长期账本存哪里 |
PostgreSQL/分析存储 |
审计和统计 |
| 是否限制并发 |
必须 |
TPM 不能代表瞬时容量 |
| 价格如何管理 |
带生效时间的版本 |
支持历史重算 |
| 失败请求是否入账 |
按实际 Usage 入账 |
供应商可能已计费 |
| 自托管如何计价 |
GPU 成本折算 |
支持真实成本比较 |
| 策略如何作用于路由 |
预算压力作为软信号 |
安全和质量底线不变 |
总结
传统限流关心的是:
请求来了多少次?
大模型平台还必须关心:
输入了多少 Token?
准备输出多少 Token?
当前同时有多少生成流?
本次请求预计花多少钱?
用户、项目和租户还剩多少额度?
供应商和 GPU 是否还有容量?
生产级治理应形成以下闭环:
请求前估算
→ 多层额度原子预占
→ Token 与并发准入
→ 模型执行
→ 收集真实 Usage
→ 释放或补扣
→ 写入成本账本
→ 预算告警和路由联动
最重要的十二条原则是:
- QPS 必须保留,但不能单独使用;
- 输入和输出 Token 应分别治理;
- TPM 与并发必须同时限制;
- 配额应覆盖租户、项目、Agent 和用户;
- 请求开始前必须预占;
- 输出预占应结合历史分布,而不是永远使用最大值;
- Redis 原子操作负责在线准入;
- PostgreSQL 或分析存储负责长期账本;
- 流式取消和异常也必须结算;
- 重试产生的真实 Usage 不能丢失;
- 自托管 GPU 也必须计算成本;
- 预算使用率应反馈给多模型路由。
最终目标不是单纯减少调用,而是:
在不突破容量和财务边界的前提下,把有限的 Token 和 GPU 资源分配给最有价值的请求。
在 云栈社区,我们持续探讨这类AI基础设施落地的“硬骨头”,从技术选型到成本控制,陪伴技术人一起成长。下一篇将继续讨论:
大模型调用太贵?Prompt Cache、Semantic Cache 与响应缓存到底能省多少,又有哪些结果绝对不能复用。
参考资料
- OpenAI, Rate limits
https://platform.openai.com/docs/guides/rate-limits
- OpenAI, Project rate limits API
https://platform.openai.com/docs/api-reference/project-rate-limits
- OpenAI, Production best practices
https://platform.openai.com/docs/guides/production-best-practices
- OpenAI, Cost optimization
https://platform.openai.com/docs/guides/cost-optimization
- Anthropic, Rate limits
https://docs.anthropic.com/en/api/rate-limits
- Anthropic, Token counting
https://docs.anthropic.com/en/docs/build-with-claude/token-counting
- Anthropic, Pricing
https://docs.anthropic.com/en/docs/about-claude/pricing
- Amazon Bedrock, Quotas
https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html
- Amazon Bedrock, How tokens are counted
https://docs.aws.amazon.com/bedrock/latest/userguide/quotas-token-burndown.html
- Amazon Bedrock, Count tokens
https://docs.aws.amazon.com/bedrock/latest/userguide/count-tokens.html
- Envoy AI Gateway, Usage-based Rate Limiting
https://aigateway.envoyproxy.io/docs/0.5/capabilities/traffic/usage-based-ratelimiting/
- Envoy AI Gateway, Reference Architecture
https://aigateway.envoyproxy.io/blog/envoy-ai-gateway-reference-architecture