Vue3流式调用大模型接口完整实践

现在主流大模型 API 都支持SSE 流式输出 ,能实现打字机实时返回回答效果,体验远优于一次性等待完整结果返回。本文基于 Vue3 <script setup> 组合式 API,使用原生 Fetch + ReadableStream 不依赖第三方库,完整实现 DeepSeek 接口普通返回 / 流式切换双模式,附带逐行代码拆解与底层原理讲解。

一、整体实现效果

  1. 输入提问文本,点击提交请求大模型
  2. 勾选 Streaming 开启流式打字机输出,取消则一次性返回完整回答
  3. 请求过程显示「思考中...」加载状态
  4. 纯原生 JS 实现流解析,无 axios、sse.js 等额外依赖
  5. 完整封装接口请求、二进制流解码、分段文本拼接逻辑

二、分段代码详细解析

3.1 响应式数据定义
复制代码

const question = ref("讲一个中国龙的故事"); const content = ref(""); const stream = ref(true);

  • ref:Vue3 基础响应式 API,处理字符串、数字等基础类型;底层为RefImpl实例,通过.value读写原始值
  • question:双向绑定输入框,存储用户提问
  • content:AI 回答渲染容器,流式模式下持续追加文本
  • stream:布尔开关,控制接口stream参数,切换两种返回模式
3.2 基础请求封装
复制代码

