深入理解流式输出:原理、应用与实现

997 阅读4分钟

深入理解流式输出:原理、应用与实现

bible-2778631_1280.jpg

无论是与 AIGC 大模型对话,还是加载网页内容,“等待” 都成为了影响用户体验的大敌。而流式输出,正是应对这一挑战的有效技术手段。

一、什么是流式输出

流式输出,简单来说,就是数据不是一次性全部传输或展示,*而是像水流一样,*逐段、逐部分地进行传输与呈现。

  • 数据传输与展示方式:数据并非一次性全部传输或展示,而是逐段、逐部分地进行传输与呈现,以小块形式逐步发送到接收端。

  • 优点:接收端可在接收数据过程中,立即处理和展示已收到的数据,打破传统 “全有或全无” 的传输模式,提升数据传输与展示效率和用户体验。

二、为什么需要流式输出

  1. 契合 AIGC 大模型输出特性

    AIGC 大模型基于 Transformer 架构,按 “token-by-token” 方式生成内容,模型根据前面已生成的 token,通过复杂的计算推理出下一个最可能的 token,如此循环,逐步构建出完整的输出内容。

  2. 优化用户体验

    若一次性输出,处理长文本或复杂内容时容易导致用户等待时间长,体验差。采用流式输出可以:

    • 前端注重用户体验,在数据生成或获取耗时情况下,流式输出让用户感觉内容实时生成输出,而非长时间空白等待。
    • 流式输出与模型生成过程匹配,用户能看到内容逐步呈现,缓解等待焦虑。

例如:网页加载,采用流式输出可先展示页面框架和部分文字,再逐步填充图片和详细内容,提升用户对产品好感度和使用意愿。

三、流式输出的实现

理解AIGC 生成内容方式之后,要为用户提供一个良好的体验,就需要达成这样的一个效果:大模型生成一个内容我们就立即将其推送给前端页面。这就需要建立一个服务端到客户端长链接,由于传统 HTTP 请求响应式协议传输完成后连接断开,无法满足流式输出需求。

所有流式输出是基于Server-Sent Events(服务发送事件SSE)。SSE是一种允许服务器通过HTTP向客户端推送实时更新的技术。它是WebSockets的一种更简单的替代方案,专门用于单向的服务器到客户端通信。

  1. 后端实现(Node)

    • 基础环境搭建:

      const express = require("express");
      const app = express();
      const http = require("http").Server(app);
      
      http.listen(3000, () => {
        console.log("服务器启动成功");
      });
      app.get("/", (req, res) => {
        // 返回index.html
        // __dirname表示文件根路径
        res.sendFile(__dirname表示文件根路径 + "/index.html");
      });
      

      这段代码创建了 Express 应用实例并绑定到 HTTP 服务器,监听 3000 端口。当服务器启动并访问后会通过sendFile方法返回首页 HTML。

    • 创建路由:

      app.get("/get", (req, res) => {
        res.set({
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          Connection: "keep-alive",
        });
        res.flushHeaders();
      
        setInterval(() => {
          let num = Math.floor(Math.random() * 10);
          res.write(`data:${num}\n\n`);
        }, 1000);
      });
      

      这里的响应头配置是 SSE 的核心

      • Content-Type: text/event-stream:告知客户端这是一个事件流响应,触发浏览器启用 SSE 机制
      • Cache-Control: no-cache:禁止缓存,确保数据实时更新。(若不设置则每次请求都会区缓存区拿之前请求的数据)
      • Connection: keep-alive:保持 HTTP 连接持续打开,为流式传输提供通道基础

      这里 res.flushHeaders() 的作用是发送HTTP响应头给客户端,告诉客户端建立实时数据流传输通道。

      通过res.flushHeaders()刷新响应头后,服务器进入定时推送状态:利用setInterval每秒生成 0-9 的随机数,通过res.write按 SSE 格式发送数据。格式规范要求以data:前缀开头,并用两个换行符\n\n作为消息结束标记,这确保了客户端能正确解析分段数据。

  2. 前端实现

    • 前端通过 HTML5 的EventSource API 建立与服务器的长连接,核心代码如下:

        <body>
          <h1>流式输出</h1>
          <div>
            <button id="getBut">开始</button>
          </div>
          <div id="output"></div>
          <script>
            // 状态控制变量
            let isStreaming = false;
            let eventSource = null;
            // 优化后的控制逻辑
            document.getElementById("getBut").addEventListener("click", () => {
              if (!isStreaming) {
                // 创建新的EventSource实例
                eventSource = new EventSource("/get");
      
                // 消息接收处理
                eventSource.onmessage = (event) => {
                  if (isStreaming) {
                    document.getElementById("output").innerHTML += event.data;
                  }
                };
      
                // 错误处理
                eventSource.onerror = () => {
                  eventSource.close();
                  isStreaming = false;
                };
      
                isStreaming = true;
                this.innerHTML = "停止";
              } else {
                // 关闭连接
                eventSource.close();
                isStreaming = false;
                this.innerHTML = "开始";
              }
            });
          </script>
        </body>
        
      

      EventSource是浏览器原生支持的服务器推送事件(SSE)的接口,通过 HTTP 长连接实现服务端到客户端的单向实时数据流传输。

      当创建EventSource("/get")实例时,浏览器会自动向服务器发起 GET 请求并保持连接。onmessage事件监听器在接收到服务器数据时触发,通过event.data获取内容并追加到output容器中。

20250627-2257.gif