自建 AI 聊天应用时,有一种情况很容易被误判为“模型太慢”:请求返回了 200,页面却一直等待,过了几秒才把整段回答显示出来。
如果应用原本支持逐段输出,这种现象可能发生在后端、Nginx、CDN 或前端。单纯增加服务器配置,未必能解决问题。
在莱卡云 Linux 云服务器上部署 AI 应用,也可以按本文的方法检查:先确认后端是否分段发送,再逐层比较数据到达的节奏。
一、先确认接口用的是 SSE
本文针对通过 HTTP 返回 SSE 事件流的接口,响应类型通常包含:
Content-Type: text/event-stream
一个简单的 SSE 事件如下,末尾需要有空行:
data: {"text":"你好"}
不过,返回这个响应头并不代表程序已经实现流式输出。如果后端先收集完整答案,最后才统一写入响应,浏览器仍然只能一起收到。
也不要把 SSE 与 WebSocket 混为一谈。SSE 不需要 WebSocket 的 Upgrade 握手配置。如果项目实际使用 WebSocket,可以参考本站的 Nginx WebSocket 400/502 排查教程。

二、用一个测试接口建立排查基准
直接排查真实 AI 接口,往往还会涉及模型速度、鉴权、请求参数和应用框架。可以先启动一个简单的测试服务,确认 HTTP 链路能否逐段传输。
下面的脚本使用 Python 3 标准库,不需要模型或 API 密钥。它只监听本机 127.0.0.1:18080,每隔约 1 秒发送一个事件,总共发送 5 个,然后结束响应。
示例假设 Nginx 与脚本运行在同一台主机、同一个网络命名空间。如果 Nginx 在 Docker 容器内,127.0.0.1 指向容器自身,需要按实际网络调整上游地址。
将下面内容保存为 sse-demo.py:
import json
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if self.path != "/sse-test":
self.send_error(404)
return
self.send_response(200)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "close")
self.end_headers()
self.close_connection = True
try:
for seq in range(1, 6):
payload = json.dumps({"seq": seq, "server_time": time.time()})
self.wfile.write(f"data: {payload}\n\n".encode("utf-8"))
self.wfile.flush()
time.sleep(1)
except (BrokenPipeError, ConnectionResetError):
pass
if __name__ == "__main__":
ThreadingHTTPServer(("127.0.0.1", 18080), Handler).serve_forever()
在服务器终端启动,并保持这个终端运行:
python3 sse-demo.py
如果 18080 端口已被占用,请同时修改脚本、下文 Nginx 配置和测试命令中的端口。这个脚本只用于临时诊断,不是生产应用服务器。
三、用 curl 检查后端是否逐段发送

另开一个服务器终端,直接访问测试服务:
curl --noproxy '*' -N -i --max-time 15 \
http://127.0.0.1:18080/sse-test
正常情况下,seq 为 1 到 5 的事件应陆续出现,相邻事件大约间隔 1 秒。观察重点是内容在终端出现的时间,而不是最后是否返回了完整的 5 条数据。
几个参数的作用分别是:
-N:关闭 curl 自己的输出缓冲。-i:同时显示响应头,便于确认状态码和内容类型。--max-time 15:让本次诊断最多运行 15 秒。--noproxy '*':避免本次请求走环境变量配置的客户端代理。
-N 不会让一个非流式后端自动变成流式接口;curl -I 只请求响应头,也不能用来判断正文是否逐段到达。初次排查时,建议直接观察终端,不要再接可能缓冲输出的格式化工具。
如果直连真实后端时,数据就已经成批到达,应先检查模型调用是否启用了流式模式,以及应用是否逐段读取、写出并刷新响应。
注意:这里的 /sse-test 是 GET 测试接口。 测试真实 AI 接口时,需要保留原来的 POST 方法、鉴权头、请求体和流式参数,不能只替换 URL。本文测试接口成功,也不能直接证明另一条业务路由已经配置正确。
四、检查 Nginx 的响应缓冲设置
后端直连正常后,可以在对应域名现有的 Nginx server 块内,临时加入以下测试路由。下例针对通过 proxy_pass 转发的 HTTP 上游,不要用它覆盖整个站点配置:
location = /sse-test {
proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
gzip off;
proxy_read_timeout 120s;
}
这里最关键的是 proxy_buffering off;,它关闭该路由的上游响应缓冲。小块内容可能因缓冲而延后到达,但开启缓冲并不意味着所有响应都一定等到结束才发送。
在本文的本地小响应对照测试中,同一个脚本直连时约每秒收到一条事件;开启 Nginx 响应缓冲后,5 条事件约在第 5 秒一起到达;关闭缓冲后恢复逐条到达。这是本次诊断案例的结果,不是所有应用的固定表现。
proxy_cache off; 用于取消可能继承的代理缓存配置;gzip off; 用于在该路由排除 Nginx 压缩的影响。应用或 CDN 自己的压缩行为仍需要单独检查。
proxy_http_version 1.1; 明确指定 Nginx 访问上游使用的 HTTP 版本,并不要求浏览器与站点之间也使用 HTTP/1.1。
另外,下面两个写法经常被用错:
proxy_request_buffering off;控制的是客户端请求体缓冲,不能代替响应端的proxy_buffering off;。add_header X-Accel-Buffering no;只是给下游增加响应头,不能代替当前这一层 Nginx 的响应缓冲配置。
由后端返回 X-Accel-Buffering: no 是另一种控制方式,但会受到 Nginx 是否忽略该上游响应头的配置影响。对本次诊断,直接在命中的路由中设置 proxy_buffering off; 更容易核对。
对于通过系统软件包安装、由 systemd 管理的 Nginx,修改后执行:
sudo nginx -t && sudo systemctl reload nginx
只有检查通过后才会执行重载。使用面板、容器或自定义 Nginx 实例时,应通过对应实例检查并重载,避免检查的是一套配置,运行的却是另一套。
将这些设置应用到真实业务路由时,要保留该路由已有的上游地址、鉴权、限流和必要的转发头。

