现场是这样的。用 httpx 0.27.2 的 client.stream 调本地 vLLM 的 /v1/chat/completions 跑 qwen2.5-14b,timeout 写成 httpx.Timeout(60),超过 60 秒的长回答必抛 ReadTimeout,服务端日志却显示每秒都在吐 token,换成 requests 逐块读又能跑完。

中间走过弯路,把「中间那层企业代理空闲 60 秒就主动断开连接」当成主因,改完现象没变。

这类报错背后常见的原因有几处,逐个排除就行。read 超时在流式里按相邻两块数据的间隔算,不是整个响应的总耗时;只在构造 Client 时给了总超时,没有单独把 read 这一项放宽;服务端在两段输出之间做长推理,超过 60 秒一个字节都没发。

第一件事是确认超时对象里 read 实际是多少秒

python -c "import httpx;t=httpx.Timeout(60,read=180);print(t.read,t.connect)"

随后把 read 单独放到 180 秒再发同一条长请求

python -c "import httpx;c=httpx.Client(timeout=httpx.Timeout(30,read=180));r=c.post('http://127.0.0.1:8000/v1/chat/completions',json={'model':'qwen2.5-14b','stream':True,'messages':[{'role':'user','content':'写 800 字'}]});print(len(r.text))"

再往下用 curl 加 -N 绕过 SDK 复现一次

curl -N --max-time 600 http://127.0.0.1:8000/v1/chat/completions -H 'Content-Type: application/json' -d @long.json

剩下的就是在请求外层自己记总耗时,和 read 超时分开看

python -c "import time,httpx;t=time.time();httpx.get('http://127.0.0.1:8000/v1/models',timeout=httpx.Timeout(30,read=180));print(round(time.time()-t,1))"

怎么算真的好了,看的是同一批现象。同一条长回答连续跑 5 次都能读完,最长一次首尾间隔 92 秒,客户端不再抛 ReadTimeout,服务端日志里 finish_reason 是 stop。

提醒一点。流式的 read 超时管的是两块数据之间的空档,总时长得在外层自己计时,两个值分开设才不会被长推理误伤。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。

作者 9823189