大模型 API 的限流、重试和降级怎么做?

进阶高频实践约 7 分钟读完

一句话回答

供应商通常按每分钟请求数和每分钟 token 数等维度限流,超出后返回 429。应对分三层:重试只针对限流、5xx、超时这类可恢复的错误,用带抖动的指数退避,响应里有 Retry-After 就按它等待;客户端侧主动控制并发和速率,请求排队,不要等被限流了才处理;降级是主模型持续不可用时切换到备用模型或供应商,再配合熔断,避免一直请求已经故障的服务。超时要分别设置连接、首 token 和两次数据之间的间隔。流式请求输出到一半失败时,不能悄悄重试,要明确告诉用户。

详细解析

哪些错误该重试

情况 是否重试 说明
429 限流 重试 按 Retry-After 或退避时间等待
429 额度或余额用完 不重试 有的供应商也用 429 表示额度耗尽,要看错误类型或错误码区分
500、502、503 等服务端错误 重试 有的供应商用 529 这类非标准状态码表示过载
网络错误、超时 重试 次数要少,超时本身已经等了很久
400、401、403、404 不重试 参数、鉴权、模型名有误,重试结果一样
用户主动取消 不重试 直接结束

指数退避和抖动

  • 第 n 次重试前等待"基础时间 × 2 的 n 次方",设置上限,最多重试 3 次左右
  • 加随机抖动:大量请求同时被限流时,如果都在同一时刻重试,会再一次一起被拒绝。常用"全抖动":在 0 到退避时间之间随机取值
  • 响应头里有 Retry-After 时优先遵守,它可能是秒数,也可能是一个 HTTP 日期
  • 控制重试的总量,比如重试请求不超过正常请求的一小部分,否则故障时重试会把流量放大好几倍

客户端侧的限流和排队

被动地等 429,不如主动控制:

  • 用信号量限制同时进行的请求数,超出的排队等待
  • 按 token 预算做令牌桶:估算每个请求的 token 数,不超过供应商的每分钟限额
  • 多实例部署时,限流的计数放在 Redis 里共享(见怎么用 Redis 实现限流)
  • 区分优先级:用户在线等待的请求优先,批量任务在空闲时执行

超时

  • 不要只设一个总超时:长回答可能正常地生成好几分钟
  • 分开设置:连接超时、首 token 超时(等太久说明在排队或卡住了)、两次数据之间的空闲超时
  • 超时后要中止请求、释放连接,避免上游还在生成

降级和熔断

  • 备用模型:主模型持续失败时,切换到另一个模型或供应商。备用模型要提前用评测集验证,Prompt 可能需要单独适配
  • 功能降级:缩短上下文、关闭工具调用、返回缓存的答案,或者给出友好的提示
  • 熔断:一段时间内失败率超过阈值,就暂停向这个服务发请求,直接走降级;冷却一段时间后放少量请求试探,成功了再恢复。避免每个请求都要等到超时才失败

流式请求中途失败

已经有内容显示给用户时,不能悄悄重试:新生成的内容和已显示的对不上。可以保留已显示的内容,提示"生成中断",并提供"重新生成"按钮;或者清空后重新生成,并明确告诉用户。还没输出第一个 token 就失败的,可以像普通请求一样重试或切换到备用模型。

代码示例

下面是不依赖 SDK 的通用写法。官方 SDK 一般自带重试(次数可以配置),在外面再包一层时要关掉其中一层,否则重试次数会相乘;判断条件也要换成 SDK 自己的错误类型。

JavaScript
// 假设 fn 抛出的错误带有 status、headers,以及供应商的错误类型 type 和错误码 code
async function withRetry(fn, { retries = 3, baseMs = 500, maxMs = 8000 } = {}) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn()
    } catch (err) {
      if (attempt >= retries || !isRetryable(err)) throw err
      const backoff = Math.min(maxMs, baseMs * 2 ** attempt)
      // 有 Retry-After 就按它等;否则在 0 到退避时间之间随机取值(全抖动)
      const delay = parseRetryAfter(err.headers?.get('retry-after')) ?? Math.random() * backoff
      await new Promise((resolve) => setTimeout(resolve, delay))
    }
  }
}

function isRetryable(err) {
  if (err.name === 'AbortError') return false // 用户主动取消
  if (err.name === 'TimeoutError' || err.status === undefined) return true // 超时、网络错误
  // 额度用完的 429 不重试。例如 OpenAI 这类错误的 type 可能是 insufficient_quota,具体字段以文档为准
  if (err.status === 429) return ![err.type, err.code].includes('insufficient_quota')
  return err.status >= 500
}

function parseRetryAfter(value) {
  if (!value) return null
  const seconds = Number(value)
  if (Number.isFinite(seconds)) return seconds * 1000
  const date = Date.parse(value) // 也可能是 HTTP 日期格式
  return Number.isNaN(date) ? null : Math.max(0, date - Date.now())
}

面试官可能追问

为什么要加随机抖动?

限流往往是集中发生的:一大批请求在同一时刻收到 429,如果都固定等 1 秒、2 秒、4 秒再重试,它们会在同样的时间点再次一起涌向服务端,形成一波波尖峰,又一起被拒绝。加上随机抖动,重试的时间被打散,服务端更容易消化。

熔断和重试是什么关系?

重试处理偶发的失败,熔断处理持续的故障。服务已经大面积出错时,每个请求还在重试、等超时,既浪费时间,又加重对方的负担。熔断打开后直接走降级,过一段时间放少量请求试探,恢复正常后再关闭熔断。两者配合使用:熔断关闭时正常重试,打开时直接降级。

切换到备用模型要注意什么?

不同模型对同一个 Prompt 的表现可能差别很大,工具调用、结构化输出的格式也可能不同。备用模型要提前适配 Prompt,并用评测集验证效果。切换时记录日志和指标,主模型恢复后及时切回。多供应商的统一接入和路由见设计一个大模型网关。

易错点

  • 对所有错误都重试,400、401 重试只会浪费时间和额度
  • 没有抖动,大量请求同时重试,形成重试风暴
  • 只设一个总超时,长回答被误杀;或者完全不设超时,请求一直挂着
  • 流式输出到一半失败后自动重试,新内容和已显示的内容拼在了一起

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。