18.11-模型调用限流
要点
- 不同 LLM 模型成本差异巨大——Claude Opus 4.6 比 Haiku 4.5 贵 10 倍以上,不能按调用次数一刀切
- 模型级限流要按 token 数计费——输入和输出 token 价格不同,要分开算
- 分级限流:不同套餐的用户能用不同档次的模型(free 用开源,pro 用 Claude Haiku,enterprise 用 Claude Opus)
- 成本预算控制:设定月度预算上限,超过后降级到便宜模型或拒绝请求
- Token 计数要准确——调用前估算输入 token,调用后记录实际消耗
- 实现:KV 做高速计数,D1 做持久化月度统计
1. 为什么模型调用要单独限流
前面的用户级限流按「调用次数」计算——每分钟 100 次、每天 1000 次。但 LLM 调用有个问题:不同调用的成本差异极大。
| 模型 | 输入价格/M token | 输出价格/M token |
|---|---|---|
@cf/meta/llama-3.1-8b-instruct | 免费 | 免费 |
| Claude Haiku 4.5 | $1 | $5 |
| Claude Sonnet 4.6 | $3 | $15 |
| Claude Opus 4.6 | $15 | $75 |
一个用户每天 1000 次 Haiku 调用成本约 $3,1000 次 Opus 调用成本约 $30——10 倍差距。按调用次数一刀切,用户可以全部用 Opus 把预算吃光。
更公平的做法:按 token 数计费,不同模型不同单价。
2. Token 计数
2.1 调用前估算输入 token
输入 token 数可以估算——大约 1 个英文字符 = 0.25 token,1 个中文字符 = 1-2 token。
function estimateTokens(text: string): number {
// 粗略估算:英文字符 / 4 + 中文字符 × 1.5
const english = text.replace(/[^\x00-\x7F]/g, '')
const chinese = text.replace(/[\x00-\x7F]/g, '')
return Math.ceil(english.length / 4 + chinese.length * 1.5)
}
function estimateMessagesTokens(messages: Message[]): number {
return messages.reduce(
(sum, msg) => sum + estimateTokens(msg.content),
0
)
}2.2 调用后记录实际 token
AI API 通常会返回 usage 字段,包含实际消耗:
const result = await env.AI.run(model, {
messages: [{ role: 'user', content: prompt }],
})
// result.usage 包含 token 消耗
const inputTokens = result.usage.prompt_tokens
const outputTokens = result.usage.completion_tokens实际 token 数比估算准确,但调用已经发生了。预估算用于「这次调用前检查配额够不够」,后记录用于「精确扣费」。
3. 成本预算控制
3.1 数据模型
CREATE TABLE model_rates (
model TEXT PRIMARY KEY,
input_rate REAL NOT NULL, -- 每 M token 价格
output_rate REAL NOT NULL -- 每 M token 价格
);
INSERT INTO model_rates VALUES
('@cf/meta/llama-3.1-8b-instruct', 0, 0),
('claude-haiku-4-5', 1, 5),
('claude-sonnet-4-6', 3, 15),
('claude-opus-4-6', 15, 75);
CREATE TABLE user_monthly_usage (
user_id TEXT,
month TEXT, -- '2026-06'
total_cost REAL NOT NULL DEFAULT 0,
cost_limit REAL NOT NULL DEFAULT 10, -- 月度成本上限
PRIMARY KEY (user_id, month)
);3.2 检查成本配额
async function checkCostBudget(
env: Env,
userId: string,
estimatedCost: number
): Promise<{ allowed: boolean; reason?: string }> {
const month = new Date().toISOString().slice(0, 7)
const usage = await env.DB.prepare(`
SELECT total_cost, cost_limit
FROM user_monthly_usage
WHERE user_id = ? AND month = ?
`).bind(userId, month).first()
if (!usage) {
// 新用户,创建记录
await env.DB.prepare(`
INSERT INTO user_monthly_usage (user_id, month, total_cost, cost_limit)
VALUES (?, ?, 0, 10)
`).bind(userId, month).run()
return { allowed: true }
}
if (usage.total_cost + estimatedCost > usage.cost_limit) {
return {
allowed: false,
reason: `Monthly budget exceeded: $${usage.total_cost.toFixed(2)} / $${usage.cost_limit}`,
}
}
return { allowed: true }
}3.3 记录实际消耗
async function recordTokenUsage(
env: Env,
userId: string,
model: string,
inputTokens: number,
outputTokens: number
) {
const rates = await env.DB.prepare(`
SELECT input_rate, output_rate FROM model_rates WHERE model = ?
`).bind(model).first()
if (!rates) return
const cost =
(inputTokens / 1_000_000) * rates.input_rate +
(outputTokens / 1_000_000) * rates.output_rate
const month = new Date().toISOString().slice(0, 7)
await env.DB.prepare(`
INSERT INTO user_monthly_usage (user_id, month, total_cost, cost_limit)
VALUES (?, ?, ?, 10)
ON CONFLICT(user_id, month) DO UPDATE SET
total_cost = total_cost + ?
`).bind(userId, month, cost, cost).run()
}4. 分级模型访问
不同套餐的用户能用不同档次的模型:
const PLAN_MODEL_ACCESS = {
free: ['@cf/meta/llama-3.1-8b-instruct'],
pro: ['@cf/meta/llama-3.1-8b-instruct', 'claude-haiku-4-5', 'claude-sonnet-4-6'],
enterprise: ['@cf/meta/llama-3.1-8b-instruct', 'claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-6'],
}
async function checkModelAccess(
env: Env,
userId: string,
requestedModel: string
): Promise<{ allowed: boolean; reason?: string }> {
const user = await env.DB.prepare(`
SELECT plan_id FROM users WHERE user_id = ?
`).bind(userId).first()
if (!user) return { allowed: false, reason: 'User not found' }
const allowedModels = PLAN_MODEL_ACCESS[user.plan_id as keyof typeof PLAN_MODEL_ACCESS]
if (!allowedModels.includes(requestedModel)) {
return {
allowed: false,
reason: `Model ${requestedModel} not available for ${user.plan_id} plan`,
}
}
return { allowed: true }
}5. 实现:完整的模型限流中间件
// src/middleware/model-rate-limit.ts
export async function modelRateLimit(c: Context, next: Next) {
const userId = c.get('userId')
const body = await c.req.clone().json()
const model = body.model || '@cf/meta/llama-3.1-8b-instruct'
// 1. 检查模型访问权限
const access = await checkModelAccess(c.env, userId, model)
if (!access.allowed) {
return c.json({ error: access.reason }, 403)
}
// 2. 估算成本
const inputTokens = estimateMessagesTokens(body.messages || [])
const estimatedOutputTokens = body.maxTokens || 1000
const estimatedCost = await estimateCost(c.env, model, inputTokens, estimatedOutputTokens)
// 3. 检查成本预算
const budget = await checkCostBudget(c.env, userId, estimatedCost)
if (!budget.allowed) {
return c.json({ error: budget.reason }, 429)
}
// 4. 放行,调用后记录实际消耗
await next()
// 从响应里拿实际 token 数(假设响应里有 usage)
// 这里简化处理——实际需要在 LLM 调用函数里记录
}
async function estimateCost(
env: Env,
model: string,
inputTokens: number,
outputTokens: number
): Promise<number> {
const rates = await env.DB.prepare(`
SELECT input_rate, output_rate FROM model_rates WHERE model = ?
`).bind(model).first()
if (!rates) return 0
return (inputTokens / 1_000_000) * rates.input_rate +
(outputTokens / 1_000_000) * rates.output_rate
}6. 预算耗尽后的降级
成本超限不一定要拒绝——可以降级到便宜模型:
async function handleQuotaExceeded(
env: Env,
userId: string,
requestedModel: string,
body: any
): Promise<Response> {
// 尝试降级到免费模型
const fallbackModel = '@cf/meta/llama-3.1-8b-instruct'
const result = await env.AI.run(fallbackModel, {
messages: body.messages,
})
return Response.json({
answer: result.response,
model: fallbackModel,
downgraded: true,
reason: 'Monthly budget exceeded, using free model',
})
}用户体验比 429 好——至少能拿到回答。
7. 全局成本监控
除了用户级预算,还要监控全局成本——防止某个异常用户或 bug 把整个项目的成本打爆。
// src/lib/cost-monitor.ts
export async function checkGlobalBudget(env: Env): Promise<boolean> {
const today = new Date().toISOString().slice(0, 10)
const totalCost = await env.DB.prepare(`
SELECT SUM(total_cost) as total
FROM user_monthly_usage
WHERE month = ?
`).bind(today.slice(0, 7)).first()
const dailyCost = await env.DB.prepare(`
SELECT SUM(amount) as total
FROM daily_costs
WHERE date = ?
`).bind(today).first()
// 月度预算 $1000
if ((totalCost?.total || 0) > 1000) {
await sendAlert(env, 'Monthly budget exceeded')
return false
}
// 日预算 $50
if ((dailyCost?.total || 0) > 50) {
await sendAlert(env, 'Daily budget exceeded')
return false
}
return true
}Cloudflare AI Gateway(第 19 篇)也提供成本监控功能。
8. 并发限流
除了成本,还要限「同时进行的 LLM 调用数」——模型 API 通常有并发限制。
// 用 Durable Objects 做全局并发计数
export class ConcurrencyLimiter {
private state: DurableObjectState
private current = 0
private max = 100 // 最多 100 个并发调用
constructor(state: DurableObjectState) {
this.state = state
this.state.blockConcurrencyWhile(async () => {
const stored = await this.state.storage.get<number>('current')
if (stored !== undefined) this.current = stored
})
}
async fetch(request: Request) {
if (this.current >= this.max) {
return new Response(JSON.stringify({ error: 'Too many concurrent requests' }), {
status: 429,
})
}
this.current++
await this.state.storage.put('current', this.current)
try {
const response = await fetch(request)
return response
} finally {
this.current--
await this.state.storage.put('current', this.current)
}
}
}9. 小结
模型调用限流的核心:
- 按 token 数计费:不同模型成本差异巨大,调用次数限流不够公平
- Token 计数:调用前估算(用于检查配额),调用后记录实际消耗(用于精确扣费)
- 成本预算:月度成本上限,超过后拒绝或降级
- 分级模型访问:不同套餐能用不同档次的模型
- 降级策略:成本超限后用免费模型兜底,比 429 体验好
- 全局监控:监控项目整体成本,防止被某个用户或 bug 打爆预算
- 并发限流:限制同时进行的 LLM 调用数,保护下游 API
下一篇讲并发控制——信号量、队列缓冲、Worker 实例并发限制、Durable Objects 串行化。