OrcaRouter

AI API 流式输出中断怎么排查?

更新于 2026-11-01 · orcarouter.cyou

简答:AI API 流式输出中断,先不要直接换模型或重试:用同一 Base URL、API Key、模型和短输入分别测试 stream=false 与 stream=true,再用 curl -N 绕过业务代码观察原始响应。若非流式成功、原始流完整而界面异常,重点查客户端解析;若原始流也停顿或提前断开,再查代理缓冲、读取超时和上游结束事件。本文按“凭据→最终 URL→模型→权限/区域→最小请求→错误响应”的顺序给出排查方法,核验日期为 2026-11-01。

定义:AI API 流式输出中断,是指 HTTP 请求已经建立或返回部分内容,但客户端在收到完整协议结束信号前停止接收、解析失败、连接被关闭,或界面未能显示后续内容。

先试这一步:用同一请求做非流式与流式对照

先固定六项变量:API Key、最终请求 URL、模型名、请求体、认证头和输入内容。只把 stream 从 false 改成 true,不要同时更换模型、代理或客户端。兼容接口不等于语义完全兼容,事件名、结束标记、错误格式和响应字段仍应以目标服务当前文档或实际响应为准。

现象优先归因下一动作
stream=false 也失败凭据、URL、模型、权限或额度查看 HTTP 状态码、响应体和请求 ID,不要先调解析器
非流式成功,流式无任何字节路径、流式能力、代理或请求体用 curl -N 直连最终 URL,记录响应头
原始流持续返回,网页一次性显示浏览器读取或中间层缓冲检查 Content-Type、ReadableStream 和代理缓冲
输出到半段后连接关闭读取空闲超时、上游异常或客户端取消保留最后一个事件、关闭原因和响应头,再判断是否可重试
内容完整但界面一直转圈未识别协议结束事件或状态未落盘把“收到文本”和“确认正常结束”分成两个状态

最小复现命令中的 URL、模型和 Key 都是示例占位符,发送前必须从目标服务当前文档或控制台复制并核实;不要把真实 Key 写进命令历史或日志。

curl -N --no-buffer -D response-headers.txt -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -X POST "https://API_HOST/v1/chat/completions" -d '{"model":"MODEL_ID","messages":[{"role":"user","content":"只回复一个短句"}],"stream":true}'

这条命令只能帮助观察客户端收到的原始字节,不能关闭服务器、网关或 CDN 的缓冲。SSE 通常使用 text/event-stream,事件块以空行分隔;这些格式规则可参见 MDN 的 SSE 事件流说明。

按三步定位:客户端、SSE 解析,还是代理与上游

第一步:确认请求真的到达并获得了流

记录但不要泄露完整凭据:请求开始时间、首字节时间、HTTP 状态码、最终 URL 的主机与路径、模型名、响应头中的请求 ID、Content-Type、Content-Encoding,以及响应体前几条已脱敏事件。若返回 400、401、403 或 404,先修正请求,不要盲目重试;这些状态码不能脱离响应体就归因于某一种固定原因。

如果状态码不是成功响应,先保存响应体再判断。若状态码成功但没有任何字节,检查是否请求到了错误路径、目标模型是否支持流式、网关是否把流式请求改写成普通请求,以及读取超时是否从请求建立时就开始计算。

第二步:确认 SSE 解析器没有把拆包当成完整事件

TCP 或 HTTP 分块不会保证一次读取就是一个 SSE 事件。一个 JSON 可能跨多个网络块,一个中文字符也可能跨多个字节块。浏览器端应先用 UTF-8 流式解码,再按空行切分事件,最后才解析 data: 中的 JSON。MDN 的 TextDecoderStream 文档说明了将二进制流转换为连续文本的方式。

安全的处理顺序是:读取字节→流式解码→追加到缓冲区→寻找完整事件边界→提取 data 行→严格 JSON.parse→按协议事件名处理。流结束时不要无条件执行残余处理函数:如果缓冲区没有完整的空行边界,先记录残余内容并标记为 incomplete;只有确认事件结构闭合且 JSON 完整时,才允许处理。否则,半个事件可能被误判为正常结束。

浏览器 Fetch 的核心逻辑可以写成以下伪代码,变量均为本地状态,不包含真实 Key:

const decoder = new TextDecoder(); let buffer = ""; let ended = false; while (true) { const {value, done} = await reader.read(); if (done) break; buffer += decoder.decode(value, {stream: true}); const events = buffer.split("\n\n"); buffer = events.pop() || ""; for (const event of events) { if (!event.trim()) continue; const data = event.split("\n").filter(line => line.startsWith("data:")).map(line => line.slice(5).trim()).join("\n"); if (data === "[DONE]") { ended = true; continue; } try { const item = JSON.parse(data); handleEvent(item); } catch { record("invalid_complete_event"); } } } buffer += decoder.decode(); if (buffer.trim() && !ended) record("incomplete_residual_buffer");

这段逻辑仍需按目标协议调整,因为 [DONE]、事件名和结束字段不是所有接口的共同规则。未知事件应记录并安全忽略,而不是让整个连接因新增事件类型崩溃。

第三步:确认代理没有缓冲、压缩或提前关闭

如果 curl 直连上游能持续收到事件,但经过应用代理、Nginx、CDN 或网关后变成一次性输出,优先检查响应缓冲、缓存、压缩、读取空闲超时和客户端断开后的上游处理。Nginx 官方文档说明,proxy_buffering 会影响代理响应的传递方式,proxy_read_timeout 只约束两次读取之间的等待时间;应结合实际配置和日志核对,不要直接复制固定值。可参考 Nginx proxy 模块文档。

