一、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()
是异步操作,每次调用会从流中读取一个二进制数据块(value
为Uint8Array
类型),并通过readerDone
标记是否读取完毕。

- 循环
while (!done)
确保持续读取,直到服务器结束传输(readerDone
为true
)。
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 的回答内容
- 输入框:绑定
实现效果
