vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3流式调用大模型接口

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

作者:mONESY

本文主要介绍了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);

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}`,
};
body: JSON.stringify({
  model: "deepseek-v4-flash",
  messages: [{ role: "user", content: question.value }],
  stream: stream.value,
})

接口入参规范:

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();
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)

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变量
}

3.done = doneReading;

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

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

①decoder.decode(value)

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

②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 个二进制包分次下发:

过滤 SSE 标准数据行

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

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

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;
  }
}

三、完整可运行代码

<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

五、环境配置与踩坑指南

5.1 环境变量配置

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

VITE_DEEPSEEK_API_KEY=你的DeepSeek密钥

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

5.2 常见问题解决

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

六、核心技术原理总结

  1. SSE 流式通信:服务端长连接持续推送增量数据,相比轮询、WebSocket 更适合大模型文本输出场景
  2. ReadableStream:浏览器原生流式处理 API,无需一次性加载全部响应数据,分片处理节省内存
  3. Vue 响应式更新:每次追加delta增量文本,ref触发局部 DOM 更新,实现实时打字效果
  4. 组合式 API 优势:接口请求、流处理、状态控制全部逻辑聚合在一处,对比 Vue2 选项式代码更易维护、复用

到此这篇关于Vue3流式调用大模型接口完整实践的文章就介绍到这了,更多相关Vue3流式调用大模型接口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文