记录一下那天晚上的排查过程。前端接流式问答,服务端每 15 秒发一行冒号注释当心跳,前面 nginx 1.24 的 proxy_read_timeout 是 60 秒,用户提问后模型想了 70 秒才出第一个字,浏览器控制台直接报 net::ERR_INCOMPLETE_CHUNKED_ENCODING。

刚开始盯着「心跳写在模型调用之前,生成阶段被同步阻塞,心跳线程没跑起来」,折腾了半个多小时,排掉了。

把日志和配置对完,能解释这个现象的基本就这几处。注释行不进事件流,某些代理会把它当空行丢弃,连接照样被判定空闲。也可能是proxy_read_timeout 计的是两次读到数据之间的间隔,超时后代理主动断链。

不容易想到的是,响应头缺 Cache-Control: no-cache,中间层缓存了半截响应。

上来先在 nginx 里把读超时和缓冲相关项一起改掉

sed -i 's/proxy_read_timeout 60s;/proxy_read_timeout 300s;/' /etc/nginx/conf.d/ai.conf && nginx -t && nginx -s reload

再往下确认 SSE 响应头与心跳行到底有没有出去

curl -N -i --max-time 20 http://127.0.0.1/v1/chat/completions -H 'Accept: text/event-stream' | head -12

随后在生成期间也保持独立心跳协程不被阻塞

python -c "import asyncio;print(asyncio.get_event_loop().time())"

等前面都确认过,再用浏览器开发者工具看事件流里冒号行的到达时间

curl -N http://127.0.0.1/v1/chat/completions -H 'Accept: text/event-stream' | while read -r l; do date +%T.%N; echo "$l"; done

别只看报错消失,验证要多做一步。问答等待 90 秒仍能收到首个 delta,开发者工具的 EventStream 里冒号行每 15 秒一条且不中断,响应状态码 200 且没有 ERR_INCOMPLETE_CHUNKED_ENCODING。

提醒一点。心跳要发成完整的 SSE 事件而不只是换行,且必须与阻塞的模型调用解耦,否则生成一开始心跳就停了,超时时间再大也没用。如果同一现象隔几天又出现,说明根因没找到,别当偶发处理。

作者 9823189