在大语言模型(LLM)的交互场景中,流式输出已成为提升用户体验的关键技术。无论是 ChatGPT 的打字机效果,还是各类 AI 助手的实时响应,都依赖于流式传输协议将模型生成的 token 逐段推送给客户端。然而,网络波动、客户端异常等情况可能导致传输中断,如何实现无缝的断点重传就成为技术实现的核心挑战。

        本文将深入对比两种主流流式传输方案 ——Streamable HTTP 和 Server-Sent Events(SSE)在处理输出中断时的断点重传机制,分析其技术原理、实现差异及适用场景。在chat model这类ai应用的早期,通常后端工程是用sse+http来实现的打字机效果的流式输出。25年3月,Model Context Protocol (MCP) 引入了一种新的传输机制 Streamable HTTP,它为服务器到客户端的实时通信提供了更优雅的解决方案。

什么是流式输出与断点重传?

在讨论具体技术前,我们需要明确两个核心概念:

  • 流式输出:指模型生成的内容(token)不是一次性返回,而是分批次、逐段推送给客户端的传输方式。这种方式能显著降低用户等待感,让 AI 响应看起来更自然流畅。

  • 断点重传:当流式传输过程中发生意外中断(如网络断开、客户端崩溃),客户端重新连接后,能够从断开的位置继续接收剩余内容,而非重新接收全部内容的机制。

断点重传的核心价值在于:

  • 减少带宽浪费,避免重复传输已接收内容
  • 提升用户体验,中断后无需重新等待整个响应
  • 保证数据完整性,确保最终接收内容的完整性

Streamable HTTP 与 SSE 的本质区别

在深入断点重传机制前,我们需要先理解这两种技术的本质差异:

Streamable HTTP 并不是一个标准化协议,而是对 HTTP 协议进行扩展以支持流式传输的实现方式的统称。它通常基于 HTTP 1.1 或 HTTP/2 的持久连接特性,通过分块传输编码(Chunked Transfer Encoding)实现流式数据传输。

SSE(Server-Sent Events) 则是一个标准化协议(定义于 HTML5 规范),专门设计用于服务器向客户端单向推送实时数据。SSE 基于 HTTP 协议,使用 text/event-stream 内容类型,并定义了特定的消息格式。

两者的核心区别在于:SSE 是一个标准化的、具有特定格式和行为的协议,而 Streamable HTTP 是一个更宽泛的概念,通常指任何利用 HTTP 实现流式传输的方案。

断点重传的核心要素

无论采用哪种技术,实现断点重传都需要三个核心要素:

  1. 会话标识(Session ID):用于唯一标识一个流式传输会话,使服务器能够定位中断前的上下文。

  2. 断点标记(Checkpoint):记录客户端已成功接收的最后一个数据单元(token)的位置或标识。

  3. 状态存储:服务器端需要保存未完成的流式传输状态,包括已生成的所有数据单元和当前生成进度。

Streamable HTTP 的断点重传实现

1. 会话初始化与标识

当客户端首次发起流式请求时,服务器生成一个唯一的会话 ID 并返回给客户端:

// 客户端请求
POST /stream HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "prompt": "请解释什么是人工智能"
}

// 服务器响应
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
X-Session-ID: ai-stream-123456

{ "token": "人", "index": 0, "done": false }

客户端存储此会话 ID(通常在内存或本地存储中),用于后续可能的重连。

2. 正常流式传输

在正常传输过程中,服务器通过分块编码逐段发送 token,并在每个块中包含当前 token 的索引:

// 后续分块传输
{ "token": "工", "index": 1, "done": false }
{ "token": "智", "index": 2, "done": false }
{ "token": "能", "index": 3, "done": true }

3. 中断检测与重连

当客户端检测到连接中断(如超时、网络错误),会使用保存的会话 ID 发起重连请求,并在请求头中指定最后成功接收的 token 索引:

// 重连请求
POST /stream/resume HTTP/1.1
Host: api.example.com
X-Session-ID: ai-stream-123456
X-Last-Index: 1

4. 服务器处理重连

服务器收到重连请求后,执行以下操作:

  1. 验证会话 ID 的有效性和状态
  2. 检查请求的最后索引是否合理
  3. 从指定索引开始继续发送剩余 token
// 重连响应
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
X-Session-ID: ai-stream-123456

{ "token": "智", "index": 2, "done": false }
{ "token": "能", "index": 3, "done": true }

5. 服务器端状态管理

为支持上述流程,服务器需要维护每个会话的状态信息,通常使用 Redis 等缓存服务:

# 服务器端会话状态存储示例(Python/Redis)
import redis
r = redis.Redis(host='localhost', port=6379, db=0)

def save_session_state(session_id, tokens, last_sent_index, is_completed):
    state = {
        "tokens": tokens,           # 已生成的所有token列表
        "last_sent_index": last_sent_index,  # 最后发送的token索引
        "is_completed": is_completed,      # 生成是否完成
        "expires_at": time.time() + 3600   # 会话过期时间(1小时后)
    }
    r.set(f"stream:{session_id}", json.dumps(state), ex=3600)

SSE 的断点重传实现

SSE 作为标准化协议,内置了对重传的支持

1. 初始化

客户端通过普通 HTTP 请求建立 SSE 连接,服务器返回特定的 text/event-stream 内容类型:

// 客户端请求
GET /stream/sse HTTP/1.1
Host: api.example.com
Accept: text/event-stream

// 服务器响应
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

2. 正常流式传输