五、分别测试源站 Nginx 和 CDN 入口

如果域名前面还有 CDN、负载均衡或其他代理,必须继续分层对比。
先在测试电脑设置实际域名和源站 IPv4 地址。下面两个值只是示例,需要替换:
domain='www.example.com'
origin_ip='203.0.113.10'
先绕过域名解析指向的 CDN,直接连接源站 Nginx:
curl --noproxy '*' -N -i --max-time 15 \
--resolve "${domain}:443:${origin_ip}" \
"https://${domain}/sse-test"
--resolve 将本次连接指向指定 IP,同时保留 URL 中的域名用于 HTTPS 访问和证书校验。
这个测试要求源站允许测试电脑连接,并提供客户端信任、匹配域名的证书。如果访问被源站规则拦截或证书校验失败,应先处理对应条件,不能据此判断 SSE 缓冲。
再按照域名正常解析的入口访问:
curl --noproxy '*' -N -i --max-time 15 \
"https://${domain}/sse-test"
两次请求应比较同一条测试路由:
| 观察结果 | 优先检查的位置 |
|---|---|
| 后端直连就成批输出 | 应用生成、读取和刷新响应的方式 |
| 后端逐段,经过源站 Nginx 后成批 | 实际命中的 Nginx 路由及其他源站代理 |
| 源站逐段,经过 CDN 后成批 | CDN 或前置网关的流式支持、缓存及响应处理规则 |
| 公网 curl 逐段,网页仍整段显示 | 前端读取、事件解析与渲染逻辑 |
这些结果用于缩小范围,不是单凭一次请求就给某一层定责。诊断路由正常后,还需要对真实业务接口按同样顺序复查。
六、curl 正常,为什么网页仍然整段显示?
如果相同业务请求在 curl 中已经能逐段到达,就应检查前端怎样消费响应。
例如,先执行 await response.text(),等读取完整响应后才更新页面,用户看到的自然是一次性出现的内容。
使用 Fetch 处理流式响应时,需要持续读取 response.body,保留未完成的片段,解析出完整事件后再更新界面。一次网络读取不等于一个完整 SSE 事件:一个事件可能被拆成多个片段,多个事件也可能合并到同一次读取中。
还要注意 SSE 事件末尾的空行。如果服务端发送了自定义 event: 类型,使用原生 EventSource 的页面也要注册对应的事件监听器。
原生 EventSource 与使用 Fetch 发起的 POST 流式请求,不是可以随意互换的两种调用方式。修改前端时,应按照项目原有接口约定处理方法、鉴权和事件格式。
七、输出一段时间后断开,先区分空闲超时
“最后一次性出现”和“输出中途断开”是两类问题。关闭缓冲后仍然断流,需要继续检查超时。
proxy_read_timeout 120s; 表示 Nginx 两次读取上游数据之间允许的最长等待时间,不是把整次回答固定限制为 120 秒。
如果模型思考或外部工具调用期间长时间没有任何字节返回,即使任务仍在运行,链路上的某一层也可能因为空闲超时关闭连接。
应用可以在等待阶段发送并刷新 SSE 注释心跳,例如以下内容,末尾同样保留空行:
: ping
心跳间隔应小于整条链路中最短的空闲超时。它不能绕过服务商对请求总时长的硬性限制,也不能替代对应用异常的检查。
另外,本文 curl 命令主动设置了 15 秒上限。拿它测试长期运行的真实接口时,如果到点出现超时退出,不能直接认定服务器发生了故障。
如果请求一开始就返回 502,优先检查上游服务是否运行、地址和端口是否正确。可以继续参考本站的 Nginx 出现 502 Bad Gateway 怎么办?常见原因与排查教程。
八、排查完成后,移除临时测试入口
确认问题位置后,将需要的配置合并到真实业务路由,并再次测试实际 AI 请求。
随后删除临时添加的 /sse-test 路由,检查并重载 Nginx,再回到运行脚本的终端按 Ctrl+C 停止测试服务。
不要只以状态码 200 作为验收标准。至少应确认:内容能够逐段到达、长时间等待不会意外断开,以及前端能够正确结束加载状态。
九、总结
AI 聊天回答一次性出现,未必是服务器性能不足。后端没有及时写出、代理缓冲了响应,或者前端等待完整正文后才渲染,都可能产生相似现象。
排查时,先用可控的 SSE 测试接口建立基准,再按“后端直连、源站 Nginx、CDN 入口、前端页面”的顺序逐层比较。找到数据从哪一层开始延迟,再调整对应配置,才能避免反复修改无关参数。
对于已经上线的 AI 应用,最终应以真实业务请求的逐段显示、超时表现和结束状态作为验证结果,而不只是测试接口能够打开。








