SSE 流式调试:从"没有输出"到"块乱序"的排查路径
流式不通有七种原因,从代理缓冲到 nginx 配置、从 SDK 超时到中转站伪流式,按这个顺序查最快。
流式输出出问题时,表现通常只有一句话:"没有输出"或者"卡住了"。但原因可能在不同层:客户端、代理、中转站、上游。下面按"从近到远"的顺序排了一条排查路径,每一步都能排除一层。
第一步:确认请求本身是流式的
curl -sS -N "https://relay.example.com/v1/chat/completions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","stream":true,"messages":[{"role":"user","content":"数到 20"}]}'
-N 是关键,它关掉 curl 的输出缓冲。少了这个参数,你会以为没有流式,其实是 curl 攒着没打印。
如果这一步能看到多个 data: 行陆续出现,说明服务端是流式的,问题在客户端。如果一次性全部出现,可能是伪流式或代理缓冲。
第二步:看是不是代理缓冲
最常见的"本地好好的,线上没有流式"就是这一条。中间的反向代理(nginx、Cloudflare、公司网关)默认会缓冲响应,等整个响应结束才转发。
nginx 需要:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
Cloudflare 对流式响应一般是透传的,但如果开了某些优化功能,也可能缓冲。测试方法:绕过代理直连源站,如果直连有流式、走代理没有,问题就在代理。
第三步:看客户端有没有读完
有些 HTTP 客户端默认会等响应体完整再返回。Python 的 requests 要设 stream=True 并逐行迭代;Node 的 fetch 要读 response.body 的 reader。
import requests, json
with requests.post(url, headers=h, json=payload, stream=True, timeout=60) as r:
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
data = line[6:]
if data == "[DONE]":
break
chunk = json.loads(data)
delta = chunk["choices"][0]["delta"].get("content")
if delta:
print(delta, end="", flush=True)
注意 flush=True。少了它,Python 会把输出攒在缓冲区,看起来像没流式。
第四步:看超时设置
流式请求的总时长可能很长(生成 2000 字要几十秒)。如果客户端设了 30 秒总超时,会在流到一半时断开,报连接错误。
正确做法是设读超时而不是总超时:只要还在收数据,就不算超时。requests 的 timeout=(connect, read) 里,read 是两次数据之间的最大间隔。
第五步:看是不是中转站伪流式
前面说过,伪流式的特征是所有块几乎同时到达。判断方法:
curl -sS -N ... | while IFS= read -r line; do
printf '%s %s\n' "$(date +%s.%N)" "$line"
done | head -20
看每行前面的时间戳。真流式的间隔是几十到几百毫秒;伪流式会是同一时刻的一串。
第六步:看块的内容是否完整
有时候流是通的,但内容被截断。原因可能是:
max_tokens到了上限(看finish_reason是不是length)- 上游断开,站点没有补发结束事件
- 代理的缓冲区大小限制,超长响应被切
检查最后一个块有没有 finish_reason。没有的话,这个流是不完整的,不能当成正常结束。
第七步:看错误发生在流的中间还是开头
- 开头就错:鉴权、模型名、参数问题,返回的是普通 JSON 错误,不是流。
- 中间断开:上游超时、站点重启、网络抖动。这类错误在 SSE 里通常表现为连接直接关闭,没有
data: [DONE]。
对中间断开的处理:客户端要能检测"没有收到结束标记",并决定是重试还是报错。流式请求重试有重复计费的风险,如果流已经输出了一部分内容,重试可能让用户看到重复内容。
一个排查顺序表
| 现象 | 最可能的原因 | 验证方法 |
|---|---|---|
完全没有 data: 行 | 请求不是流式,或代理缓冲 | curl -N 直连 |
| 一次性出现全部内容 | 代理缓冲或伪流式 | 加时间戳看间隔 |
| 前几块正常,后面卡住 | 读超时太短 | 加长 read timeout |
| 流中间断开无结束标记 | 上游超时/重启 | 看站点状态页 |
| 块内容重复 | 客户端重试了 | 查客户端重试逻辑 |
| 有流但解析报错 | 事件格式不是 OpenAI 风格 | 打印原始行 |
最后
本站检测台的第七项检查会统计 SSE 块数和首 token 时间。多块且首 token 可测记 PASS,只有单块记 WATCH(疑似伪流式),没有内容块记 FAIL。这项检查目前覆盖 OpenAI Chat 和 Anthropic 两种协议。