OpenAI API 流式输出中断:原因、判断方法与修复步骤
简答:OpenAI API 流式输出中断,不一定是模型故障:先看是否收到正常结束信号,再检查 HTTP 状态码、响应体、客户端超时和代理层。Chat Completions 重点看 finish_reason 与最后的 usage chunk;Responses API 重点看 response.completed、response.incomplete 和 error 事件。若收到 200 后直接断开,优先排查客户端、SSE 解析器、反向代理和中转网关;若返回 429、500 或 503,则按错误码处理,不要盲目重试。
定义:OpenAI API 流式输出中断,是指请求已经开始返回增量内容,但连接在正常结束事件、结束标记或完整响应状态到达前停止。
先用这张表判断:正常结束还是异常断流
先记录请求使用的 endpoint、模型、HTTP 状态码、响应头中的请求标识、最后一个事件类型、已接收字符数和客户端异常。不要只根据“屏幕上没有继续输出”判断故障。
| 现象 | 优先判断 | 下一步 |
|---|---|---|
| Chat Completions 收到 finish_reason=stop | 模型正常结束 | 检查提示词、stop 参数和前端渲染逻辑 |
| finish_reason=length | 达到请求设置的输出上限 | 检查 max_tokens 或对应输出上限参数,不要当成网络断流 |
| finish_reason=tool_calls | 模型完成了工具调用阶段 | 处理工具调用并按协议继续后续请求 |
| Responses API 收到 response.incomplete | 响应以不完整状态结束 | 读取 incomplete_details.reason,例如 max_output_tokens |
| HTTP 200 后没有结束事件,客户端报连接重置或 EOF | 连接、SSE 解析、代理或网关中断 | 关闭中转层后做最小复现,再逐层恢复 |
| 429 | 限流、额度、消费上限或使用量限制之一 | 先看 error.code 和响应体,只有明确属于限流时才按 Retry-After 重试 |
| 500/503 | 服务端错误或模型暂时过载 | 记录请求信息,检查状态页;有 Retry-After 时遵循它 |
Chat Completions 的 finish_reason 会区分自然结束、达到输出上限、工具调用和内容过滤等情况;启用 stream_options.include_usage 后,最终 chunk 才可能包含整次请求的 usage,而且流被中断或取消时可能收不到该 chunk。OpenAI Chat Completions streaming events
Responses API 则使用事件类型表达状态,例如 response.completed、response.incomplete 和 error。OpenAI Responses streaming events
最短排查流程:凭据、最终 URL、模型、响应体、日志
- 确认凭据。确认程序实际读取到的是当前有效的 API Key 或兼容网关 Key,不要把 Key 写入日志、前端代码或异常截图。
- 确认最终 URL。打印脱敏后的 endpoint、路径和是否经过反向代理。OpenAI 原生接口与兼容实现的 Base URL、路径和事件格式可能不同,兼容服务的具体规则应以服务商文档为准。
- 确认模型。记录实际发送的模型 ID,并用同一个模型执行一次非流式请求。模型 ID 不要凭记忆填写,以控制台、账户可用模型列表或服务商文档为准。
- 确认最小请求。只保留一条短文本输入,暂时关闭工具调用、结构化输出和复杂上下文,降低变量数量。
- 记录响应体和结束信号。日志至少记录时间戳、时区、endpoint、模型、HTTP 状态码、请求 ID、最后事件类型、已接收字节数和客户端异常;不要记录完整 Key 或敏感输入。
- 逐层恢复。先直连 API,再经过 SDK,再经过自己的后端、Nginx、负载均衡或兼容网关。哪一层恢复后重新出现断流,通常就是重点排查对象。
OpenAI 的排障建议要求按模型、服务层级和项目过滤指标;如果客户端报错但服务健康数据中没有对应请求,说明请求可能没有到达 OpenAI,常见方向是客户端超时、代理或网络问题。OpenAI API 错误与延迟排查
先关闭 streaming 做一次对照
如果非流式请求能够稳定返回完整结果,而流式请求中断,问题范围通常集中在 SSE 读取、连接空闲超时、代理缓冲、HTTP 客户端取消或兼容网关的流式实现。OpenAI 文档说明,HTTP 流式响应使用 stream=true,并通过 Server-Sent Events 增量返回内容。OpenAI Streaming API responses
Python 最小复现:记录结束原因而不是只打印文本
先安装与你项目锁定版本相符的 OpenAI Python SDK,并设置真实的环境变量。OPENAI_MODEL 必须替换为当前账户或兼容服务实际支持的模型;如果使用兼容 API,还要依据服务商文档设置 OPENAI_BASE_URL。
python -m pip install -U openai
export OPENAI_API_KEY='在本机环境中设置'
export OPENAI_MODEL='以控制台或服务商文档为准'
# 使用兼容接口时,按服务商文档设置:
# export OPENAI_BASE_URL='以服务商文档为准'
python stream_check.py下面的代码只输出脱敏诊断信息和已收到的文本。它把“正常结束”和“异常抛错”分开,便于判断是否真的发生了断流。
import os
from openai import OpenAI
api_key = os.environ.get("OPENAI_API_KEY")
model = os.environ.get("OPENAI_MODEL")
if not api_key or not model:
raise SystemExit("请先设置 OPENAI_API_KEY 和 OPENAI_MODEL")
client_kwargs = {"api_key": api_key}
base_url = os.environ.get("OPENAI_BASE_URL")
if base_url:
client_kwargs["base_url"] = base_url
client = OpenAI(**client_kwargs)
received = []
last_finish_reason = None
try:
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "用一句话回答:什么是 SSE?"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices:
choice = chunk.choices[0]
text = choice.delta.content or ""
if text:
received.append(text)
print(text, end="", flush=True)
if choice.finish_reason is not None:
last_finish_reason = choice.finish_reason
if getattr(chunk, "usage", None) is not None:
print("\nusage=received", flush=True)
print("\nfinish_reason=", last_finish_reason)
print("received_chars=", len("".join(received)))
except Exception as exc:
print("\nstream_error_type=", type(exc).__name__)
print("received_chars=", len("".join(received)))
raise如果代码打印了 finish_reason=stop 或其他明确结束原因,通常不是“中途断流”。如果异常发生在已有文本之后,保留已收到内容,同时记录异常类型;不要把异常后的整段内容无条件重新拼接,否则重试可能造成重复输出。
Node.js 最小复现:区分事件结束、EOF 与请求超时
Node.js 示例使用官方 SDK 的异步迭代方式。先安装 SDK、设置 Key 和模型变量,再运行测试;不要把真实 Key 放进浏览器端代码。
npm install openai
export OPENAI_API_KEY='在本机环境中设置'
export OPENAI_MODEL='以控制台或服务商文档为准'
node stream-check.mjsimport OpenAI from "openai";
const apiKey = process.env.OPENAI_API_KEY;
const model = process.env.OPENAI_MODEL;
if (!apiKey || !model) {
throw new Error("请先设置 OPENAI_API_KEY 和 OPENAI_MODEL");
}
const clientOptions = { apiKey };
if (process.env.OPENAI_BASE_URL) {
clientOptions.baseURL = process.env.OPENAI_BASE_URL;
}
const client = new OpenAI(clientOptions);
let received = "";
let finishReason = null;
try {
const stream = await client.chat.completions.create({
model,
messages: [{ role: "user", content: "用一句话回答:什么是 SSE?" }],
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) {
const choice = chunk.choices?.[0];
const text = choice?.delta?.content ?? "";
if (text) {
received += text;
process.stdout.write(text);
}
if (choice?.finish_reason) finishReason = choice.finish_reason;
if (chunk.usage) process.stderr.write("\nusage=received\n");
}
console.error(`\nfinish_reason=${finishReason}`);
console.error(`received_chars=${received.length}`);
} catch (error) {
console.error(`\nstream_error_type=${error?.constructor?.name ?? "UnknownError"}`);
console.error(`received_chars=${received.length}`);
throw error;
}官方流式示例同样通过异步迭代读取增量 chunk,并从 delta.content 取文本。OpenAI Streaming API responses
按错误类型修复:不要把所有 429 都自动重试
| 错误或现象 | 可能原因 | 处理方式 |
|---|---|---|
| 429 + rate limit 或 slow_down | 请求频率、并发或增长速度过快 | 优先遵循 Retry-After;没有该响应头时使用带抖动的指数退避,并限制重试次数 |
| 429 + credit_balance_exhausted | 预付余额耗尽 | 检查账户额度或计费状态,不能靠重试恢复 |
| 429 + spend/usage limit | 项目或组织消费、使用量上限 | 检查对应限制并调整,不要循环重试 |
| 500 | 服务端处理错误 | 短暂等待后有限重试,同时检查状态页和请求日志 |
| 503 + server_is_overloaded | 模型暂时过载 | 遵循 Retry-After;没有时延迟重试并检查状态页 |
| 客户端 EOF、连接重置、APITimeoutError | 网络、代理、SSL、客户端或空闲超时 | 检查客户端超时、代理配置、SSE 缓冲和连接复用 |
官方错误文档明确区分了余额耗尽、请求限流、消费上限、使用量上限和模型过载;计费、消费或配额类错误不会因为重复发送请求而恢复。OpenAI Error codes
重试流式请求还要考虑副作用:如果上一次请求已经产生部分输出,直接重试可能得到不同内容或重复内容。更安全的做法是保存已接收文本、请求参数摘要和唯一业务请求 ID;只有在业务允许重复生成时才重试。若要续写,应把已接收内容作为明确上下文传入新请求,并在应用层去重,而不是假设服务端会自动从断点继续。
反向代理、SSE 与兼容接口的检查清单
- SSE 缓冲:检查 Nginx、负载均衡、网关或 CDN 是否对
text/event-stream做了响应缓冲。缓冲可能表现为长时间没有新内容,随后一次性返回多块数据。 - 空闲超时:确认客户端、代理和网关的读取超时是否短于模型生成过程中两次事件之间的间隔。不要只调大客户端超时,代理层也要逐项核对。
- 连接复用:对比 HTTP/1.1 与 HTTP/2、连接池和 Keep-Alive 设置;如果只有复用连接容易失败,可暂时关闭复用做诊断。
- 事件解析:不要把每个网络数据包当成完整事件。SSE 事件可能跨多个数据块,必须使用可靠的 SSE 解析器或 SDK 迭代接口。
- 结束信号:Chat Completions 应检查 finish_reason;Responses API 应检查 response.completed、response.incomplete 和 error。仅判断文本是否为空是不够的。
- 兼容实现:如果使用 OpenAI 兼容 Base URL,逐项核对认证头、路径、模型映射、SSE 事件格式、usage 返回方式和错误结构。上述字段是否完全兼容,需要以对应服务商文档和实际最小请求为准。
任何兼容接口都不能仅凭“支持 OpenAI API”推断其一定支持全部流式字段。建议先用短输入测试,再逐步加入工具调用、结构化输出和长上下文。
流式中断后的安全处理与定位资料
中断时可能没有最终 usage chunk,因此不要把缺失 usage 直接当成“没有消耗”。应用可以先记录请求 ID、模型、开始和结束时间、已接收字节数及状态;之后再通过可用的用量面板或服务商账单数据核对。OpenAI 文档特别提示,流被中断或取消时可能无法收到包含总 token 用量的最终 chunk。Chat Completions streaming events
如果怀疑是平台侧故障,先按单一模型、单一项目和时间范围过滤服务健康或请求指标,再查看状态页。历史上曾有一次于 2026 年 4 月 16 日影响部分 Java SDK 用户的 Responses API 流式事件故障,该事件后来标记为已恢复;它说明状态页适合用于确认平台事件,但不能替代本机和代理层排查。OpenAI 状态页:Responses API Streaming Error
提交支持请求时,提供模型、错误消息和代码、请求时间及时区、脱敏后的请求参数、请求 ID 和客户端异常;不要提交 API Key、完整 Authorization 头或敏感用户内容。OpenAI Error codes
常见问题
OpenAI API 流式输出没有 [DONE],一定是失败吗?
不一定。对于 Chat Completions,先检查是否已经收到带有 finish_reason 的最终 chunk;对于 Responses API,检查 response.completed、response.incomplete 或 error。不同 SDK 和兼容实现对结束标记的暴露方式可能不同,不能只以 [DONE] 是否出现判定。
finish_reason=length 是网络断流吗?
通常不是。它表示生成达到请求指定的最大 token 数,应该先检查输出上限参数和上下文长度。只有在没有结束原因、同时出现 EOF、连接重置或超时异常时,才重点排查网络和代理。
为什么流式中断后没有 usage?
usage 可能只出现在最后的 usage chunk;官方文档说明,流被中断或取消时可能收不到这个最终 chunk。应保存请求 ID和时间,之后通过可用的用量面板或服务商账单核对。
429 后可以直接无限重试吗?
不可以。429 可能代表请求限流,也可能代表余额耗尽、项目消费上限或组织使用量上限。只有确认属于可恢复的限流或 slow_down,并遵循 Retry-After 或有限指数退避时,才适合重试。
如何判断是 OpenAI 故障还是自己的代理故障?
先用同一模型执行最小非流式请求,再用最小流式请求;同时记录 HTTP 状态码和请求 ID。若客户端有错误但服务健康或请求指标中没有对应记录,优先检查客户端超时、网络、反向代理和兼容网关。
OpenAI 兼容接口能否直接使用这些 finish_reason 和 usage 判断?
只能作为核对项,不能默认完全一致。兼容服务可能改变路径、模型映射、事件字段、usage 返回方式或错误结构,应以该服务商文档和实际最小请求为准。