首页 > 网页制作 >AI流式输出前端必修课:从敲下回车到文字逐字出现

AI流式输出前端必修课:从敲下回车到文字逐字出现

来源:互联网 2026-07-18 08:17:09

流式输出通过设置stream:true参数,使服务器按token逐次生成并发送数据。浏览器端利用ReadableStream流式读取,配合TextDecoder解码,按SSE协议解析每个消息中的delta.content字段,并逐字渲染到页面,从而实现AI回复内容逐字出现的效果。

有没有想过:为什么在 ChatGPT 或 DeepSeek 的对话框里敲下一个问题,回答是一个字一个字“蹦”出来的,而不是像普通网页请求那样转半天然后“啪”一下全部出现?

AI流式输出前端必修课:从敲下回车到文字逐字出现

长期稳定更新的攒劲资源: >>>点此立即查看<<<

这背后藏着的前端技术,在 AI 时代正变得越来越关键——流式输出(Streaming)。今天不堆砌概念,就从浏览器拿到数据的那一刻开始,一步步拆解“流”到底是怎么流起来的。

一、先搞懂一个问题:LLM 为什么“慢”

在理解流式输出之前,需要先明白大语言模型(LLM)的“慢”从哪来。

LLM 生成回答的过程,本质上是一个逐 token 的自回归过程。所谓 token,可以粗浅理解为“最小的文本单元”——可能是一个汉字、一个英文单词的一部分,甚至是一个标点符号。模型每生成一个 token,都要把它拼回已有的上下文,再预测下一个 token 是什么。这个过程涉及大量的矩阵运算,参数量越大、回答越长,耗时就越久。

如果等模型把全部内容生成完再一次性返回,用户等待的时间就是 N 个 token 的推理时间总和。而流式输出的思路极其简单:不用等全部生成完,生成一个 token 就给你一个 token

这就是流式输出的核心逻辑。

二、HTTP 协议层到底发生了什么

流式输出能跑起来,靠的是前后端之间的一个约定

客户端在请求中设置 stream: true,相当于告诉服务器:“我不要等你全部做完再回复,你边做边给我就行。”服务端收到这个参数后,不会等到全部内容生成完毕才写入响应体,而是每推理出一个 token 就立刻写入。

text

 复制代码// 请求体中的关键参数
{
  model: "deepseek-v4-flash",
  messages: [{ role: "user", content: "讲一个中国龙的故事" }],
  stream: true   // ← 就是这个开关
}

在 HTTP 协议层面,这依赖于 分块传输编码(Chunked Transfer Encoding)。服务器不需要提前知道响应体的总长度,可以一边生成数据一边把数据块发给客户端。浏览器这边,则通过一套叫作 Web Streams API 的机制来接收这些陆续到达的数据块。

三、浏览器端的武器:ReadableStream

当用 fetch 发起一个 stream: true 的请求后,返回的 response 对象里藏着一个关键属性——response.body

ja vascript

 复制代码const response = await fetch(endpoint, {
  method: "POST",
  headers: { /* ... */ },
  body: JSON.stringify({
    model: "deepseek-v4-flash",
    messages: [{ role: "user", content: question.value }],
    stream: true
  })
});
console.log(response.body);  // ReadableStream 对象

response.body 是一个 ReadableStream 对象。可以把它理解成一根“水管”——服务器那边源源不断地往管子里送水(数据),浏览器这边负责从管子的另一头接水。

拿到 ReadableStream 之后,要做两件事:

  1. 获取一个“读取器”(reader),用来从水管里一口一口“嘬”数据
  2. 循环读取,直到服务器说“数据发完了”

ja vascript

 复制代码const reader = response.body.getReader();  // 拿到读取器
let done = false;
while (!done) {
  const { value, done: doneReading } = await reader.read();
  done = doneReading;
  // value 就是这一口“嘬”到的数据
}

reader.read() 返回一个 Promise,resolve 之后得到一个对象,包含两个字段:

  • value:本次读取到的数据块,类型是 Uint8Array
  • done:布尔值,true 表示数据已经全部读完

四、Uint8Array 与 TextDecoder:二进制怎么变回文字

