Vue3 对接大模型流式输出:读懂 ReadableStream 实现打字机效果

210 阅读5分钟

📝摘要

在 AI 应用、智能 Agent 开发中,流式输出已经是对话界面标配交互。不用等待大模型完整生成全部内容再一次性渲染,借助浏览器原生ReadableStream,搭配 DeepSeek 大模型接口,轻松实现边生成、边展示的打字机动画。本文从底层原理、工程思路逐层讲解,附 Vue3 <script setup>实战代码,无需全盘复制代码,吃透核心逻辑就能迁移到任意前端项目

🤖 一、Agent 时代,我们该如何调整开发思路

当下大模型 Agent 持续迭代,能力越来越贴近自主完成复杂任务,也是迈向 AGI 的重要方向。一套全新的人机协作模式正在成型:把 AI 擅长的标准化、重复性工作交给 Agent,人类聚焦决策、审核与核心业务把控。

落实到前端开发,有两条极具实践价值的思路:

  1. 拒绝从零搭建项目骨架不必每次新建项目就手写index.htmlApp.vue等基础文件,可以直接前往 Github 拉取成熟模板,在模板基础上迭代业务。
  2. 合理划分人与 AI 工作边界工程初始化、样板代码、通用基础组件搭建交给 Agent;复杂交互逻辑、边界异常处理、核心业务规则由开发者主导把控。

⚡ 二、Vite 热更新:调试流式项目的效率神器

本次 Demo 基于 Vite + Vue3 开发,Vite 自带的 热更新(Hot Reload 是本地开发的利器。

  • 传统打包模式:修改代码 → 整页刷新 → 页面所有运行状态全部丢失试想调试流式对话,每次改动代码都要重新输入提问,大量时间被无效消耗。
  • 热更新机制:仅更新被修改的模块,保留页面当前数据与运行状态,实现局部刷新。调试 LLM 流式逻辑时,能够省去大量重复操作。

🌊 三、流式输出底层原理:二进制 ReadableStream

传统一次性请求模式

前端发起请求 → 大模型服务器完整计算全部答案 → 返回完整 JSON → 页面一次性渲染。当 AI 输出长文本时,用户长时间只能看到 “加载中”,交互体验很差。

Stream 流式模式

大模型一边生成 Token,一边持续把增量文本分片下发至前端。前端收到分片数据立刻追加渲染,也就是大家熟悉的打字机效果。

浏览器层面,LLM 返回的流式响应本质就是二进制数据流 ReadableStream,可以用水管模型通俗理解:

  1. response.body:服务端返回的管道 ReadableStream;
  2. .getReader():获取读取器 reader,持续从管道中取数据;
  3. reader.read():每次取出一块Uint8Array二进制数组(数值范围 0~255 无符号整数);
  4. TextDecoder:二进制字节无法直接转换成可读文本,依靠解码器完成翻译。

📡 四、LLM 流式通信规范:类 SSE 分片格式

DeepSeek、OpenAI 等主流大模型流式接口,统一遵循类似 SSE 的数据分片规则:

  • 每一条有效数据行以 data: 开头;
  • 全部内容推送完成,服务端下发 data: [DONE] 作为结束标记。

示例原始流片段:

plaintext

data: {"choices":[{"delta":{"content":"从"}}]}
data: {"choices":[{"delta":{"content":"前"}}]}
data: [DONE]

前端标准处理流程:

  1. 将解码后的字符串按照换行分割;
  2. 过滤筛选出以data: 开头的数据行;
  3. 过滤结束标记[DONE]
  4. JSON 解析分片,取出delta.content增量文本;
  5. 持续拼接文本,更新页面视图。

⚠️ 两个新手高频踩坑点:

  1. JS 原生字符串方法是startsWith,极易手误写成startWith,直接抛出函数不存在报错;
  2. 单次读取的二进制分片有可能截断 JSON 字符串,必须设置buffer缓冲区,保存残缺片段留给下一轮循环解析。

💻 五、Vue3 核心实战思路(重点!不必全盘抄代码)

采用 Vue3 <script setup>组合式 API,对比 Vue2 选项式 API 优势明显:同一业务相关逻辑收拢在一处,代码内聚性更强,方便维护。

基础响应式变量

js

运行

import { ref } from 'vue';
// 用户提问
const question = ref('讲一个中国龙的故事');
// AI输出内容
const content = ref('');
// 开关:是否启用流式输出
const stream = ref(true);

请求主干逻辑

js

运行

const update = async () => {
  if (!question.value) return;
  content.value = '思考中...';

  const endpoint = 'https://api.deepseek.com/chat/completions';
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
    },
    body: JSON.stringify({
      model: 'deepseek-v4-flash',
      messages: [{ role: 'user', content: question.value }],
      stream: stream.value
    })
  });

  if(stream.value){
    // ========== 🎯 流式核心逻辑 ==========
    content.value = '';
    const reader = response.body?.getReader();
    const decoder = new TextDecoder();
    let done = false;
    let buffer = ''; // 缓存残缺JSON字符串

    while (!done) {
      const { value, done: doneReading } = await reader.read();
      done = doneReading;
      if(!value) continue;
      
      // 二进制解码,拼接缓存
      const chunk = buffer + decoder.decode(value);
      buffer = '';
      const lines = chunk.split('\n');
      
      for(const line of lines){
        const trimLine = line.trim();
        if(!trimLine.startsWith('data: ')) continue;
        const dataStr = trimLine.replace('data: ','');
        if(dataStr === '[DONE]') continue;
        
        try{
          const res = JSON.parse(dataStr);
          const text = res.choices[0].delta.content;
          if(text) content.value += text;
        }catch{
          // JSON不完整,存入buffer,下一轮继续解析
          buffer = line;
        }
      }
    }
  }else{
    // 非流式:一次性接收完整结果
    const data = await response.json();
    content.value = data.choices[0].message.content;
  }
}

✅ 需要重点掌握 3 块核心,其余代码可按需删减改造:

  1. while循环持续调用reader.read()拉取二进制分片;
  2. buffer缓冲区容错,解决 JSON 被截断问题;
  3. 不断追加增量文本,利用 Vue 响应式自动更新页面。

极简模板参考:

vue

<template>
  <div class="container">
    <div>
      <label>输入:</label>
      <input v-model="question"/>
      <button @click="update">提交</button>
    </div>
    <div>
      <label>Streaming</label>
      <input type="checkbox" v-model="stream"/>
    </div>
    <div>{{ content }}</div>
  </div>
</template>

🚨 六、上线前必须重视的 3 个风险

  1. CORS 跨域限制浏览器直接在前端调用 DeepSeek 公网接口会触发跨域。本地开发可以使用 Vite 代理;生产环境必须增加后端中转层
  2. API 密钥泄露前端项目内的密钥无法做到保密,用户打开浏览器开发者工具就能抓取。线上项目禁止前端直接请求大模型接口。
  3. 网络异常处理断流、超时、分片解析失败都需要兜底捕获,buffer缓冲区不能随意删除,是流式稳定运行的关键。

🧠 七、拓展:流式输出在 Agent 开发中的价值

流式输出不只是实现好看的打字动画。在智能 Agent 场景下,AI 会分步完成思考、工具调用、结果总结。依托流式推送,我们可以实时展示 Agent 完整思考链路、工具调用日志,让使用者清晰看见 AI 如何拆分任务、执行动作。

掌握浏览器数据流处理,是开发 AI 对话、智能 Agent 前端界面的必备基础能力