AI API SSE流式输出卡住、截断或重复:代理缓冲排查指南
直接答案
从SSE帧边界、HTTP状态、Content-Type、反向代理缓冲、压缩、超时、断线重连和客户端增量解析排查AI API流式响应异常。
症状、差异与判断依据
直接答案
先绕过代理直连上游,记录首字节时间、每个数据块的到达时间和字节数,再逐层加回 Nginx/CDN/网关。确保成功响应使用与 SSE 一致的 text/event-stream,代理不对该路由做响应缓冲,读取超时大于允许的无数据间隔,且客户端按空行分隔完整 SSE event,而不是把每个 TCP/Fetch chunk 当一个 JSON。如果存在断线重试,必须按请求 ID、event ID 或应用层序号去重,不能把重连后的全量结果直接追加。
一、先区分四种现象
首字节很慢,但一旦开始就持续流式输出:优先查上游排队、建连、TLS 和模型首 token 时间。 上游实时输出,但经代理后成批到达:优先查响应缓冲、压缩和 CDN 优化。 流在固定时长或空闲间隔后断开:优先查读超时、空闲超时和心跳。 文本重复、乱序或 JSON 偶发解析失败:优先查增量解析、多字节 UTF-8 边界和自动重连。 四种现象可能同时存在,但证据不同。保存每层的时间线,不要只录制前端屏幕。
二、不要把传输Chunk当成SSE Event
HTTP 响应流交给客户端的 chunk 边界不是应用层消息边界。一个 SSE event 可能被拆成多个 chunk,多个 event 也可能合并到一个 chunk。因此,对每个 reader.read() 结果立即执行 JSON.parse 是一个常见错误。 客户端应维护字节到文本的增量解码器,将未完成文本保留在 buffer 中,直到遇到 SSE 空行事件分隔符。然后按行处理 data:、event:、id: 和注释字段,同一 event 中的多个 data: 行按规范合并。
三、正确处理UTF-8拆包
中文、emoji 等字符在 UTF-8 中占多个字节,传输 chunk 可能恰好在一个字符中间切开。如果对每个 chunk 独立转字符串,可能生成替换字符,进而破坏 JSON 或用户文本。 在 Web 客户端使用支持流式状态的 TextDecoder,中间 chunk 解码时保留未完整字节,流结束时再 flush。在其他语言中使用等价的增量 UTF-8 解码器,不要手写多字节拼接。
四、核对HTTP状态和Content-Type
在进入 SSE 解析前,先检查 HTTP 状态、最终 URL 和媒体类型。认证失败、限流、网关超时或 WAF 可能返回普通 JSON/HTML 错误体,它不是 SSE event stream。如果客户端不区分,就会把错误页按 data: 协议解析,丢失真实状态码。 对错误响应安全记录状态、请求 ID、媒体类型和限长脱敏正文,再进入重试分类。只有成功响应且内容类型与协议一致时,才交给 SSE 解析器。
五、定位Nginx响应缓冲
Nginx 反向代理默认可以缓冲上游响应。这对普通页面有利,但对希望立即转发的 SSE 路由会造成“上游在流,前端批量跳字”。对精确的流式路由评审 proxybuffering、上游发送的 X-Accel-Buffering 以及其他缓冲相关设置。 不要全局关闭所有站点的响应缓冲,否则会改变普通流量的内存、磁盘和慢客户端行为。只对已确认的 SSE location 做最小配置,并在发布前后对比首字节和 chunk 到达间隔。
六、检查CDN压缩与响应优化
CDN 可能对文本响应做压缩、缓冲、自动优化或连接聚合。压缩器为等待更多输入可能延迟小 event 的到达;错误的缓存键还可能将一个用户的流当作可共享响应,带来严重的隔离风险。 对 SSE 路由显式审查 CDN 缓存、响应改写、压缩和超时。验证缓存头与用户隔离策略,不要只通过添加随机查询参数绕过。如需关闭压缩,应用真实流量测量时延与带宽取舍。
七、读超时不是整个请求总时长
反向代理的上游读超时常按两次读取之间的无数据间隔计算,不一定是从请求开始计时的总上限。但负载均衡、CDN、API 网关和客户端可能各有不同的绝对或空闲超时。 记录断开时间是否稳定在 30、60、100 秒等阈值,并逐层对照配置。心跳注释可用于保持合法长连接的活性,但它不应用来绕过组织的总请求时长或资源治理限制。
排障步骤与验证
八、区分正常完成、错误与传输中断
客户端不能把 TCP 连接关闭一律当作成功完成。应根据具体 AI API 的协议识别完成 event、错误 event 和最终使用量/状态。如果连接关闭前没有看到协议完成信号,结果应标记为不确定或不完整,不应直接存为完整答案。 记录上游请求 ID、最后成功 event 类型、event ID/序号、接收字节数和断开层级。不要记录完整提示词或模型输出,除非符合数据政策且完成脱敏。
九、重连会制造重复内容
EventSource 语义和自定义 Fetch 流式客户端的重连行为不相同。中间层、SDK 或业务代码也可能在断线后重新发起整个 AI 请求。如果客户端保留旧文本并把新流从头追加,就会出现成段重复。 在网络边界重试前,明确该 API 是否支持从某 event 恢复。若不支持,将新请求视为新生成,用幂等键或业务状态避免重复计费/写入,并在 UI 明确替换或新建结果,而不是无条件追加。
十、运行时反向压力与慢客户端
当客户端渲染或网络速度慢于上游产生速度时,缓冲会在应用、代理或客户端内存中积累。全局禁用代理缓冲后,慢客户端还可能长时间占用上游连接。 监控活跃流数、每流缓冲字节、客户端断开后上游取消延迟、进程内存和文件描述符。前端离开页面或用户点击停止时,将 Abort/取消传递到上游,不要继续产生无人消费的内容。
十一、建立逐层时间线
为每个测试请求保存:DNS/TCP/TLS 完成时间,HTTP 响应头时间,首个上游 event 时间,每层首 chunk 时间,最后 event 与连接关闭时间。只记录字节数、序号和类型,避免泄露内容。 对比直连上游、经 Nginx、经 CDN 和真实浏览器四条时间线。第一个开始成批到达或固定时长断开的边界,就是优先排查点。
十二、最小可重复测试
建立一个不调用模型的测试 SSE 端点:每秒发送一个包含序号和服务器时间的 event,持续固定次数,最后发送明确完成 event。客户端记录接收时间与序号,不显示敏感数据。 如果测试端点经代理也成批到达,根因在传输路径;如果测试正常而 AI API 不正常,再检查上游 SDK、事件转换和应用逻辑。这样可以在不重复计费的情况下调试基础设施。
十三、常见错误
将每个 Fetch/TCP chunk 直接当作完整 SSE event 或 JSON。 对每个 chunk 独立 UTF-8 解码,在中文或 emoji 中间产生乱码。 为修复一条 SSE 路由而全局关闭 Nginx 缓冲。 忽略 CDN 的压缩、缓存和空闲超时,只改源站。 把连接关闭当作正常完成,保存了被截断答案。 断线后重新生成并无条件追加,导致内容重复和重复写入。 记录完整提示词与输出来排查时间问题,造成数据泄露。
十四、验收清单
直连和经每层代理的首 event 延迟有可解释的差异。 每秒测试 event 不会被长时间批量缓冲。 中文、emoji 和被拆分的 JSON 在 chunk 边界仍正确解析。 非 2xx 或非 SSE 响应被分类为 HTTP/API 错误,不进入事件解析。 超时配置与合法最大无数据间隔一致,超时断开可观测。 正常完成、上游错误和传输中断有不同状态。 重连不会导致重复文本、重复计费或重复业务写入。 客户端取消能传递到上游,活跃连接和内存恢复正常。
总结
SSE 流异常要从边界和时间线定位:chunk 不是 event,多字节字符不能逐块独立解码,代理缓冲会改变到达时间,超时会在长时间无数据时断开,重连可能产生全新结果。用无模型的最小 SSE 端点逐层测量,再修正精确路由和客户端状态机,能避免将基础设施故障误判为模型问题。
常见问题
1. 为什么本地逐字输出,经Nginx后一次出现?
最常见线索是反向代理响应缓冲。用直连与经代理的 chunk 到达时间对比确认,再对精确 SSE 路由做最小调整。
2. 只设置`Cache-Control: no-cache`就够了吗?
不一定。它与 HTTP 缓存语义有关,但 Nginx 上游响应缓冲、CDN 压缩和超时是独立配置,需逐层核对。
3. SSE的每一行`data:`都是一个事件吗?
不是。一个 event 可以有多个 `data:` 行,事件通过空行结束。解析器应按 SSE 规则聚合,不是逐行盲目 `JSON.parse`。
4. 心跳能解决所有断流吗?
不能。心跳可以防止某些空闲超时,但无法修复绝对超时、上游错误、网络中断、客户端取消或资源配额。
5. 断线后可以自动重试吗?
只有在明确 API 的幂等、恢复和计费语义后才能做。不支持续传时,重试可能是新的完整生成,需要替换旧结果或显式去重。