这里有一个关键问题:网络传输的底层是字节流reader.read() 拿到的 value 是一个 Uint8Array——也就是一堆 0 到 255 之间的数字组成的数组。

但需要显示的是中文、英文、标点符号,不是数字。所以需要一个解码器,把这些字节还原成字符串。

ja vascript

 复制代码const decoder = new TextDecoder();  // 创建一个 UTF-8 解码器
// 在循环里:
const chunk = decoder.decode(value);  // 把 Uint8Array 转成字符串
content += chunk;              // 拼到已有的内容后面

TextDecoder 支持流式解码——也就是说,如果一个字符的字节被拆到了两个数据块里(比如一个中文汉字在 UTF-8 下可能占 3 个字节),TextDecoder 能正确处理这种跨块的情况,不会出现乱码。

五、SSE 数据格式:一行一行解析

DeepSeek API 的流式响应遵循 SSE(Server-Sent Events) 格式。具体来说,服务器每次发送一个数据块,格式是这样的:

text

 复制代码data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"在"}}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"遥远"}}]}
data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"的"}}]}
data: [DONE]

每一行以 data: 开头,后面跟着一个 JSON 字符串。delta 字段里装着本次新增的内容(也就是新生成的那个 token)。最后一行是 data: [DONE],表示全部发送完毕。

所以在前端,不能直接把 decoder.decode(value) 的结果拼到页面上——需要先按行解析,提取出 delta.content,再拼进去。

ja vascript

 复制代码let buffer = "";  // 缓存上一次未处理完的数据
while (!done) {
  const { value, done: doneReading } = await reader.read();
  done = doneReading;
  // 把新数据追加到缓冲区
  const chunkValue = buffer + decoder.decode(value);
  buffer = "";
  // 按换行符拆分成多行
  const lines = chunkValue.split("n").filter(line => line.startsWith("data: "));
  for (const line of lines) {
    const jsonStr = line.replace("data: ", "");
    if (jsonStr === "[DONE]") {
      done = true;
      break;
    }
    try {
      const parsed = JSON.parse(jsonStr);
      const deltaContent = parsed.choices.[0].delta.content;
      if (deltaContent) {
        content += deltaContent;  // 逐字拼接到页面上
      }
    } catch (e) {
      // 忽略解析错误(可能是不完整的 JSON)
    }
  }
}

这里有一个容易被忽视的细节:网络数据块不保证按“行”对齐。也就是说,一次 reader.read() 拿到的数据可能只包含半行,也可能包含好几行。所以用了一个 buffer 变量来缓存上一次没处理完的尾巴,等下一轮数据来了再拼起来一起解析。

六、在 Vue 3 里串起来

有了上面的基础,将其集成到 Vue 3 组件里。

vue

 复制代码

这里用了 Vue 3 的 ref 来定义响应式数据。content.value 每变化一次,页面上绑定了 {{ content }} 的地方就会自动更新——这就是 Vue 数据驱动视图的核心机制。

七、热更新:开发阶段的“局部刷新”

最后补充一个开发中的小技巧:热更新(Hot Reload)

在开发阶段,使用的是 Vite 这样的构建工具。当修改了 App.vue 的代码,Vite 会检测到文件变化,然后只替换发生变化的那部分模块,而不是刷新整个页面。

这意味着什么?页面上输入的一段话、展开的一个菜单、滚动到的某个位置——这些页面状态不会因为代码修改而丢失。如果是传统的“保存→刷新页面”流程,所有状态都会重置,就得重新操作一遍才能回到刚才的调试位置。

Vue/React 的响应式数据状态往往“密密麻麻”地分布在各个组件里,热更新让开发者能保持当前的调试上下文,效率提升非常明显。

总结

流式输出的完整链路可以概括为四步:

  1. 客户端在请求中设置 stream: true,告诉服务器“我要流式数据”
  2. 服务端每生成一个 token 就通过 HTTP 分块传输发出来,格式为 SSE
  3. 浏览器通过 response.body.getReader() 逐块读取数据
  4. 前端TextDecoder 解码、按行解析 SSE、提取 delta.content 后逐字渲染

这四个环节环环相扣,少一个都跑不起来。掌握了这条链路,就掌握了 AI 时代前端与 LLM 交互的核心基本功。

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。