大模型 API 的限流、重试和降级怎么做?
一句话回答
供应商通常按每分钟请求数和每分钟 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 自己的错误类型。
// 假设 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 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。