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

发布时间:2026/7/17 22:53:14
Vue3流式调用大模型接口完整实践 现在主流大模型 API 都支持SSE 流式输出能实现打字机实时返回回答效果体验远优于一次性等待完整结果返回。本文基于 Vue3script setup组合式 API使用原生FetchReadableStream不依赖第三方库完整实现 DeepSeek 接口普通返回 / 流式切换双模式附带逐行代码拆解与底层原理讲解。一、整体实现效果输入提问文本点击提交请求大模型勾选 Streaming 开启流式打字机输出取消则一次性返回完整回答请求过程显示「思考中...」加载状态纯原生 JS 实现流解析无 axios、sse.js 等额外依赖完整封装接口请求、二进制流解码、分段文本拼接逻辑二、分段代码详细解析3.1 响应式数据定义const question ref(讲一个中国龙的故事); const content ref(); const stream ref(true);refVue3 基础响应式 API处理字符串、数字等基础类型底层为RefImpl实例通过.value读写原始值question双向绑定输入框存储用户提问contentAI 回答渲染容器流式模式下持续追加文本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 Tokenimport.meta.env.VITE_XXXVite 环境变量将 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.bodyFetch 响应内置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 truereader.read()返回doneReading true服务端所有数据推送完成流自然关闭手动捕获到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()是异步方法必须加awaitreader.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等待下一轮拼接。完整运行流程示例初始done falsebuffer 进入循环await reader.read()等待服务端下发二进制块得到value二进制、doneReading falsedone false循环继续二进制转字符串和空 buffer 拼接得到完整chunkValue清空 buffer解析chunkValue内所有data:数据行若解析过程有残缺 JSON将残缺片段存入 buffer回到循环开头再次拉取下一块二进制分片自动拼接上一轮残缺缓存关键设计目的buffer 分片容错网络传输时服务端的一行完整 SSE 数据可能被拆成 2~3 个二进制包分次下发包 1data: {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 classcontainer !-- 提问输入区域 -- div label输入/label input classinput v-modelquestion / button clickupdate提交/button /div !-- 输出控制与展示区域 -- div classoutput div labelStreaming/label input typecheckbox v-modelstream / /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输入框使用v-model双向绑定question按钮绑定点击事件触发请求复选框v-model绑定stream一键切换流式开关{{ content }}响应式渲染 AI 回答数据流更新页面自动刷新Style使用 flex 纵向布局适配页面全屏高度输出区域设置最小高度预留回答展示空间基础尺寸、边距优化保证基础交互观感五、环境配置与踩坑指南5.1 环境变量配置项目根目录新建.env文件VITE_DEEPSEEK_API_KEY你的DeepSeek密钥重启项目后import.meta.env可正常读取密钥。5.2 常见问题解决跨域报错本地开发前端直接请求 DeepSeek 接口会触发 CORS解决方案Vite 配置本地代理转发接口请求后端搭建中转接口前端请求自身服务由服务端调用大模型 API中文乱码必须使用TextDecoder解析二进制分片不可直接转字符串二进制分片无法正确识别多字节中文。JSON 解析报错网络分片截断单行 JSON依靠buffer缓存碎片不要直接丢弃解析失败的行。流读取无响应判断response.body是否存在低版本浏览器不支持ReadableStream可增加降级逻辑切换非流式模式。六、核心技术原理总结SSE 流式通信服务端长连接持续推送增量数据相比轮询、WebSocket 更适合大模型文本输出场景ReadableStream浏览器原生流式处理 API无需一次性加载全部响应数据分片处理节省内存Vue 响应式更新每次追加delta增量文本ref触发局部 DOM 更新实现实时打字效果组合式 API 优势接口请求、流处理、状态控制全部逻辑聚合在一处对比 Vue2 选项式代码更易维护、复用