遇到 Grok 4.5 流式返回中途停住,既没有异常也没有结束标记时,不要先站队查网络或查响应头,而是把链路拆成三段:服务端是否发了结束信号、中间层是否维持了分块传输、客户端是否按块读到超时。网络层的重置、代理缓冲、读取循环提前退出,表现都可能像“断流”。先记录已收长度和响应头,再用非流式同一请求对照,最后才调超时与重试。没有日志和头部证据前,直接换网络或加长超时容易掩盖问题。
流式输出中途停住时,先看响应头里的传输编码、连接状态和结束方式,判断是服务端主动结束还是连接被中断;同时记录断流时间、已收字节和最后一块内容。若响应头正常但客户端没有收到结束标记,优先查逐块读取循环和读超时;若非流式同一请求也失败,再查鉴权、参数、出口网络和服务可用性。边界是:这些步骤只能定位方向,不能替代对具体接口和中间代理的核实。
记录断流时间点和已收到的内容长度
这一步的目标是判断断流发生在固定长度处还是随机位置:固定长度更像读取缓冲区、代理限制或服务端分块边界;随机位置更像网络抖动、读超时或上游偶发结束。每次请求至少记录:开始时间、断流时间、已收字节数、断流前最后一段内容;有请求 ID 或事件 ID 也一并留下。可以先在本地用脚本包一层,不依赖界面提示。
import time, requests
start = time.time()
received = b''
last_text = ''
try:
with requests.post(url, json=payload, stream=True, timeout=(10, 30)) as r:
for chunk in r.iter_content(chunk_size=None):
if chunk:
received += chunk
last_text = chunk.decode('utf-8', 'ignore')[-200:]
print('chunk', round(time.time()-start, 3), len(received), repr(last_text))
except Exception as e:
print('断流时间', round(time.time()-start, 3))
print('已收字节数', len(received))
print('最后内容', repr(last_text))
print('错误', repr(e))
替换 url、payload 和超时值即可运行。如果多次断流都停在同一字节数附近,重点查响应头和中间层;如果每次长度和时间都不同,再优先看网络与读超时。
检查响应头与状态码确认连接被谁结束
响应头能区分“服务端正常收尾”和“连接被掐断”。先抓完整响应头,不要只看界面。需要看 Content-Type、Transfer-Encoding、Connection、Content-Length、Cache-Control,以及是否存在 Trailer 或自定义结束标记。用 curl 可以先拿到头部和原始流:
curl -N -i -X POST 'https://example.invalid/stream' -H 'Content-Type: application/json' -H 'Accept: text/event-stream' `--data-binary` @payload.json | tee stream.log
把真实地址和请求体替换成你的 Grok 4.5 接入参数。下面这些组合只作方向判断,最终要结合中间代理和服务端实现确认。
- 状态码 200,Content-Type 为 text/event-stream,Transfer-Encoding 为 chunked:流式通道已建立。中途停时查看有没有结束事件、[DONE] 或 chunked 终止块;有正常终止块但内容不全,偏服务端逻辑;没有终止块就断开,偏连接中断或客户端读退出。
- 状态码 200,Content-Type 为 application/json,且有 Content-Length:更像普通整包响应,不是逐块流。客户端可能一直等到整包或超时,中途停止要查网关缓冲、请求总时长和内容长度。
- 状态码 200,无 Transfer-Encoding: chunked,但有 Connection: close:可能是服务端或代理在发完后直接关闭。没有结束标记就关闭时,不能只凭状态码 200 判断成功。
- 状态码 499、502、504 或 5xx:连接结束点更可能在网关、代理或上游服务,先查中间层日志和上游耗时,再查本地网络。
- Cache-Control 缺少 no-cache、no-transform,或中间层开启缓冲:分块可能被攒成整包,表现为长时间无数据后一次性到达,或在缓冲边界断流。
如果响应头正常、状态码也正常,但客户端没有看到结束事件,下一步不要继续猜网络,先加逐块读取日志。
在客户端加逐块读取和超时日志
逐块日志要回答两个问题:每一块什么时候到,以及最后是读到空、抛超时,还是读循环自己退出。下面是最小骨架,重点是逐块追加内容、记录每块时间、超时后打印已收内容。连接超时和读超时分开设置,读超时按业务允许的最长静默间隔给,不要用总超时一把抓。
import time, requests
start = time.time()
buf = []
last = start
try:
with requests.post(url, json=payload, stream=True,
timeout=(10, 30)) as resp:
print('status', resp.status_code)
print('headers', dict(resp.headers))
for chunk in resp.iter_content(chunk_size=1024):
now = time.time()
if chunk:
buf.append(chunk)
print('chunk at', round(now-start, 3),
'gap', round(now-last, 3),
'total', sum(len(x) for x in buf))
last = now
else:
print('empty chunk at', round(now-start, 3))
except requests.exceptions.ReadTimeout as e:
print('ReadTimeout, 已收内容:')
print(b''.join(buf).decode('utf-8', 'ignore'))
except Exception as e:
print('中断:', repr(e))
print('已收内容:')
print(b''.join(buf).decode('utf-8', 'ignore'))
看日志时:块间隔均匀但突然 ReadTimeout,偏服务端停止发送或中间层静默断开;间隔忽大忽小且最终超时,偏网络慢或服务端排队;很快 EOF 且没有结束事件,偏服务端关闭或客户端读条件写错;一直接不到任何块但状态码正常,查代理是否缓冲。日志里保留 gap 和 total,复测时才有对照。
用非流式的同一请求做对照
同一 URL、同一鉴权头、同一请求体,只把 stream 参数改为 false 或用接口提供的非流式开关,记录状态码、耗时、响应体长度和错误类型。目标是判断问题只出现在流式路径,还是请求本身就失败。若接口不支持非流式,可以用最小请求核对鉴权、网络出口和服务可用性,但不要把它当成流式路径的等价对照。
| 对照结果 | 指向的排查方向 |
|---|---|
| 流式失败,非流式成功 | 流式路径:响应头、分块传输、代理缓冲、客户端逐块读取和读超时。 |
| 两者都失败 | 请求本身:鉴权、参数、配额、出口网络、DNS、TLS、服务端可用性。 |
| 两者都成功,流式偶发停 | 偶发网络或服务端长尾:保留逐块日志和断流字节数,调超时与重试。 |
| 非流式也失败且耗时接近超时 | 链路最大时长限制:网关、反向代理、客户端超时层层缩短。 |
调整超时与重试后复测
超时取值可以用保守思路:连接超时短一些,读超时覆盖正常最长静默间隔,总时长设上限,避免一个请求无限挂着。重试次数不必多,配合退避即可;偶发断流时,目标是保住已经收到的内容,而不是把整段结果丢掉。重试前先判断是否可以续接:如果服务端支持事件 ID、游标或 offset,就带上最后已收位置;如果不支持,不要假装能续传,先考虑改用非流式、分页或让服务端补齐,否则重发整请求可能得到重复内容。
last_event_id = None
for attempt in range(3):
headers = {}
if last_event_id:
headers['Last-Event-ID'] = last_event_id
try:
with requests.post(url, json=payload, headers=headers,
stream=True, timeout=(10, 30)) as r:
finished = False
for event in parse_sse(r):
if event.id:
last_event_id = event.id
append_text(event.data)
if event.done:
finished = True
break
if finished:
break
except requests.exceptions.ReadTimeout:
time.sleep(2 ** attempt)
parse_sse 和 append_text 需要按你的客户端补实现,Last-Event-ID 也不是所有服务端都支持。复测时继续记录开始时间、断流时间、已收字节数、最后一段内容,并和非流式对照结果放在一起看。只有当日志显示断流位置随机、响应头没有异常结束、非流式请求正常时,才更倾向把超时和重试作为主要处理方向。