New API

API 流式输出中断怎么办?一套分层排查流程

更新于 2026-10-26 · poloapi.cyou

简答:API 流式输出中断不要先重试,先判断是否能完成非流式请求,再用 curl --no-buffer 观察原始 SSE。若 curl 正常而 SDK 或页面中断,重点查解析、AbortController、读取空闲超时与代理缓冲;若 curl 也中断,再核对 Base URL、模型、Key、网关超时和上游事件。

定义:API 流式输出中断,是指请求已经建立或返回 HTTP 200 后,SSE 数据没有出现完整结束事件、连接提前关闭,或客户端因超时、取消和解析错误停止读取。

先做这三步:把问题定位到客户端还是接口链路

本文是一篇独立的 AI API 接入排查指南,不代表任何 API 服务商或模型平台的官方立场。对于 OpenAI 兼容接口,具体事件名、结束标记和错误字段仍应以实际接口文档为准。

最有效的顺序不是直接更换模型,而是先建立三个基线:非流式请求是否成功、原始 SSE 是否持续返回、业务客户端是否正确解析。OpenAI 的 Responses API 在启用 stream: true 后会通过 Server-Sent Events 逐步发送事件;官方事件参考中包含创建、进行中、完成和失败等不同事件类型,不能只根据 HTTP 200 判断请求已经成功。

可先用下面的命令检查接口地址、模型名和 Key 是否基本可用。示例中的 BASE_URL、MODEL 和 API_KEY 都需要替换为你的实际配置,Key 不要写进 URL 查询参数。

#!/usr/bin/env bash
set -eu

command -v curl >/dev/null || { echo "缺少 curl" >&2; exit 1; }
command -v jq >/dev/null || { echo "缺少 jq" >&2; exit 1; }

read -r -p "Base URL,例如 https://api.example.com/v1: " BASE_URL
read -r -p "模型名: " MODEL
read -r -s -p "API Key: " API_KEY
echo

BASE_URL="${BASE_URL%/}"
BODY=$(jq -n --arg model "$MODEL" '{model:$model,input:"请只回复:ok"}')

curl --silent --show-error --max-time 60 \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  "$BASE_URL/responses" \
  -d "$BODY"

如果非流式请求失败,先处理地址、模型、权限或上游错误;如果非流式成功,再测试流式请求。curl 的 --no-buffer 会尽快输出收到的数据,--max-time 则限制单次传输总时长。需要注意,Authorization 头可能出现在本机进程参数或调试日志中,因此不要把完整命令直接发到工单或聊天群。

curl --no-buffer --silent --show-error --max-time 120 \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  "$BASE_URL/responses" \
  -d "$(jq -n --arg model "$MODEL" '{model:$model,input:"请输出一段较长的测试文本",stream:true}')"

若接口实际使用 Chat Completions 路径,可将请求路径和请求体改成该接口要求的格式。官方 Chat Completions 流式事件通常以增量 chunk 返回,Responses API 则使用事件类型和字段结构,两者不能混用解析器。可分别参考 Responses 流式事件参考 和 Chat Completions 流式事件参考。

按现象判断:无首字节、批量出现、半途断开还是一直转圈

现象优先怀疑位置下一步动作
长时间没有任何数据连接建立、模型排队、网关读取超时或上游未产生首个事件记录请求开始时间,先用 curl 测试,并区分连接超时与首字节等待时间
内容一次性成批出现代理或网关响应缓冲,客户端也可能在积累完整块后才刷新检查响应头、网关缓冲配置和客户端读取循环;Nginx 可核对 proxy_buffering
输出到一半停止上游连接关闭、读取空闲超时、代理断开、解析器遇到拆包或非法 JSON保留最后一个完整事件,查看是否有失败事件或完成事件,再对比 curl 原始输出
HTTP 200 但界面一直转圈客户端等待错误的结束标记,或没有处理完成事件按事件类型判断结束,不要只等待固定字符串;同时记录最后收到的事件类型
浏览器偶发断开并自动重连EventSource 默认重连、浏览器连接限制、网络变化或服务端主动关闭明确区分自动重连与重复生成,必要时使用请求 ID 或业务幂等键
非流式成功,流式失败流式路径、SSE 格式、代理长连接或兼容层实现差异确认请求体中的 stream 类型、响应的 Content-Type 和原始事件边界

SSE 不是普通的逐行 JSON。规范要求事件流使用 UTF-8 编码,消息之间用空行分隔;一个事件可以包含多行 data:,客户端需要把同一事件的多行数据合并后再解析。因此,按每次网络读取直接调用 JSON.parse,可能在数据被拆包时失败。MDN 对 SSE 的事件格式、UTF-8、空行分隔、多行 data 和重连行为有完整说明,可参考 Using server-sent events。

