LLM流式输出实现

一、LLM 流式输出的工作机制

LLM 流式输出的核心逻辑是将模型生成的内容 "碎片化推送" ,而非等待完整结果生成后一次性呈现。其底层依托大语言模型的自回归生成特性:模型每次生成一个 token(可能是字、词或子词),并将该 token 纳入下一次生成的输入,循环往复直至完成整段文本生成。

在流式输出模式中,这一过程被赋予了 "实时推送" 的能力。当模型生成的 token 积累到设定阈值(如一个短句或 10 个字符),就会立即通过网络传输至用户端,用户端接收后即时渲染显示。例如,当用户询问 "如何提升阅读效率" 时,模型会先输出 "提升阅读效率可尝试以下方法:",随后逐句生成 "1. 采用 SQ3R 阅读法,即 Survey(浏览)、Question(提问)、Read(阅读)、Recite(复述)、Review(复习)......",整个过程中,用户无需等待全部内容生成,就能同步跟进模型的 "思路"。

这种机制类似人类 "边思考边表达" 的过程,让模型从 "黑箱式的结果输出工具" 转变为 "可实时互动的对话伙伴"。

二、实现 LLM 流式输出的关键技术

LLM 流式输出的顺畅运行,需要模型层、传输层与应用层的协同配合,三者共同构成完整的技术链路。

(一)模型层:分块生成与阈值控制

大语言模型本身的生成逻辑是流式输出的基础。以 GPT 系列模型为例,其采用的 Transformer 架构通过自注意力机制实现 token 的序列生成,每一步生成的 token 都可被独立提取。开发者通过设置 "推送阈值",决定何时将已生成的 token 打包推送 ------ 阈值可以是固定的 token 数量(如每生成 8 个 token 推送一次),也可以是语义断点(如遇到句号、逗号时触发推送),后者能让推送内容更符合人类阅读的语义习惯。

(二)传输层:低延迟通信协议支撑

为实现生成内容的实时传输,流式输出依赖高效的通信协议。目前主流方案包括两种:

一是基于 HTTP/1.1 的分块传输(Chunked Transfer Encoding) ,服务器通过 "Transfer-Encoding: chunked" 头字段告知客户端数据将分块传输,每块数据前附带长度标识,客户端接收后逐块解析;客户端通过response.body.getReader()获取响应体的可读流(ReadableStream),这是浏览器提供的原生 API,支持异步逐块读取服务器推送的二进制数据块,无需等待整个响应完成。

二是基于 WebSocket 的全双工通信,一旦建立连接,服务器可主动向客户端推送数据,省去了 HTTP 请求的反复握手,延迟更低,尤其适合长对话场景。例如,OpenAI 的 API 采用 WebSocket 协议实现流式输出,客户端发送请求时指定 "stream: true" 参数,服务器便会以 "SSE(Server-Sent Events)" 格式持续推送数据,每条数据包含新增的 token 内容,客户端通过监听事件实时更新界面。

(三)应用层:实时渲染与体验优化

客户端的渲染逻辑直接影响用户对流式输出的感知。优秀的应用层设计会包含三大核心功能:

一是增量渲染,仅更新新增的文本内容,避免整体刷新导致的视觉跳跃;

二是节奏控制,通过调整 token 显示速度(如模拟打字机效果),让输出节奏贴合人类阅读习惯,避免信息过载;

三是异常处理,当网络中断时,能记录已接收的 token 序列,网络恢复后仅请求后续内容,实现 "断点续传",减少重复生成带来的资源浪费。

三、流式输出与传统输出的核心差异

传统 LLM 输出模式中,用户发出请求后需经历 "模型完整生成→全量传输→客户端渲染" 三个串行步骤,整个过程处于 "无反馈等待" 状态。而流式输出将这三个步骤改为并行:模型生成部分内容后立即启动传输,客户端接收后同步渲染,三个环节重叠进行,大幅缩短了用户感知到的等待时间。

以生成一篇 500 字的回答为例,传统模式可能需要 10 秒生成完整内容,用户需等待 10 秒后才能看到结果;流式输出则可能在 1 秒后开始推送首段内容,随后每秒推送 50 字,总耗时仍为 10 秒,但用户从第 1 秒起就能获取信息,等待焦虑显著降低。此外,在长文本生成场景中,用户可基于已输出内容提前判断是否需要调整提问方向,减少无效等待 ------ 例如,当模型输出的前半部分偏离需求时,用户可及时中断生成,节省后续时间。

四、流式输出的应用价值

(一)提升用户体验,降低交互门槛

人类对 "无反馈等待" 的容忍度极低,流式输出通过 "实时响应" 让用户感受到模型的 "积极性",尤其对非技术用户而言,这种 "边说边想" 的交互模式更贴近日常对话习惯,降低了使用大模型的心理门槛。在客服对话、教育答疑等场景中,流式输出能让用户快速获取初步答案,减少因等待产生的负面情绪。

(二)优化资源效率,适配复杂场景