const endpoint = "https://api.deepseek.com/chat/completions"; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`, };

  • DeepSeek 官方对话接口地址,鉴权使用 Bearer Token
  • import.meta.env.VITE_XXX:Vite 环境变量,将 API 密钥存入.env文件,避免硬编码泄露密钥
复制代码

body: JSON.stringify({ model: "deepseek-v4-flash", messages: [{ role: "user", content: question.value }], stream: stream.value, })

接口入参规范:

  • model:指定调用模型
  • messages:对话上下文数组,role: user代表用户提问
  • stream:布尔值,true开启 SSE 流式分片返回,false同步返回完整结果
3.3 非流式逻辑(同步一次性返回)
复制代码

const data = await response.json(); content.value = data.choices[0].message.content;

关闭流式时,接口阻塞等待 AI 完整生成回答后返回 JSON,直接读取message.content完整文本赋值,缺点是长文本等待时间长,无实时反馈。

3.4 流式输出核心:ReadableStream 数据流处理
1. 获取二进制读取器与解码器
复制代码

const reader = response.body?.getReader(); const decoder = new TextDecoder();

  • response.body:Fetch 响应内置ReadableStream二进制流对象,服务端持续推送二进制数据分片
  • getReader():创建流读取器,提供read()方法逐块拉取数据
  • TextDecoder:浏览器内置 API,将二进制 Uint8Array 转 UTF-8 字符串,解决中文乱码问题

2. 循环读取分片 代码逐行深度拆解

复制代码

while (!done) { const { value, done: doneReading } = await reader?.read(); done = doneReading; const chunkValue = buffer + decoder.decode(value); buffer = ""; }

整体逻辑概述

while (!done)无限循环读取二进制数据流 ,只要服务端还在持续返回大模型 Token 分片,循环就不会停止;直到服务端推送结束标识 [DONE] 或流读取完毕,done 变为 true,跳出循环。 整个流程本质:一次拉取一小块二进制数据 → 拼接缓存 → 转字符串 → 解析文本 → 继续拉取下一块

逐行拆解

1. while (!done)
  • done:自定义布尔标记,初始值 false,代表「流是否读取完毕」

  • 循环条件:只要流没有结束,就持续执行读取逻辑

  • 终止条件:两种场景会让 done = true

    1. reader.read() 返回 doneReading = true:服务端所有数据推送完成,流自然关闭
    2. 手动捕获到 data: [DONE] 结束标记,主动赋值 done = true 跳出循环
2. const { value, done: doneReading } = await reader?.read();
reader?.read() 可选链 ?.

response.body?.getReader() 有可能返回 undefined(老旧浏览器不支持 ReadableStream),使用可选链避免 read() is not a function 报错,做兼容性兜底。

read() 是异步方法,必须加 await

reader.read() 会阻塞等待,直到浏览器收到服务端下发的一块二进制分片才会 resolve 返回结果; 如果当前没有新数据,代码会挂起等待,不会无限空跑循环,不会占用 CPU。

③ 返回值解构 { value, done: doneReading }

read() 执行后返回一个 Promise,成功后得到对象:

复制代码

{ value: Uint8Array | undefined, // 本次拿到的二进制字节数组 done: boolean // 原生流结束标记,重命名为 doneReading 区分自定义done变量 }

  • value:本次从服务端拉取的二进制原始数据,存储在 Uint8Array 类型数组中,中文、英文、符号全部以二进制字节形式存在;

  • done: doneReading:重命名变量,防止和外层自定义 done 冲突。

    • true:服务端数据流彻底关闭,不会再有新分片;
    • false:还有后续数据,继续循环读取。
3. done = doneReading;

把原生流的结束状态同步给外层循环标记 done。 当服务端传输完毕,doneReading 为 true,赋值后下一轮 while (!done) 条件不成立,循环终止。

4. const chunkValue = buffer + decoder.decode(value);
decoder.decode(value)

TextDecoder 浏览器原生 API,专门解析 Uint8Array 二进制数组:

  • 将二进制字节转为 UTF-8 标准字符串,完美解决中文多字节乱码问题;
  • 单独解码单块 value 可能出现截断残缺字符串(网络分片切割在一个汉字 / 一条 JSON 中间)。
buffer + 解码后的字符串

buffer 是全局缓存变量,专门存放上一轮解析残缺、不完整无法解析的字符串片段 。 举例场景: 上一轮分片结尾只读到 {"delta":{"content":"你好,JSON 不完整,解析失败,存入 buffer; 本轮新分片开头是 世界"}},拼接后 {"delta":{"content":"你好世界"}},形成完整可解析 JSON。

5. buffer = "";

本轮缓存 + 新分片拼接完成 chunkValue 后,清空缓存; 后续解析 chunkValue 过程中如果再次遇到残缺 JSON,再重新把残缺片段写入 buffer,等待下一轮拼接。

完整运行流程示例

  1. 初始:done = falsebuffer = "",进入循环
  2. await reader.read() 等待服务端下发二进制块,得到 value 二进制、doneReading = false
  3. done = false,循环继续
  4. 二进制转字符串,和空 buffer 拼接得到完整 chunkValue
  5. 清空 buffer,解析 chunkValue 内所有 data: 数据行
  6. 若解析过程有残缺 JSON,将残缺片段存入 buffer
  7. 回到循环开头,再次拉取下一块二进制分片,自动拼接上一轮残缺缓存

关键设计目的:buffer 分片容错

网络传输时,服务端的一行完整 SSE 数据可能被拆成 2~3 个二进制包分次下发:

  • 包 1:data: {"delta":{"content":"我今
  • 包 2:天去公园"}}\n 只解析单包会 JSON.parse 报错,依靠 buffer 跨循环拼接分片,保证一定拿到完整 JSON 字符串再解析,是流式处理必不可少的容错逻辑。
3. 过滤 SSE 标准数据行
复制代码

const lines = chunkValue .split("\n") .filter((line) => line.startsWith("data: "));

大模型 SSE 协议规范:每一条数据以data: 开头,换行分隔,过滤无关空行、注释行,只保留有效业务数据。

4. JSON 解析与增量文本拼接
复制代码

for (const line of lines) { if (line === "data: [DONE]") { done = true; break; } const jsonStr = line.replace("data: ", ""); try { const resJson = JSON.parse(jsonStr); const deltaText = resJson.choices[0].delta.content || ""; content.value += deltaText; } catch (err) { buffer += line; } }

  • data: [DONE]:服务端流结束标识,终止读取循环
  • delta.content:流式独有增量字段,每次只返回新增文字,持续追加到content实现打字机效果
  • try/catch容错:JSON 解析失败代表分片不完整,存入 buffer 等待下一轮合并

三、完整可运行代码

复制代码

<script setup> // vue3 composition 组合 api // 业务逻辑聚合,区分Vue2选项式分散写法 import { ref } from "vue"; // 响应式数据 const question = ref("讲一个中国龙的故事"); // 用户提问 const content = ref(""); // AI返回内容 const stream = ref(true); // 是否开启流式输出开关 // 核心请求函数 const update = async () => { // 空输入拦截 if (!question.value) return; content.value = "思考中..."; // DeepSeek 接口地址 const endpoint = "https://api.deepseek.com/chat/completions"; // 请求头,携带鉴权密钥 const headers = { "Content-Type": "application/json", Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`, }; // 发起POST请求 const response = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify({ model: "deepseek-v4-flash", messages: [{ role: "user", content: question.value }], stream: stream.value, // 根据开关切换流式/一次性返回 }), }); // 分支1:流式输出处理逻辑 if (stream.value) { content.value = ""; // 获取响应二进制读取器 const reader = response.body?.getReader(); // 二进制转UTF-8文本解码器 const decoder = new TextDecoder(); let done = false; let buffer = ""; // 分片缓存,防止单行数据截断 // 循环读取数据流,直到流结束 while (!done) { // 读取一块二进制分片 const { value, done: doneReading } = await reader?.read(); done = doneReading; // 拼接缓存+当前分片,转字符串 const chunkValue = buffer + decoder.decode(value); buffer = ""; // 过滤SSE标准data: 前缀数据行 const lines = chunkValue .split("\n") .filter((line) => line.startsWith("data: ")); // 遍历每一行分片JSON for (const line of lines) { // 服务端结束标识,终止循环 if (line === "data: [DONE]") { done = true; break; } // 截取data: 后面的JSON字符串 const jsonStr = line.replace("data: ", ""); try { const resJson = JSON.parse(jsonStr); // 增量文本delta追加到页面内容 const deltaText = resJson.choices[0].delta.content || ""; content.value += deltaText; } catch (err) { // JSON解析失败存入缓存,下一轮拼接处理 buffer += line; } } } } else { // 分支2:非流式,一次性获取完整JSON const data = await response.json(); content.value = data.choices[0].message.content; } }; </script> <template> <div class="container"> <!-- 提问输入区域 --> <div> <label>输入:</label> <input class="input" v-model="question" /> <button @click="update">提交</button> </div> <!-- 输出控制与展示区域 --> <div class="output"> <div> <label>Streaming</label> <input type="checkbox" v-model="stream" /> </div> <div>{````{ content }}</div> </div> </div> </template> <style> .container { /* flex纵向布局,贴合文档流 */ display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; height: 100vh; font-size: 0.85rem; } .input { width: 200px; } .output { margin-top: 10px; min-height: 300px; width: 100%; text-align: left; } button { padding: 0 10px; margin-left: 6px; } </style>

