
字节笔记本
2026年10月24日 · 约 15 分钟读完
重试别只加 sleep:指数退避与 equal jitter

本文是《从 0 写 Agent》系列第 22 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「Retry:指数退避与 equal jitter」。
模型 API 不是 100% 可靠,会超时、会 429 限流、会 500。没有重试机制,一次抖动就让 Agent 失败。
agents-go 的
NewRetryModel装饰器用几何退避 + equal jitter 算法重试。这一章拆这个算法,以及一个关键约束,流式重试只在"首 event 前"能做。
引子:模型 API 的不可靠
调 OpenAI/Anthropic API,你会遇到:
- 429 Too Many Requests,限流,稍后再试
- 500/502/503,服务端临时故障
- 超时,网络抖动或服务端慢
- 连接重置,网络问题
这些都是"临时性错误",等一下再试就好了。 但如果你不重试,用户看到的就是"Agent 挂了"。生产环境必须有重试。

原理:为什么不能"立即重试"
最朴素的重试,失败就立即重试。但这有个大问题,惊群。
想象 OpenAI 刚限流了 1000 个用户。这 1000 个用户的 Agent 都立即重试 → 1000 个请求同时打过去 → OpenAI 又限流 → 又同时重试 → 永远恢复不了。
解法是"退避",每次失败,等更久再试。 让负载逐渐恢复,而不是持续冲击。
几何退避
最常用的退避,几何增长。每次失败,等待时间翻倍:
第1次失败 → 等 500ms → 重试
第2次失败 → 等 1000ms → 重试
第3次失败 → 等 2000ms → 重试
第4次失败 → 等 4000ms → 重试
...公式:delay = base * multiplier^(attempt-1),其中 base=500ms,multiplier=2。
这让负载"逐渐"恢复,第一次冲击后等 500ms,给服务端喘息;还不行就等 1s,更多喘息。
Equal Jitter,防止同步
但几何退避还有个问题,所有客户端的退避节奏一样。1000 个用户都在第 1 次失败后等 500ms,500ms 后又同时重试。
Equal Jitter 的解法,在退避时间上加随机抖动:
agents-go 的做法(model_retry.go:128):
half := d / 2
delay = half + rand.Float64() * half // 落在 [d/2, d]延迟落在 [d/2, d] 区间,每个客户端等不同的时间,避免同步冲击。这叫"equal jitter"(一半固定 + 一半随机)。
源码对照:RetryPolicy 和 backoff
RetryPolicy(model_retry.go:15)配置重试行为:
// agents/model_retry.go:15
type RetryPolicy struct {
MaxAttempts int // 总尝试次数(含首次),默认 3
BaseDelay time.Duration // 第二次尝试前的等待,默认 500ms
MaxDelay time.Duration // 等待上限,默认 30s
Multiplier float64 // 几何增长因子,默认 2
RetryIf func(error) bool // 哪些错误该重试
RetryAfter func(error) (time.Duration, bool) // 从错误里提取服务端建议的延迟
Sleep func(ctx, d) error // 测试可注入假实现
}backoff(model_retry.go:111)是核心算法:
// agents/model_retry.go:111(骨架)
func (p RetryPolicy) backoff(attempt int) time.Duration {
base := 500 * time.Millisecond // 默认
limit := 30 * time.Second // 默认上限
mult := 2.0 // 默认乘数
d := float64(base) * math.Pow(mult, float64(attempt-1)) // 几何增长
if d > float64(limit) {
d = float64(limit) // 封顶
}
half := d / 2
return time.Duration(half + rand.Float64()*half) // equal jitter
}四步,几何增长、封顶、分半、随机抖动。 注意 math.Pow(mult, attempt-1),第 1 次重试是 mult^0=1(base delay),第 2 次是 mult^1(翻倍),依此类推。
MaxDelay 封顶的重要性
没有 MaxDelay,几何增长会失控,第 10 次重试等 500ms * 2^9 = 256 秒。用户等不了这么久。MaxDelay(默认 30s)保证最坏情况等待可控。
设计权衡:哪些错误该重试
DefaultRetryIf(model_retry.go:84)的默认策略:
// agents/model_retry.go:84
func DefaultRetryIf(err error) bool {
if err == nil { return false }
// context 取消/超时不重试(重试没意义,调用方已经放弃了)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
return true // 其他都重试
}默认重试一切,除了 context 取消。 为什么?
- context 取消意味着调用方主动放弃了,重试没意义(结果没人要了)
- 其他错误(429/500/超时/网络)都可能是临时的,值得重试
但这个默认可能太激进,比如 400 Bad Request(参数错误)重试 100 次还是 400。所以 agents-go 允许自定义 RetryIf:
// 用 OpenAI 专用的错误分类:只重试 429/5xx,不重试 4xx
policy := agents.RetryPolicy{
RetryIf: openai.RetryableError, // 429/5xx 才重试
}好的重试策略要区分"临时错误"(重试有意义)和"永久错误"(重试浪费)。 默认宽松(全重试),生产环境建议配精确的 RetryIf。
RetryAfter:尊重服务端的建议
有些错误带 Retry-After 头,服务端告诉你"多久后再试"。RetryAfter(model_retry.go:39)提取这个值:
policy := agents.RetryPolicy{
RetryAfter: openai.RetryAfter, // 从 429 的 Retry-After 头提取
}如果服务端建议等 10 秒,就等 10 秒,不用自己算的几何退避。 服务端最清楚自己什么时候能恢复。wait 函数(model_retry.go:134)优先用 RetryAfter,没有才用 backoff。
设计权衡:流式重试的"首 event 前"约束
这是 retry 最微妙的设计。回忆第 2 章,流式调用一旦吐过 token,就不能重试。原因:重试会让模型从头生成,已经吐给用户的 token 和新生成的不一致。
agents-go 的流式重试(model_retry.go:194)用 producedAny 标志实现这个约束:
// agents/model_retry.go:194(骨架)
producedAny := false
for event, err := range inner.StreamResponse(ctx, req) {
if err != nil && !producedAny {
// 还没吐过 event,可以重试
if shouldRetry {
continue // 重新开始流式
}
}
producedAny = true // 吐过 event 了
yield(event, err) // 后续错误直通,不重试
}producedAny 标志,一旦为 true(吐过 event),后续错误直通不重试。
这是工程纪律,"宁可让用户看到一个失败,也不要给他看一个错乱的重试结果"。 流式场景下,部分输出比"看起来成功但内容变了"更诚实。
自定义 JSON 编解码:配置友好
注意 RetryPolicy 有自定义的 MarshalJSON/UnmarshalJSON(model_retry.go:59-80),把 time.Duration 转成毫秒整数:
type retryPolicyJSON struct {
MaxAttempts int `json:"max_attempts"`
BaseDelayMs int `json:"base_delay_ms"` // 毫秒整数
MaxDelayMs int `json:"max_delay_ms"`
Multiplier float64 `json:"multiplier"`
}为什么? 因为 time.Duration 的默认 JSON 序列化是纳秒整数(500000000 表示 500ms),对人不可读。转成毫秒(500)后,配置文件里写 "base_delay_ms": 500 一目了然。
这让 RetryPolicy 可以直接从配置数据库 json.Unmarshal,agents-server(Part VI)就是这么存重试配置的。工程细节,但很重要。
RetryModel 是装饰器
NewRetryModel(model_retry.go:170)返回的还是 Model 接口,它包装另一个 Model,给它加重试能力:
// 用法
baseModel := openai.NewProvider().GetModel("gpt-5")
retryModel := agents.NewRetryModel(baseModel, agents.RetryPolicy{
MaxAttempts: 5,
})
// retryModel 还是 Model 接口,Agent 循环无感知
agents.Run(ctx, agent, input, agents.RunOptions{
Model: retryModel,
})装饰器模式,接口不变,能力增加。 这让 retry 可以和 Fallback(第 23 章)、Router(第 21 章)任意组合。第 24 章会讲怎么组合。
小结
读完这一章,你应该理解:
- 为什么要退避,防惊群,让服务端有机会恢复
- 几何退避,
delay = base * mult^(attempt-1),翻倍增长 - Equal Jitter,
half + rand*half,落在[d/2, d],防同步 - MaxDelay 封顶,防失控,默认 30s
- DefaultRetryIf,重试一切除了 context 取消
- 流式"首 event 前"约束,吐过 token 不重试,避免内容错乱
- 装饰器模式,RetryModel 实现同一接口,可任意组合
Retry 让模型调用"容忍临时故障"。 但如果某个模型彻底挂了(不是临时抖动),retry 也没用,这时需要 Fallback(降级到备用模型)。下一章讲。
下一篇
第 23 章,Fallback:模型降级链。当主模型彻底不可用,自动切到备用模型。agents-go 的推荐组合,"每个模型先包 Retry,再组 Fallback",让每个 provider 先耗尽自己的重试,再降级。
本文是《从 0 写 Agent》系列第 22 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。



