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 串行化。