一个重要判断是:连接关闭不等于正常完成。Responses API 应检查是否收到完成事件或失败事件;Chat Completions 则应检查最后 chunk 的结束字段。对于兼容接口,如果只返回通用 data 块或自定义结束标记,应把该接口的真实样本保存下来再编写解析规则,不能凭经验假设所有平台都返回相同字段。

Node.js、Python 和浏览器 Fetch 的最小自查方式

Node.js:把读取超时和主动取消分开

Node.js 的 AbortSignal.timeout() 可以为整个 fetch 设置截止时间;如果业务还需要“连续一段时间没有新数据就中止”,应在每次成功读取后单独刷新空闲计时器。两者不是同一个概念:总超时限制整个请求,读取空闲超时限制两次数据之间允许停顿的时间。Node.js 官方文档说明了 AbortSignal.timeout() 和 Fetch 的可用方式,可参考 Node.js Global objects 文档。

const BASE_URL = process.env.BASE_URL.replace(/\/$/, "");
const API_KEY = process.env.API_KEY;
const MODEL = process.env.MODEL;

const response = await fetch(`${BASE_URL}/responses`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "Accept": "text/event-stream"
  },
  body: JSON.stringify({
    model: MODEL,
    input: "请输出一段测试文本",
    stream: true
  }),
  signal: AbortSignal.timeout(120000)
});

if (!response.ok) {
  const text = await response.text();
  throw new Error(`HTTP ${response.status}: ${text}`);
}
if (!response.body) throw new Error("响应没有可读取的 body");

const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let lastEventAt = Date.now();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  lastEventAt = Date.now();
  buffer += decoder.decode(value, { stream: true });

  const parts = buffer.split("\n\n");
  buffer = parts.pop();
  for (const part of parts) {
    if (!part.trim()) continue;
    console.log("RAW EVENT:", part);
  }
}

console.log("stream closed; last data age(ms)=", Date.now() - lastEventAt);

这段代码故意先打印原始事件,不直接假设事件字段。确认原始流稳定后,再针对实际协议处理 type、delta、response.completed、response.failed 或 Chat Completions 的 chunk 字段。

Python:不要把网络分块当成完整事件

import json
import os
import requests

base_url = os.environ["BASE_URL"].rstrip("/")
api_key = os.environ["API_KEY"]
model = os.environ["MODEL"]

payload = {
    "model": model,
    "input": "请输出一段测试文本",
    "stream": True,
}

with requests.post(
    f"{base_url}/responses",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Accept": "text/event-stream",
    },
    json=payload,
    stream=True,
    timeout=(10, 120),
) as response:
    response.raise_for_status()
    event_lines = []
    for line in response.iter_lines(decode_unicode=True):
        if line == "":
            if event_lines:
                print("RAW EVENT:", "\\n".join(event_lines))
                event_lines = []
            continue
        event_lines.append(line)

    if event_lines:
        print("INCOMPLETE EVENT:", "\\n".join(event_lines))

示例中的连接超时和读取超时只是诊断模板,具体数值应按模型生成时间、网关限制和业务容忍度调整。一个请求可能已经产生部分输出但没有完成事件,此时不要把已显示文本直接拼接后无条件重试,否则可能得到重复内容,甚至产生重复计费。

浏览器 Fetch:检查 AbortController、CORS 和自动重连

const controller = new AbortController();
const timer = setTimeout(() => controller.abort("total timeout"), 120000);

try {
  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message: "测试" }),
    signal: controller.signal
  });

  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  if (!res.body) throw new Error("浏览器没有返回可读流");

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  for (;;) {
    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()) console.log(event);
    }
  }
} catch (error) {
  console.error("stream error:", error);
} finally {
  clearTimeout(timer);
}

如果使用原生 EventSource,浏览器在连接关闭后可能自动重连;这适合事件订阅,但不一定适合会产生模型输出的 POST 请求。对于生成任务,应明确记录请求 ID、是否已经收到部分内容以及是否收到完成事件,避免把自动重连误当成安全续传。

代理、Nginx 和网关:重点查缓冲与读取空闲超时

如果 curl 能看到原始事件,但经过 Nginx、CDN、API 网关或业务后端后变成一次性返回,优先检查响应缓冲。Nginx 官方文档显示,proxy_buffering 默认开启;关闭后,响应会在从上游收到时同步传给客户端。对于流式接口,还应核对响应是否使用 Content-Type: text/event-stream,以及中间层是否改写、压缩或缓存了响应。

location /api/stream {
    proxy_pass http://upstream_api;
    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 300s;
    proxy_send_timeout 60s;
}

上面的配置只是排查模板,不能直接替代你的生产配置。Nginx 的 proxy_read_timeout 是两次读取之间的等待时间,不是整个响应的总时长;如果上游在该时间内没有发送任何数据,连接会被关闭。若模型生成期间可能出现较长空档,应由服务端发送合适的 SSE 注释或心跳,同时确保客户端不会把心跳当成模型文本。相关定义可查看 Nginx ngx_http_proxy_module 文档。