如果链路经过 Cloudflare 等中间层,应检查对应域名和规则是否启用了响应体缓冲。Cloudflare 文档提供了 response_body_buffering 设置,但关闭缓冲可能影响依赖响应体检查的功能,因此应只对明确的流式路径评估,不要全站修改。参考 Cloudflare Configuration Rules 设置说明。

不同协议的正常结束信号不能混用

“HTTP 200”只表示 HTTP 层请求成功,不等于模型业务流完整结束。客户端应同时保存已输出文本、最后一个事件类型、协议结束字段、连接关闭原因和是否收到完整结束事件。

协议流式观察点完整性判断
OpenAI Responses API事件类型以 response.* 为主,文本增量和响应状态分开传递重点确认是否收到 response.completed,并检查响应状态及错误字段;参考 Responses Streaming Events 官方文档
OpenAI Chat Completions接收 chat.completion.chunk,文本位于增量字段,结束原因位于 finish_reason检查最后块是否带有结束原因;length、content_filter、tool_calls 与自然停止的含义不同,参考 Chat Completions Streaming Events 官方文档
Anthropic Messages通常经历 message_start、内容块事件、message_delta 和 message_stop,期间还可能出现 ping 或 error不要只看最后一段文本;应确认 message_stop,并处理流中的错误事件。事件顺序和增量字段以 Anthropic Streaming Messages 官方文档为准

如果使用 OpenAI Compatible 网关,不能因为请求路径类似就假定它完整复刻上述协议。实际返回的事件名、字段和结束语义应以该网关文档与脱敏原始响应为准;查不到的兼容性信息应标记为“需核实”。

按现象处理:无输出、停顿、半段断开和重复重试

建议每次请求至少记录:请求开始时间、首字节时间、首事件时间、最后事件时间、HTTP 状态码、事件数量、输出字符数、结束事件、关闭异常类型和脱敏请求 ID。日志中不要记录完整 API Key、Authorization 头、完整提示词或可能包含敏感数据的原始响应。

一页式排查清单:证据→结论→下一动作

  1. 凭据:确认 Key 未过期、未被前端暴露,且请求使用了目标服务要求的认证头。
  2. 最终 URL:打印主机和路径,不打印完整 Authorization;确认应用代理没有重复拼接 /v1 或把路径改写到非流式接口。
  3. 模型:从当前控制台或官方文档复制模型 ID,确认该模型和账号区域支持当前协议与流式参数。
  4. 权限与区域:查看响应体、请求 ID 和服务端日志;不要仅凭 401、403 或 404 的数字猜测原因。
  5. 最小请求:用短输入、低输出上限和 stream=false 建立基线,再用 curl -N 测试原始流。
  6. 解析:确认 UTF-8 流式解码、事件边界、JSON 拆包、未知事件和残余缓冲区处理正确。
  7. 链路:逐层绕过浏览器、应用代理、Nginx、CDN 或网关,比较首事件时间、事件间隔和最后事件。
  8. 完成:只有收到目标协议的正常结束信号并完成业务状态落盘,才把请求标记为成功;否则标记为中断或 incomplete。

如需在排查前先整理 Base URL、Key、模型和最小请求,可使用接口错误排查页面建立非流式基线;如果问题出在客户端配置,再参考Codex 的 API Key 与 Base URL 配置步骤。了解 API 网关和路由概念,可阅读AI API 路由网关说明。

常见问题

AI API 返回 200,为什么仍然算流式失败?

200 只说明 HTTP 层响应成功,不代表业务流完整。还要确认收到了协议规定的正常结束事件,并检查是否存在 incomplete、错误事件、截断的 JSON 或未完成的工具调用。没有结束信号时,应标记为中断,而不是自动标记成功。

curl -N 能解决 AI API 流式输出中断吗?

不能直接解决。curl 的作用是绕过业务代码,观察原始响应是否持续到达;它仍可能受到服务器、网关、CDN 或代理缓冲影响。若 curl 有连续事件而网页没有,优先查客户端解析和中间层;若 curl 也提前停止,再查上游和链路超时。

流结束时可以直接把剩余 buffer 交给 JSON.parse 吗?

不可以无条件这样做。未遇到完整空行边界的残余内容可能只是半个 SSE 事件或不完整 JSON。应先记录残余缓冲区,只有确认事件闭合且 JSON 严格可解析时才处理;否则标记为 incomplete,并把它作为断流证据。

OpenAI Responses、Chat Completions 和 Anthropic 的结束事件一样吗?

不一样。Responses API、Chat Completions 和 Anthropic Messages 使用不同的事件名称、增量字段和完成语义。Responses 重点核对 response.completed,Chat Completions 要检查最后块的 finish_reason,Anthropic Messages 要确认 message_stop,并处理可能出现的 error 事件。兼容接口不能仅凭路径相似就视为语义完全一致。

流式中断后应该马上重试吗?

不应马上重试。先区分 400、401、403、404 等请求或权限问题,与 429、部分 5xx、暂时性断网等可能短暂问题;同时确认上游是否已经生成部分结果。涉及工具调用、写入数据库或其他副作用时,要先设计幂等和去重,否则重试可能重复执行。

相关阅读

参考资料

  1. OpenAI Responses Streaming Events 官方文档
  2. MDN Server-sent events 使用说明
  3. MDN TextDecoderStream 文档
  4. Nginx proxy 模块官方文档(nginx.org)
  5. Cloudflare Configuration Rules 设置说明