线上最容易把流式输出做坏的,不是“能不能流出来”,而是流量一上来就抖:token 断片、客户端卡死、超时雪崩、重试风暴。

先定目标:稳态优先,不追求单次极限

生产里要先保证三件事:

  1. 可持续吞吐:高峰时不把上游和自己打爆。
  2. 可恢复:中断后能续传或重建上下文,避免整段丢失。
  3. 可观测:能快速定位是网络、模型、还是你自己的缓冲逻辑有问题。

建议 SLO:

  • p95 首 token 延迟 < 2.5s
  • p99 流中断恢复时间 < 8s
  • 流式请求错误率 < 1%

架构基线:三段式流控

把链路拆成 3 段,各自限流:

  • 入口层(HTTP/API Gateway):并发阈值 + 全局速率限制
  • 应用层(你的服务):连接池、背压队列、超时预算
  • 下游层(模型 API):配额/429/5xx 的退避重试

这样故障不会一次性穿透整条链路。

背压控制:别让慢客户端拖垮全场

常见事故:服务端一直读模型流,客户端消费慢,内存队列越堆越大。

推荐策略:

  • 每个流连接设定最大缓冲(例如 256KB~1MB)
  • 超过阈值触发策略:
    • 优先:暂停从上游读取(可行时)
    • 其次:丢弃低价值增量(仅保留最终完整段)
    • 最后:主动断流并返回可重试错误

Go 伪代码:

const maxBuf = 512 * 1024

if conn.BufferedBytes() > maxBuf {
    metrics.Inc("stream_backpressure_trigger")
    return ErrClientTooSlow
}

分片重组:把“增量 token”变成“可验证文本”

流式返回常是碎片,直接拼接容易出现:

  • UTF-8 半个字符被切断
  • markdown/code fence 不闭合
  • JSON 结构中途断裂

处理建议:

  1. 维护 []byte 原始缓冲,先做 UTF-8 校验后再转字符串。
  2. 对结构化输出(JSON)做“增量可解析检查”。
  3. 每 N 个 chunk 生成 checkpoint(offset + hash),便于中断恢复。
if !utf8.Valid(buf) {
    // 等待更多 chunk 再解码,避免脏字符
    continue
}

超时预算闭环:不要只设一个总超时

把超时拆成分层预算:

  • dial_timeout: 1s
  • tls_handshake_timeout: 1s
  • first_token_timeout: 3s
  • stream_idle_timeout: 8s
  • total_timeout: 45s

关键点:idle timeout 必须独立。很多流不是总时长超时,而是中途静默太久。

重试策略:只重试“可重试段”

流式场景最忌讳无脑全量重试。

  • 仅对网络抖动、429、短暂 5xx 做指数退避重试。
  • 每次重试带上幂等键(request-id)与 checkpoint。
  • 超过重试预算后快速失败,避免把系统拖进风暴。

建议参数:

  • max_retries: 2~3
  • base_backoff: 300ms
  • jitter: 20%~30%

观测与告警:至少要打这 8 个指标

  1. stream_first_token_latency_ms
  2. stream_chunk_gap_ms
  3. stream_bytes_total
  4. stream_abort_client_slow_total
  5. stream_retry_total
  6. stream_resume_success_total
  7. stream_timeout_idle_total
  8. stream_error_rate

先把指标打全,再谈优化。没有观测,调优基本是盲飞。

最小落地清单(可直接执行)

  • 为每条流连接设置最大缓冲和慢客户端保护
  • 实现 UTF-8 安全分片重组
  • 引入 checkpoint(offset/hash)用于续传
  • 分层超时预算(含 idle timeout)
  • 只对可重试错误做有限重试
  • 接入流式关键指标与告警阈值

总结

OpenAI Responses 流式输出要稳定,核心不是“快”,而是控流 + 可恢复 + 可观测三件套。先把背压、重组、超时预算做成闭环,再去追求吞吐和成本优化,线上会省很多坑。