建议按以下顺序对比:

  1. 在应用服务器所在机器直接请求上游,记录响应头和最后事件。
  2. 绕过业务后端请求网关入口,比较首字节时间和事件到达间隔。
  3. 在浏览器或 SDK 中请求同一路径,确认是否只是前端显示或解析延迟。
  4. 逐层检查响应缓冲、压缩、缓存、连接复用、空闲超时和最大响应时间。

如果只能修改客户端,先把原始响应写入日志或文件,确认中断发生在网络层还是解析层;如果只能修改网关,优先保留请求 ID、上游状态、连接关闭原因和最后一次转发时间。

HTTP 200 仍可能失败:如何判断是否完整成功

流式请求的成功条件至少包括三层:HTTP 状态码正常、事件流格式可解析、协议层出现明确的完成状态。HTTP 200 只说明服务器接受并开始返回响应,不代表模型已经完整生成。

证据可得出的结论不能直接得出的结论
HTTP 200请求在 HTTP 层获得成功响应不能证明模型输出完整,也不能证明客户端已读完
收到增量文本至少有部分输出到达客户端不能证明没有后续错误或输出截断
收到完成事件按该协议的事件语义,服务端报告生成完成不能证明你的业务已经正确保存内容
收到失败事件服务端明确报告失败不能仅凭错误文本判断责任一定在上游
连接直接 EOF传输层或中间层结束了连接不能当作正常完成,除非协议明确规定 EOF 即结束

Responses API 官方事件参考包含 response.completed 和 response.failed 等事件;如果使用的是兼容层,可能只映射其中一部分事件,或者返回不同的结束标志。因此,排查时应保存最后 20 个原始事件、HTTP 响应头、模型名、请求 ID、客户端版本和各层耗时,不要只保存页面上已经显示的文本。

安全重试也要分情况处理:连接在首个事件前失败时,通常更容易判断请求未产生可见输出;已经收到部分文本后断开,则不能默认从中断位置继续,也不能默认再次请求不会重复生成。除非接口提供明确的响应恢复、游标或幂等机制,否则应把重试视为一次新的模型请求,并在业务层提示用户确认。

一份可提交给服务商或内部网关团队的脱敏记录

为了避免反复沟通,建议一次性提供以下信息:

时间:2026-10-26 14:30:00 -0500
接口类型:Responses / Chat Completions / 兼容接口
模型:已脱敏模型名
HTTP 状态:200
首字节耗时:已记录
最后事件时间:已记录
完成事件:是 / 否 / 未知
失败事件:是 / 否 / 未知
客户端:Node.js 版本或 Python 版本
链路:客户端 → 业务后端 → Nginx/CDN → 上游
复现命令:已脱敏,Key 未提交
请求 ID:仅保留非敏感标识

如果需要确认服务状态或延迟分布,可结合平台的服务状态页、用量面板和请求日志进行比对;OpenAI 的错误与延迟排查说明也建议结合请求时间、请求 ID、模型和服务层级定位,而不是只看前端现象,详见 Troubleshooting API Errors and Latency。

常见问题

API 流式输出中断,先换模型还是先查客户端?

先做非流式和 curl 原始流测试。若 curl 稳定而 SDK 或页面中断,应优先查 SSE 拆包、完成事件、AbortController、读取空闲超时和代理缓冲;只有原始流也失败时,再比较模型、上游通道和接口状态。

为什么 API 返回 200,内容还是没有完整生成?

HTTP 200 只代表 HTTP 层已经开始返回响应。流式请求还要确认事件可以持续解析,并收到该协议定义的完成事件;如果连接直接 EOF、收到失败事件或最后一个事件不完整,都不能直接当作正常完成。

SSE 解析时为什么不能每读一块就 JSON.parse?

网络读取边界不等于 SSE 事件边界,一个事件可能被拆成多次读取,也可能包含多行 data。应先按空行组装完整事件,再合并同一事件的 data 内容,最后按实际协议解析 JSON。

Nginx 的 proxy_read_timeout 是整个请求的最大时长吗?

不是。Nginx 文档说明它限制的是两次读取之间的等待时间;如果上游在该时间内没有发送数据,连接会被关闭。整个请求的总时长还可能受到客户端、网关、CDN、SDK 或业务代码的其他超时限制。

流式输出中断后能不能直接重试?

可以重试,但不能默认安全续传。首个事件前失败与已经收到部分文本后失败的风险不同;后者可能造成重复生成或重复计费。除非接口明确提供游标、恢复或幂等机制,否则应记录原请求状态,并把重试当作一次新的请求。

相关阅读

参考资料

  1. Responses streaming events
  2. Chat Completions streaming events
  3. Create a model response
  4. Using server-sent events
  5. Module ngx_http_proxy_module(nginx.org)
  6. curl command line options(curl.se)
  7. Troubleshooting API Errors and Latency