线上流式生成最怕两件事:用户在等,你的连接先断;日志里报错一堆,你却不知道是哪一层炸了。
这篇给你一个能直接落地的 Go 工程模板:把 OpenAI Responses API 的流式调用做成可超时、可重试、可观测的生产级链路。
1. 先定边界:哪些错误该重试,哪些不该
别把所有错误都扔进重试器。正确做法是分层处理:
- 可重试:429、5xx、网络瞬断、上游网关超时
- 不可重试:401/403、请求参数错误、上下文被主动取消
- 半可重试:流中途断开(要看是否允许“续写”)
如果你把 400 也重试,只会稳定放大故障。
2. Go 客户端超时模型(推荐)
建议用三层超时:
context.WithTimeout:整次请求硬超时(比如 45s)- HTTP Client Timeout:防止连接层悬挂(比如 50s)
- 读取空闲超时:流长时间无 token 时主动中断
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
defer cancel()
httpClient := &http.Client{Timeout: 50 * time.Second}
经验值:HTTP 超时略大于 context 超时,避免“外层还活着,内层先杀”。
3. 指数退避 + 抖动重试(带上限)
核心原则:
- 初始退避 300~500ms
- 指数增长到上限(比如 5s)
- 加 jitter,避免雪崩同步
- 最大重试次数 3~5 次
func backoff(attempt int) time.Duration {
base := 400 * time.Millisecond
max := 5 * time.Second
d := base * time.Duration(1<<attempt)
if d > max {
d = max
}
jitter := time.Duration(rand.Int63n(int64(d / 4)))
return d + jitter
}
4. 流式输出的可观测性:至少打这 6 个指标
无监控的流式就是盲飞。建议最少上报:
llm_stream_first_token_msllm_stream_total_duration_msllm_stream_tokens_inllm_stream_tokens_outllm_stream_retry_countllm_stream_error_total{code,type}
并给每次调用加 trace_id,串起入口请求、模型调用、回包耗时。
5. 生产可用的调用骨架(简化版)
func StreamWithRetry(ctx context.Context, req *Request) error {
var lastErr error
for attempt := 0; attempt < 4; attempt++ {
start := time.Now()
err := streamOnce(ctx, req)
recordMetrics(time.Since(start), attempt, err)
if err == nil {
return nil
}
if !isRetryable(err) {
return err
}
lastErr = err
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(backoff(attempt)):
}
}
return fmt.Errorf("stream failed after retries: %w", lastErr)
}
6. 常见翻车点(你大概率踩过)
- 没有在客户端断开时取消 context,导致 goroutine 泄漏
- 把整段流内容先缓存再返回,失去流式意义
- 只记失败次数,不记首 token 延迟,定位不了“慢”
- 重试不区分错误类型,导致流量自杀
7. 验证清单(上线前 10 分钟)
- 人为注入 429,确认退避与重试生效
- 人为注入 5xx,确认最终失败能被监控捕获
- 人为断网 3 秒,确认可恢复
- 压测并发 50/100,确认 p95 首 token 延迟可接受
总结
想把 Responses API 流式输出跑稳,不是“能连上就行”,而是把三件事做好:
- 错误分层(重试边界)
- 时间分层(超时边界)
- 观测分层(指标与追踪)
先把这三层补齐,再谈模型效果优化,才是工程上不翻车的顺序。