线上最容易把流式输出做坏的,不是“能不能流出来”,而是流量一上来就抖:token 断片、客户端卡死、超时雪崩、重试风暴。
先定目标:稳态优先,不追求单次极限
生产里要先保证三件事:
- 可持续吞吐:高峰时不把上游和自己打爆。
- 可恢复:中断后能续传或重建上下文,避免整段丢失。
- 可观测:能快速定位是网络、模型、还是你自己的缓冲逻辑有问题。
建议 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 结构中途断裂
处理建议:
- 维护
[]byte原始缓冲,先做 UTF-8 校验后再转字符串。 - 对结构化输出(JSON)做“增量可解析检查”。
- 每 N 个 chunk 生成 checkpoint(offset + hash),便于中断恢复。
if !utf8.Valid(buf) {
// 等待更多 chunk 再解码,避免脏字符
continue
}
超时预算闭环:不要只设一个总超时
把超时拆成分层预算:
dial_timeout: 1stls_handshake_timeout: 1sfirst_token_timeout: 3sstream_idle_timeout: 8stotal_timeout: 45s
关键点:idle timeout 必须独立。很多流不是总时长超时,而是中途静默太久。
重试策略:只重试“可重试段”
流式场景最忌讳无脑全量重试。
- 仅对网络抖动、429、短暂 5xx 做指数退避重试。
- 每次重试带上幂等键(request-id)与 checkpoint。
- 超过重试预算后快速失败,避免把系统拖进风暴。
建议参数:
- max_retries: 2~3
- base_backoff: 300ms
- jitter: 20%~30%
观测与告警:至少要打这 8 个指标
stream_first_token_latency_msstream_chunk_gap_msstream_bytes_totalstream_abort_client_slow_totalstream_retry_totalstream_resume_success_totalstream_timeout_idle_totalstream_error_rate
先把指标打全,再谈优化。没有观测,调优基本是盲飞。
最小落地清单(可直接执行)
- 为每条流连接设置最大缓冲和慢客户端保护
- 实现 UTF-8 安全分片重组
- 引入 checkpoint(offset/hash)用于续传
- 分层超时预算(含 idle timeout)
- 只对可重试错误做有限重试
- 接入流式关键指标与告警阈值
总结
OpenAI Responses 流式输出要稳定,核心不是“快”,而是控流 + 可恢复 + 可观测三件套。先把背压、重组、超时预算做成闭环,再去追求吞吐和成本优化,线上会省很多坑。