AI API 流式输出中断怎么排查?
简答: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 网关,不能因为请求路径类似就假定它完整复刻上述协议。实际返回的事件名、字段和结束语义应以该网关文档与脱敏原始响应为准;查不到的兼容性信息应标记为“需核实”。
按现象处理:无输出、停顿、半段断开和重复重试
- 完全无输出:先看首字节是否到达。没有首字节时查最终 URL、模型、权限、流式开关和代理;有首字节但解析器无事件时查
Content-Type、换行边界和data:提取逻辑。 - 延迟后批量出现:对比直连与经过代理的结果,检查代理缓冲、压缩和 CDN 响应体处理;同时记录首事件时间与事件间隔,不要只看页面观感。
- 输出到半段后断开:检查最后一个完整事件、是否收到协议结束事件、读取空闲超时、上游日志和客户端 AbortController。只有证据显示是暂时性网络错误时,才考虑有限退避重试。
- 内容完整但前端不结束:把文本累积状态与完成状态分离,补充结束事件处理;不能用“连接关闭”自动当作成功。
- 重试后文本重复:流式请求可能已经在上游生成或产生副作用,重试前要考虑重复计费、重复工具调用和业务幂等键。涉及工具调用时,不要自动重放未经确认的副作用操作。
建议每次请求至少记录:请求开始时间、首字节时间、首事件时间、最后事件时间、HTTP 状态码、事件数量、输出字符数、结束事件、关闭异常类型和脱敏请求 ID。日志中不要记录完整 API Key、Authorization 头、完整提示词或可能包含敏感数据的原始响应。
一页式排查清单:证据→结论→下一动作
- 凭据:确认 Key 未过期、未被前端暴露,且请求使用了目标服务要求的认证头。
- 最终 URL:打印主机和路径,不打印完整 Authorization;确认应用代理没有重复拼接
/v1或把路径改写到非流式接口。 - 模型:从当前控制台或官方文档复制模型 ID,确认该模型和账号区域支持当前协议与流式参数。
- 权限与区域:查看响应体、请求 ID 和服务端日志;不要仅凭 401、403 或 404 的数字猜测原因。
- 最小请求:用短输入、低输出上限和
stream=false建立基线,再用curl -N测试原始流。 - 解析:确认 UTF-8 流式解码、事件边界、JSON 拆包、未知事件和残余缓冲区处理正确。
- 链路:逐层绕过浏览器、应用代理、Nginx、CDN 或网关,比较首事件时间、事件间隔和最后事件。
- 完成:只有收到目标协议的正常结束信号并完成业务状态落盘,才把请求标记为成功;否则标记为中断或 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、暂时性断网等可能短暂问题;同时确认上游是否已经生成部分结果。涉及工具调用、写入数据库或其他副作用时,要先设计幂等和去重,否则重试可能重复执行。