四、模板与样式说明

Template
  1. 输入框使用v-model双向绑定question,按钮绑定点击事件触发请求
  2. 复选框v-model绑定stream,一键切换流式开关
  3. {``{ content }}响应式渲染 AI 回答,数据流更新页面自动刷新
Style
  • 使用 flex 纵向布局,适配页面全屏高度
  • 输出区域设置最小高度,预留回答展示空间
  • 基础尺寸、边距优化,保证基础交互观感

五、环境配置与踩坑指南

5.1 环境变量配置

项目根目录新建.env文件:

复制代码

VITE_DEEPSEEK_API_KEY=你的DeepSeek密钥

重启项目后import.meta.env可正常读取密钥。

5.2 常见问题解决
  1. 跨域报错 本地开发前端直接请求 DeepSeek 接口会触发 CORS,解决方案:
  • Vite 配置本地代理转发接口请求
  • 后端搭建中转接口,前端请求自身服务,由服务端调用大模型 API
  1. 中文乱码 必须使用TextDecoder解析二进制分片,不可直接转字符串,二进制分片无法正确识别多字节中文。
  2. JSON 解析报错 网络分片截断单行 JSON,依靠buffer缓存碎片,不要直接丢弃解析失败的行。
  3. 流读取无响应 判断response.body是否存在,低版本浏览器不支持ReadableStream,可增加降级逻辑切换非流式模式。

六、核心技术原理总结

  1. SSE 流式通信:服务端长连接持续推送增量数据,相比轮询、WebSocket 更适合大模型文本输出场景
  2. ReadableStream:浏览器原生流式处理 API,无需一次性加载全部响应数据,分片处理节省内存
  3. Vue 响应式更新 :每次追加delta增量文本,ref触发局部 DOM 更新,实现实时打字效果
  4. 组合式 API 优势:接口请求、流处理、状态控制全部逻辑聚合在一处,对比 Vue2 选项式代码更易维护、复用
相关推荐
宠友信息1 小时前
从重复请求到订单同步看内容社区源码的处理思路
java·spring boot·redis·后端·mysql·spring
怪兽学LLM1 小时前
AI Agent 记忆系统设计:长短期记忆如何实现?什么时候存?什么时候查?
人工智能·python
DFT计算杂谈1 小时前
DeepSeek 集群服务器无root本地部署指南
数据库·人工智能·python·opencv·算法
MobotStone1 小时前
五个AI使用习惯让你的效率翻倍
后端
卷无止境1 小时前
Python 跨平台 GUI 解决方案全景报告
前端·python
pixcarp1 小时前
GMP调度模型和GC垃圾回收机制解析
后端·学习·golang·内存管理·gc·gmp
万少8 小时前
用腻了 WorkBuddy 的默认界面?这个开源小工具给它换上了毛玻璃
前端·javascript·后端
皮皮林55110 小时前
代码越“整洁”,性能越“拉胯”?
后端
benchmark_cc11 小时前
如何用 Python + QuantDash 快速构建高胜率“配对交易(Pairs Trading)”策略?
开发语言·人工智能·python·pandas·量化交易·quantdash