SSE 定义了标准化的消息格式,每个消息可以包含 id(事件 ID)、data(数据)和 retry(重连间隔)字段:

id: 0
data: 人
retry: 1000

id: 1
data: 工

id: 2
data: 智

id: 3
data: 能
event: complete

客户端自动记录最后接收的事件 ID(id 字段的值)。

3. 中断与自动重连

SSE 客户端(如浏览器内置的 EventSource)具有自动重连机制。当连接中断时,客户端会根据 retry 字段指定的毫秒数进行重试。

重连时,客户端会在请求头中包含 Last-Event-ID 字段,告知服务器最后成功接收的事件 ID:

// SSE重连请求
GET /stream/sse HTTP/1.1
Host: api.example.com
Accept: text/event-stream
Last-Event-ID: 1

4. 服务器处理 SSE 重连

服务器收到包含 Last-Event-ID 的请求后,从该 ID 对应的下一个事件开始继续发送:

id: 2
data: 智

id: 3
data: 能
event: complete

5. SSE 重传的客户端实现

浏览器原生支持 SSE 重连机制,使用 EventSource 接口即可:

// 客户端SSE实现
const source = new EventSource('/stream/sse');

// 接收消息
source.onmessage = function(event) {
  console.log('Received token:', event.data);
  // event.lastEventId 自动记录最后接收的ID
};

// 连接中断时的处理
source.onerror = function(error) {
  console.log('Connection error:', error);
  // 无需手动重连,EventSource会自动重试
};

技术对比与优劣势分析

特性 Streamable HTTP SSE
标准化 非标准化,自定义实现 标准化协议
重连机制 需要手动实现 客户端自动重连
断点标识 自定义(如 X-Last-Index) 标准 Last-Event-ID 头
消息格式 自定义(通常为 JSON) 固定格式(id、data、event 等字段)
双向通信 可扩展支持 仅服务器到客户端单向
浏览器支持 所有浏览器 大多数现代浏览器(IE 不支持)
灵活性 高,可完全自定义 中,受限于标准
实现复杂度 较高,需处理各种边缘情况 较低,利用标准实现
适用场景 复杂流式交互,需要双向通信 简单单向流式推送

Streamable HTTP 的断点续传机制与其他协议(如 SSE)相比,具有以下优势:

  • 支持会话状态恢复:Streamable HTTP 引入了会话 ID 机制,连接中断时,客户端可使用之前的会话 ID 重新连接,服务器可恢复会话状态继续之前的交互。而 SSE 连接断开时,所有会话状态丢失,客户端必须重新建立连接并初始化整个会话
  • SSE 的 “断点重传” 高度依赖 客户端原生行为(如浏览器 EventSource 自动携带 Last-Event-ID)和 服务器端的简单状态存储(通常仅存 “最后事件 ID 对应的内容”)。但 SSE 没有标准的 “会话 ID” 机制,服务器很难关联 “同一用户的多次连接”—— 比如:用户第一次连接用浏览器,中断后用手机重连,SSE 无法识别这是同一个会话,只能重新开始。

  • Streamable HTTP 的优势
    Streamable HTTP 强制引入 全局唯一的会话 ID(如 X-Session-ID: stream-123),并将 “完整的业务会话状态”(包括用户查询、所有已生成的 token、输出进度、甚至模型参数)主动存储在服务器端(如 Redis、数据库)。无论客户端用什么设备、通过什么网络重连,只要携带正确的会话 ID,服务器就能准确找到 “属于这个用户的交互清单”,恢复所有上下文

  • 兼容性更好:Streamable HTTP 可在浏览器、Node.js、Python、Go 等环境中统一实现,并原生支持现代 HTTP 协议,极大简化了多端开发。相比之下,SSE 主要是浏览器专属的协议,在其他环境中的支持和兼容性较差。
  • 适应复杂事件流传输:Streamable HTTP 能够以 ReadableStream 的形式持续传输 JSON、NDJSON、Protobuf 等结构化数据,适用于 MCP 中如 token、tool - call、log、error、result 等复杂事件流的传输需求。而 SSE 在传输复杂事件流方面相对较弱。
  • 资源利用更高效Streamable HTTP 支持无状态服务器,无需维持高可用的长连接,服务器可以灵活选择返回标准 HTTP 响应,也可以跃迁到 SSE 事件流,按需分配资源。SSE 则需要服务器一直保持一个稳定的长连接,资源消耗较高。
  • 更易集成现有基础设施:Streamable HTTP 因为 “只是 HTTP”,可以与中间件和现有基础设施良好集成,例如兼容防火墙、代理、负载均衡等。而 SSE 在与一些中间件集成时,容易被中断,兼容性较差。

断点重传的进阶考量

无论采用哪种技术,实现可靠的断点重传还需要考虑以下因素:

1. 会话过期策略

服务器不可能无限期保存会话状态,需要设置合理的过期时间。通常根据业务场景设置为 30 分钟到 24 小时。

2. 幂等性设计

确保重传机制的幂等性,即多次重传同一请求不会导致错误或重复数据。

3. 异常处理

  • 会话已过期:需要告知客户端重新发起请求
  • 断点索引无效:处理客户端提供的索引超出有效范围的情况
  • 部分生成:处理模型尚未完成全部生成时的中断情况

4. 性能优化

  • 会话状态存储应选择高性能存储(如 Redis)
  • 考虑批量发送多个 token 以减少传输开销
  • 实现流量控制,避免重传导致服务器负载过高
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