手记

打造“打字机式”AI对话体验:SSE 流式输出从入门到企业级实践

当你使用 ChatGPT、豆包或 Claude 时,是否注意到回答内容是一个字一个字“蹦”出来的?这种流畅的流式交互(Streaming)不仅是用户体验的关键,更是现代 AI 对话产品的标配。

那么,如何在自己的项目中实现这一效果?答案是:Server-Sent Events(SSE)

相比 WebSocket,SSE 凭借其轻量、基于标准 HTTP、天然支持流式传输等优势,已成为当前 AI 应用中服务端向客户端推送生成内容的首选方案。本文将带你从原理到实战,彻底掌握 SSE 在 AI 场景下的落地方法。


一、SSE 是什么?为什么它适合 AI 流式输出?

SSE(Server-Sent Events) 是一种基于 HTTP/1.1 的服务端推送技术。它通过维持一个长连接,让服务器能够持续向客户端发送文本数据,而无需客户端轮询。

核心特性

  • 单向通信:仅支持 服务端 → 客户端 的数据流(完美契合 AI 回答场景);
  • 纯文本协议:默认使用 UTF-8 编码,天然兼容 JSON;
  • 自动重连:浏览器原生支持断线后自动重试;
  • 基于标准 HTTP:无需协议升级,穿透代理、CDN、防火墙更简单。

✅ 正因如此,SSE 成为 AI 对话中“模型一边生成、前端一边渲染”的理想载体。


二、基础用法:原生 EventSource 的局限

浏览器提供了 EventSource 接口用于接收 SSE 流:

const es = new EventSource('/api/chat/stream');

es.onmessage = (event) => {
  console.log('收到片段:', event.data);
};

es.onopen = () => console.log('连接建立');
es.onerror = (err) => console.error('连接出错', err);

但问题来了:

  • 仅支持 GET 请求:无法携带复杂的用户输入、上下文或鉴权信息;
  • 无法自定义 Header:比如传递 Authorization: Bearer xxx 几乎不可能;
  • 参数只能拼在 URL 中:既不安全,也不符合 RESTful 设计。

在 AI 场景中,用户提问通常包含多轮上下文、敏感 token 和复杂结构,必须通过 POST + JSON 传输。此时,原生 EventSource 就显得力不从心。


三、企业级方案:用 fetch-event-source 突破限制

微软开源的 @microsoft/fetch-event-source 库,正是为解决上述痛点而生。

它的优势包括:

  • 支持 POST、PUT 等任意 HTTP 方法
  • 可自由设置 请求头(Headers),轻松集成 JWT 鉴权;
  • 提供细粒度的 错误处理、重试策略与超时控制
  • 基于标准 fetch API,兼容现代前端框架。

实战封装示例(TypeScript)

import { fetchEventSource } from '@microsoft/fetch-event-source';
import { ElMessage } from 'element-plus';

let abortController: AbortController;

/**
 * 发起 SSE 流式请求
 */
export const requestStream = (
  url: string,
  headers: Record<string, string>,
  data: unknown,
  onChunk: (chunk: string) => void
) => {
  abortController = new AbortController();

  fetchEventSource(url, {
    method: 'POST',
    signal: abortController.signal,
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
    openWhenHidden: true, // 页面后台时仍保持连接

    onopen(response) {
      console.log('SSE 连接已建立');
    },

    onmessage(event) {
      if (event.data) {
        onChunk(event.data);
      }
    },

    onclose() {
      console.log('SSE 连接正常关闭');
    },

    onerror(error) {
      ElMessage.error('流式请求失败');
      throw error; // 抛出错误以触发重试或终止
    }
  });
};

/**
 * 主动中断请求(谨慎使用!)
 */
export const stopStream = () => {
  if (abortController) {
    abortController.abort();
    abortController = new AbortController();
  }
};

⚠️ 重要提醒:前端“停止” ≠ 后端“停止”

调用 abort() 仅会中断前端接收,后端仍在继续生成。这会导致:

  • 历史消息缺失;
  • Token 计费不一致;
  • 多端同步异常。

最佳实践:提供一个 /stop 接口,由前端调用通知后端终止生成,确保状态一致。


四、关键排坑:Nginx 代理必须关闭缓冲!

即使前后端代码正确,你可能仍会发现内容“整段弹出”,而非逐字流式显示。罪魁祸首往往是 Nginx 的响应缓冲机制

Nginx 默认会缓存后端响应,攒够一定量才转发给客户端,破坏了流式体验。

正确配置如下:

location ^~/api/v1/chat/ {
    proxy_pass http://your_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 3600s;  # 防止长连接被超时断开

    # 关键!关闭代理缓冲
    proxy_buffering off;

    # 启用分块传输(Chunked Transfer)
    chunked_transfer_encoding on;
}

🔧 proxy_buffering off 是实现“打字机效果”的必要条件


五、SSE vs WebSocket:为何 AI 场景偏爱 SSE?
特性 SSE WebSocket
通信方向 单向(Server → Client) 全双工(双向)
协议基础 标准 HTTP/1.1 独立 WS 协议(需 Upgrade)
数据格式 文本(UTF-8) 文本或二进制
连接开销 轻量,复用 HTTP 连接 需握手、维护独立连接
穿透性 天然兼容 CDN、网关、负载均衡 部分中间件需特殊配置
适用场景 服务端持续推送(如 AI 生成) 实时双向交互(如聊天室、游戏)

在 AI 对话中,前端只发一次请求,后端持续返回——这正是 SSE 的“舒适区”。而 WebSocket 的双向能力在此属于“过度设计”,徒增复杂度。


结语:流式体验,细节决定成败

实现“打字机式”AI 回答,看似只是前端加个流式渲染,实则涉及:

  • 后端分块输出(Transfer-Encoding: chunked);
  • 前端使用合适的 SSE 客户端;
  • 网关/Nginx 关闭缓冲;
  • 状态一致性保障(避免前端强行中断)。

SSE 虽简单,却承载着现代 AI 产品最核心的交互体验。掌握它,你就掌握了通往专业级 AI 应用的第一把钥匙。

💡 小建议:在调试时,可用 curl -N http://your-api/stream 查看原始流式响应,快速定位是后端未分块,还是前端/Nginx 缓冲问题。

让每一字都“呼吸”起来,才是真正的 AI 对话体验。

0人推荐
随时随地看视频
慕课网APP