对服务器而言,流式输出无需缓存完整结果,可减少内存占用,尤其适合生成超长文本(如万字报告、代码文件)时的资源管理;对客户端而言,分块接收数据降低了一次性加载大文本的内存压力,使低配置设备(如手机、平板)也能流畅运行大模型应用;在网络带宽有限的环境中,流式输出通过 "小批量传输" 减少单次数据量,降低了传输超时的风险。

(三)拓展应用边界,赋能实时场景

流式输出让 LLM 在实时交互场景中发挥更大价值。在实时翻译场景中,模型可边接收原文边生成译文,实现 "同声传译" 级别的实时性;在直播弹幕互动中,能基于观众实时发送的弹幕内容,流式生成回应并推送给主播,提升互动效率;在代码辅助工具中,开发者输入需求后,模型流式输出代码片段,开发者可边看边调试,大幅缩短开发周期。

五、流式输出实现代码

在React项目中让请求deep seek接口返回的内容流式输出。

1. 引入依赖与组件定义

javascript 复制代码
import { useState } from "react"; // 引入React的useState钩子,用于管理组件状态
import "./index.css"; // 引入样式文件

function Deepseek() { // 定义名为Deepseek的函数组件
  • 这部分是组件的基础结构,引入了状态管理所需的useState和样式文件,并声明了组件主体。

2. 状态管理

javascript 复制代码
  const [question, setQuestion] = useState(""); // 存储用户输入的问题,初始值为空字符串
  const [content, setContent] = useState(""); // 存储AI返回的回答内容,初始值为空
  const [streaming, setStreaming] = useState(false); // 控制是否启用流式输出,默认关闭
  • 通过useState定义了三个状态变量:

    • question:绑定到输入框,记录用户输入的问题
    • content:展示 AI 的回答结果
    • streaming:通过复选框控制,决定是否使用流式输出

3. 核心交互函数update

javascript 复制代码
  const update = async () => { // 异步函数,处理发送请求和接收响应的逻辑
    // 获取到用户在input框输入的内容
    if (!question) return; // 如果输入为空,直接返回,不执行后续操作
    setContent(""); // 发送新请求前,清空之前的回答内容
  • update是点击 "发送" 按钮时触发的函数,首先做输入校验和初始化(清空历史回答)。

4. API 请求配置

javascript 复制代码
    // 与deepseek交互
    const endpoint = "https://api.deepseek.com/chat/completions"; // Deepseek API的请求地址
    const headers = { // 请求头配置
      Authorization: `Bearer ${import.meta.env.VITE_DEEKSEEK_KEY}`, // 授权令牌,从环境变量获取
      "Content-Type": "application/json", // 声明请求体为JSON格式
    };
  • 配置了 API 的基础信息:请求地址、请求头(包含授权信息,通过环境变量避免硬编码密钥)。

5. 发送请求

javascript 复制代码
    const response = await fetch(endpoint, { // 发送POST请求
      method: "POST", // 请求方法为POST
      headers: headers, // 使用上面定义的请求头
      body: JSON.stringify({ // 请求体,转换为JSON字符串
        model: "deepseek-chat", // 指定使用的模型
        messages: [ // 对话消息数组
          {
            role: "user", // 消息角色为"用户"
            content: question, // 消息内容为用户输入的question
          },
        ],
        stream: streaming, // 是否启用流式输出,由复选框状态控制
      }),
    });
  • 通过fetch发送请求到 Deepseek API,请求体中包含模型名称、用户消息和流式输出开关(stream: streaming)。

6. 流式输出处理(核心逻辑)

javascript 复制代码
    // 流式输出
    if (streaming) { // 如果启用了流式输出
      const reader = response.body.getReader(); // 获取响应体的可读流读取器
      const decoder = new TextDecoder(); // 创建文本解码器,用于将二进制数据转换为字符串
      let done = false; // 标记流式传输是否结束
      let buffer = ""; // 缓冲区,用于处理跨块的不完整数据

      while (!done) { // 循环读取流,直到传输结束
        const { value, done: readerDone } = await reader.read(); // 读取一段二进制数据(value)和是否结束(readerDone)
        done = readerDone; // 更新done状态
        const chunkValue = buffer + decoder.decode(value); // 将缓冲区数据与当前解码后的数据拼接
        buffer = ""; // 清空缓冲区(后续会重新处理未完成的片段)

        // 处理SSE格式的数据(Server-Sent Events,服务器发送事件)
        const lines = chunkValue
          .split("\n") // 按换行分割数据
          .filter((line) => line.startsWith("data: ")); // 筛选出SSE格式的行(以"data: "开头)

        for (const line of lines) { // 遍历每一行有效数据
          const incoming = line.slice(6); // 去除开头的"data: "(长度为6)
          if (incoming === "[DONE]") { // 如果收到结束标记
            done = true; // 标记传输结束
            break; // 跳出循环
          }
          try {
            const data = JSON.parse(incoming); // 解析JSON格式的内容
            const delta = data.choices[0].delta.content; // 获取当前片段的内容(流式输出的增量部分)
            if (delta) { // 如果有增量内容
              setContent((prev) => prev + delta); // 累加更新回答内容(使用函数式更新,确保基于最新状态)

              // str += delta
              // setContent(str) 
              // 上面两行是错误示例:因为str会形成闭包,无法获取最新值,导致更新不及时
            }
          } catch (err) {
            console.log(err); // 解析出错时,在控制台打印错误
          }
        }
      }
    }

1. 逐块读取二进制数据

js 复制代码
const { value, done: readerDone } = await reader.read();
  • reader.read()是异步操作,每次调用会从流中读取一个二进制数据块(valueUint8Array类型),并通过readerDone标记是否读取完毕。
  • 循环while (!done)确保持续读取,直到服务器结束传输(readerDonetrue)。

2. 处理数据完整性:缓冲区拼接

javascript 复制代码
    const chunkValue = buffer + decoder.decode(value);
    buffer = "";
  • 服务器推送的数据可能被分割在多个块中(例如一行完整的 SSE 数据被拆成两个 TCP 包发送),buffer用于暂存上一次未处理完的残留数据,与当前块解码后的数据拼接,确保内容完整。
  • TextDecoder将二进制数据(Uint8Array)解码为字符串,完成 "二进制→文本" 的转换。

3. 解析 SSE 格式:提取增量内容

服务器推送的流式数据遵循 SSE(Server-Sent Events)格式,每条有效数据以data: 开头,例如:

代码通过以下步骤提取增量内容:

javascript 复制代码
// 筛选SSE格式的行(以"data: "开头)
const lines = chunkValue.split("\n").filter(line => line.startsWith("data: "));

// 提取并解析增量内容
for (const line of lines) {
  const incoming = line.slice(6); // 移除"数据前缀"data: 
  if (incoming === "[DONE]") { done = true; break; } // 结束标记
  const data = JSON.parse(incoming); 
  const delta = data.choices[0].delta.content; // 增量文本(如"你好"、"世界")
}
  • 每次解析都会得到模型生成的增量文本(delta ,这是流式输出的核心 ------ 服务器每次只返回一小段内容(而非完整结果)。

4. 实时更新 UI:累加展示

javascript 复制代码
    setContent((prev) => prev + delta);
  • 当获取到delta(如 "你"、"好"、"世"、"界"),通过函数式更新将增量内容累加到现有内容中,立即更新页面展示。
  • 函数式更新(prev) => prev + delta确保基于最新的content状态计算新值,避免闭包导致的状态滞后问题,保证每一段增量都能正确拼接。

7.组件 UI 渲染

javascript 复制代码
  return (
    <div className="container"> {/* 外层容器 */}
      <div className="response"> {/* 输入区域 */}
        <label htmlFor="">请输入</label> {/* 输入框标签 */}
        <input
          type="text"
          className="input"
          onChange={(e) => { // 输入变化时,更新question状态
            setQuestion(e.target.value);
          }}
        />
        <button onClick={update}>发送</button> {/* 点击触发update函数 */}
      </div>
      <div> {/* 流式输出开关 */}
        <label htmlFor="">streaming</label> {/* 复选框标签 */}
        <input
          type="checkbox"
          onChange={(e) => { // 勾选状态变化时,更新streaming状态
            setStreaming(e.target.checked);
          }}
        />
      </div>
      <div>{content}</div> {/* 展示回答内容 */}
    </div>
  );
}
  • 渲染了三个核心区域:

    • 输入框:绑定question状态,实时更新用户输入
    • 流式开关:复选框绑定streaming状态,控制输出模式
    • 回答区域:展示content状态,即 AI 的回答内容

实现效果

相关推荐
小飞悟20 分钟前
那些年我们忽略的高频事件,正在拖垮你的页面
javascript·设计模式·面试
绅士玖23 分钟前
📝 深入浅出 JavaScript 拷贝:从浅拷贝到深拷贝 🚀
前端
李元豪24 分钟前
【知足常乐ai笔记】机器人强化学习
人工智能·笔记·机器人
沫儿笙25 分钟前
焊接机器人智能节气装置
人工智能·机器人
MidJourney中文版26 分钟前
老年人与机器人玩具的情感连接
人工智能·机器人·语音识别
Codebee27 分钟前
AI驱动的低代码革命:解构与重塑开发范式
人工智能·低代码·代码规范
数据库安全29 分钟前
首批|美创智能数据安全分类分级平台获CCIA“网络安全新产品”
大数据·人工智能·web安全
Dymc30 分钟前
【目标检测之Ultralytics预测框颜色修改】
人工智能·yolo·目标检测·计算机视觉
中微子33 分钟前
闭包面试宝典:高频考点与实战解析
前端·javascript
brzhang33 分钟前
前端死在了 Python 朋友的嘴里?他用 Python 写了个交互式数据看板,着实秀了我一把,没碰一行 JavaScript
前端·后端·架构