# LLM 流式输出中断：区分 SSE、超时和完成状态

> 排查 LLM 流式输出提前停止，区分网络分片、协议事件、结束原因和用量记录，避免把部分文字当成成功答案。

_发布 2026-09-29 · 更新 2026-09-29 · [KeepRouter Editorial](https://keeprouter.com/editorial-policy#editorial-team) · 5 分钟阅读_

![拆分客户端、网关、供应商、重试与生成阶段的端到端 AI 请求延迟轨迹](https://keeprouter.com/editorial/blog/ai-gateway-latency-guide.png)

_区分首个可见事件与完整任务结束。概念示意图。_

LLM 流式响应只有到达对应协议的有效终态，才算完成。收到文字或 HTTP 200 都不够：连接可能在中途断开、输出达到上限，也可能是解析器丢掉了跨网络分片的数据。

已经能显示文字、但偶尔截断的应用，应先定位证据在哪一层消失。

## 网络分片不是协议事件

[MDN 的 SSE 说明](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)定义了事件边界。一次网络读取可能包含半个事件或多个事件，UTF-8 字符也可能跨读取。

使用流式解码和协议解析，或官方 SDK。不要对每个网络块直接 `JSON.parse`；未完整数据需要缓存。本地能工作，并不能证明不同网络分片下也正确。

| 信号 | 能说明什么 | 不能说明什么 |
| --- | --- | --- |
| HTTP 200 | 初始响应被接受 | 生成结束 |
| 网络读取 | 收到字节 | 收到完整事件 |
| 文本增量 | 有部分内容 | 答案完整 |
| 结束事件 | 协议到达某种终态 | 所有终态都算成功 |
| 用量记录 | 提供了测量字段 | 缺失应填成零 |

## 按实际 API 识别结束状态

Chat Completions、Responses 和 Anthropic Messages 使用不同事件格式。[Claude 流式文档](https://platform.claude.com/docs/en/build-with-claude/streaming)描述了消息块和错误处理，不能直接沿用另一种协议的结束标志。

分别测试正常结束、输出上限、拒答、工具移交和显式错误。工具移交可以结束一次模型请求，但用户任务尚未结束。

```python
def result_state(text, finish_reason, transport_error=False):
    if transport_error:
        return "interrupted"
    if finish_reason == "length":
        return "truncated"
    if finish_reason == "tool_calls":
        return "needs_tool"
    if finish_reason == "stop":
        return "complete"
    return "incomplete"

assert result_state("半段答案", None) == "incomplete"
assert result_state("完整答案", "stop") == "complete"
assert result_state("半个 JSON", "length") == "truncated"
```

这是本地状态示例，不是 SSE 解析器，也不是真实供应商响应。具体实现应按 API 协议适配；空的正常响应仍需业务层判断是否有用。

## 一层一层复现

先对同一短问题关闭流式。如果成功，再在后端消费流但不渲染，最后加入前端显示。这样可以区分模型、后端解析、代理与 UI 状态。

记录事件类型和时间，不记录私有文本。分别观察请求开始、首事件、首段文字和终态。代理缓冲、空闲超时与总请求截止时间也是不同问题；提高超时不会修复解析器。

## 如实处理用户取消

用户停止时，通过客户端中止请求，并把回答标为已取消。保留部分草稿可以，但不能称为完整答案。

取消不代表上游计算和计费在同一瞬间停止。若断流前未收到最终用量，标为未知，等待可靠记录，不补零。工具参数不完整时不能执行，见[工具循环指南](/zh/blog/llm-tool-calling-loop)。

## 重试时分开两次结果

部分输出后重试可能产生两个重叠答案，应保留独立尝试，由应用决定采用哪一个。不能把第二个 JSON 直接拼到第一个被截断的对象后面。

涉及副作用时，先确认动作是否已完成。模型请求重试与业务动作重试不是同一件事。

从[错误参考](/docs/errors)和 [API 文档](/api/docs)确认线路行为，再沿真实部署路径复现。用[延迟指南](/zh/blog/ai-gateway-latency-guide)区分首字时间与完整完成时间，最后比较合格回答及总费用。

## 常见问题

### HTTP 200 表示流式回答完成了吗？

不表示。它只说明初始响应，需要继续检查结束事件与原因，区分截断、工具移交和取消。

### 可以把每个网络分片直接当 JSON 解析吗？

不能。网络分片不对应 SSE 事件边界，需要缓存完整事件或使用相应 SDK。

### 取消流式请求就不收费了吗？

不能这样假设。计算可能已经发生，应按服务计费规则和可靠用量记录核对，而不是只看可见文字长度。

## 参考的一手资料

_本文最近复核 2026-09-29_

1. [MDN Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
2. [Claude streaming events](https://platform.claude.com/docs/en/build-with-claude/streaming)

## 继续阅读

- [LLM 工具调用：实现完整的请求与结果循环](https://keeprouter.com/zh/blog/llm-tool-calling-loop.md)
- [OpenAI 兼容 API 的 429 错误：限流、额度与重试](https://keeprouter.com/zh/blog/openai-compatible-api-429-errors.md)
- [AI Gateway 延迟指南：分别测量网关、供应商与获救请求](https://keeprouter.com/zh/blog/ai-gateway-latency-guide.md)

## 检查应用需要的具体行为

把文中例子用于应用时，按产品记录的请求格式与控制范围实现。

[查看产品说明](https://keeprouter.com/docs/errors)

[创建 Key，测试免费模型](https://keeprouter.com/login?returnTo=%2Fconsole%2Fkeys%3Fmodel%3Dfree)

免费测试使用 free 模型；其他付费型号需要足够预付额度。

[全部文章](https://keeprouter.com/zh/blog.md) · [模型与价格](https://keeprouter.com/models.md)
