线上最可怕的不是一次失败,而是失败后被重试放大

在 OpenAI Responses + Go 的工具调用链路里,如果没有幂等键、退避抖动和熔断阈值,10 个请求很快就能打成 1000 个下游调用,账单和延迟一起爆炸。

先给结论:三道闸门缺一不可

  1. 幂等键:同一业务动作只能“生效一次”。
  2. 退避 + 抖动:失败重试要“错峰”,避免同步风暴。
  3. 熔断阈值:错误率超线就快速失败,给系统喘息窗口。

典型事故链路(为什么会风暴)

常见错误配置:

  • HTTP client timeout 过短(例如 3s)
  • 网关重试 3 次 + 业务层再重试 3 次
  • 工具执行无幂等控制(重复写库/重复扣费)
  • 所有实例固定间隔重试(无抖动)

最终效果:

  • 上游抖一下,下游被放大 9~27 倍
  • P95 延迟升高,队列堆积
  • 告警同时覆盖 API 错误、DB 锁冲突、缓存击穿

Go 实战:幂等键设计

推荐键结构:

idem:{tenant}:{workflow}:{biz_id}:{step}

要求:

  • 由业务唯一字段构成(不要用随机 UUID)
  • TTL 覆盖“最大重试窗口”(例如 15 分钟)
  • 幂等记录至少包含:状态、响应摘要、首次时间、最后更新时间

Redis 示例(SETNX + TTL):

ok, err := rdb.SetNX(ctx, idemKey, "PENDING", 15*time.Minute).Result()
if err != nil {
    return err
}
if !ok {
    // 已存在:直接读取之前结果,避免重复执行
    return ErrDuplicateSuppressed
}

工具成功后写入结果摘要:

_ = rdb.Set(ctx, idemKey, "DONE:tool_result_hash", 15*time.Minute).Err()

Go 实战:指数退避 + Full Jitter

错误做法:固定 500ms 重试。

正确做法:指数退避 + 抖动(Full Jitter):

func backoff(attempt int, base, cap time.Duration) time.Duration {
    // base * 2^attempt, capped
    max := base << attempt
    if max > cap {
        max = cap
    }
    // full jitter: [0, max)
    return time.Duration(rand.Int63n(int64(max)))
}

建议参数(保守):

  • base = 200ms
  • cap = 5s
  • maxAttempts = 4
  • 仅对可重试错误(429/5xx/网络瞬断)生效

Go 实战:熔断阈值(错误预算优先)

按 30 秒滑动窗口统计:

  • 请求量 >= 50
  • 错误率 >= 25%
  • 连续触发 2 个窗口 → 熔断 20 秒

伪代码:

if window.Req >= 50 && window.ErrRate() >= 0.25 {
    breaker.Trip(20 * time.Second)
}
if breaker.Open() {
    return ErrFastFail
}

配合降级策略:

  • 返回缓存摘要或上次成功结果
  • 非关键工具调用直接跳过
  • 向用户明确提示“结果可能不完整”

观测指标(必须落地)

至少打这 6 个指标:

  • tool_call_total{tool,status}
  • retry_total{reason}
  • idempotency_suppressed_total
  • breaker_open_total
  • llm_latency_ms_p95
  • cost_usd_total

告警建议:

  • 5 分钟内 retry_total 较基线上涨 > 3 倍
  • idempotency_suppressed_total 突增(说明上游重复提交)
  • breaker_open_total 持续 > 0(说明系统在硬降级)

排障清单(可直接照着查)

  1. 查最近 15 分钟 429/5xx 比例。
  2. 核对是否出现“双层重试”(网关 + 代码)。
  3. 抽样 20 条失败请求,确认幂等键是否稳定。
  4. 检查重试是否带 jitter(不是固定 sleep)。
  5. 看熔断是否触发、是否自动半开恢复。
  6. 对账:重复调用是否造成重复写入/重复扣费。

总结

重试不是免费午餐。

在 Responses + Go 的生产链路里,先做幂等,再做抖动重试,最后加熔断阈值,这套组合能把“雪崩”变成“可控抖动”。

如果你现在只能做一件事:先把幂等键补上。它通常是 ROI 最高的一刀。