写在前面: 流式输出就和它的名字一样,呈现一种水流形式一点一点进行输出。它是服务端对客户端的一种响应形式,这种输出使得响应数据可以一点点加载到页面上,避免用户等待很长时间,极大提高了用户体验。本文主要讲前端如何实现 SSE(Server-Sent Events)输出加载。
流式输出的概念
什么是流式输出?
流式输出就和它的名字一样,呈现一种水流形式一点一点进行输出。它是服务端对客户端的一种响应形式,这种输出使得响应数据可以一点点加载到页面上,避免用户等待很长时间,极大提高了用户体验。
图中所示就是一种典型的流式输出(用户和聊天机器人聊天的网络请求响应): SSE 是单向输出,仅支持服务器向客户端发送消息。但在 AI 时代,用户通常需要向服务端传输复杂的 Body 信息数据(如 Prompt、上下文历史等),因此大多数现代流式输出方案都是基于 POST 形式的 HTTP 请求实现的。
为什么要有流式输出?
- 即时反馈:早期的大语言模型生成内容时是逐个 Token 产生的,如果等待内容全部生成才显示,用户需要忍受漫长的空白等待。
- 降低感知延迟:流式输出可以解决这个问题,显著减少用户感知的延迟,极大提高了用户的响应感官速度。
流式输出的实现
核心实现方案:Fetch API
对于这种不寻常的输出,前端应该怎么做? SSE 有它自己独特的响应接收形式,我们需要使用最底层的请求方式来实现它。我们通过 Fetch API 来实现。
- EventSource 的局限性:原生
EventSource只支持 GET 请求,无法在 Body 中发送 JSON 数据,且不支持自定义 Headers(如Authorization),这在现代鉴权系统中是致命的。 - Fetch 的优势:Fetch 支持 POST,支持自定义 Headers,且通过
response.body.getReader()可以实现与 SSE 完全一致的流式读取。
实现步骤详解
第一步: 建立连接 流式输出虽然是获取数据,但由于需要携带复杂的 Data,通常使用 POST 方法并配置相应的请求头。
第二步: 使用 Streams API 实现流式读取 传统的 API 是等待整个 Response 下载完成,但使用 Stream 可以实现:后端生成一个字,前端收到一个字。内存占用极低,且用户能立即看到反馈。
const reader = response.body.getReader();
const { done, value } = await reader.read();