Vue3 + DeepSeek 实现AI流式输出,吃透响应式与流式交互

98 阅读6分钟

前言

在AI应用开发中,流式输出是提升用户体验的关键——相比等待完整结果返回,实时看到内容逐字生成的交互感更符合人类认知习惯。本文将基于Vue3 + DeepSeek API,逐行拆解核心逻辑,从Vue3响应式基础到流式数据解析,手把手教你实现一个可切换流式/非流式的AI聊天Demo,同时吃透Vue3 ref 响应式、Fetch API、ReadableStream等核心知识点。

一、项目初始化:快速搭建Vue3工程

1. 为什么选Vite?

Vite是目前Vue3生态中最轻量化、高性能的构建工具,相比传统webpack,它的冷启动速度、热更新效率提升数倍,尤其适合快速开发小Demo。

2. 初始化步骤(附核心注释)

# 初始化Vite项目
npm init vite
# 交互选择:Vue → JavaScript(新手友好,无需TS)
# 进入项目目录
cd 你的项目名
# 安装依赖
npm install
# 启动开发服务(默认端口5173)
npm run dev

生成的项目结构中,src/App.vue是核心组件文件,我们所有代码都集中在这里开发。

二、核心代码逐行解析(从脚本到视图)

(一)脚本部分:业务逻辑核心(<script setup>

<script setup>是Vue3组合式API的语法糖,无需手动注册组件/方法,代码更简洁,是当前Vue3开发的主流写法。

1. 响应式数据定义(ref 核心用法)

// 1. 导入Vue3响应式核心API:ref
import { ref } from 'vue'

// 2. 定义响应式数据(核心:数据变 → 视图自动更)
// 重点:ref包装后是「响应式对象」,而非原始值
// 用于绑定输入框的问题内容(双向绑定)
const question = ref('讲一个喜洋洋和灰太狼的故事,20字')
// 控制是否开启流式输出(绑定复选框)
const stream = ref(true)
// 存储AI返回的内容(单向绑定到页面展示)
const content = ref("")

关键知识点拆解

  • ref 作用:为基本数据类型(字符串/数字/布尔) 创建响应式包装器(复杂类型推荐reactive,但新手先掌握ref即可);
  • 脚本中修改:必须通过.value(如content.value = "新内容"),模板中可直接用(如{{content}});
  • 响应式本质:Vue3通过代理(Proxy)监听ref.value变化,一旦修改,自动触发模板更新(告别jQuery手动操作DOM的时代)。

2. 核心方法:调用DeepSeek API(askLLM

const askLLM = async () => { 
  // 1. 输入校验(用户体验:空问题不请求)
  if (!question.value) {
    console.log('question 不能为空');
    return 
  }

  // 2. 加载状态提示(提升用户体验)
  content.value = '思考中...';

  // 3. API请求配置(请求行+请求头+请求体)
  // 接口地址(DeepSeek官方聊天接口)
  const endpoint = 'https://api.deepseek.com/chat/completions';
  // 请求头:鉴权 + 数据格式声明
  const headers = {
    // 环境变量:保护敏感信息(API Key不硬编码)
    'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
    'Content-Type': 'application/json' // JSON格式请求体必须声明
  }

  // 4. 发送POST请求(Fetch API异步请求)
  const response = await fetch(endpoint, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      model: 'deepseek-chat', // DeepSeek固定模型名
      stream: stream.value,   // 控制是否流式输出(绑定复选框)
      messages: [             // 对话消息体(遵循OpenAI格式)
        {
          role: 'user',       // 角色:用户
          content: question.value // 问题内容(响应式数据)
        }
      ]
    })
  })

关键知识点拆解

  • async/await:处理异步请求,避免回调地狱(Fetch API返回Promise);

  • 环境变量:Vite中以VITE_开头的变量会被暴露,需在项目根目录创建.env文件配置:

VITE_DEEPSEEK_API_KEY=你的DeepSeek API Key
  • Fetch API:浏览器原生请求API。

3. 流式输出处理(核心难点,逐行注释)

if (stream.value) {
  // 清空上次的输出内容(避免新旧内容叠加)
  content.value = "";
  // 获取流式响应读取器(ReadableStream核心)
  // response.body是ReadableStream,getReader()用于逐块读取
  const reader = response.body?.getReader();
  // 二进制流解码为字符串(解决流式数据的编码问题)
  const decoder = new TextDecoder();
  let done = false;  // 标记流是否读取完成
  let buffer = '';   // 缓存不完整的分片数据(关键:处理JSON截断)

  // 循环读取流数据,直到done为true
  while(!done) { 
    // 读取下一块数据:解构并重命名done为doneReading(避免变量冲突)
    const { value, done: doneReading } = await reader?.read()
    done = doneReading; // 更新流结束状态

    // 解码二进制数据 + 拼接缓存(处理分片不完整的情况)
    // value是Uint8Array格式,decoder.decode转为字符串
    const chunkValue = buffer + decoder.decode(value);
    buffer = ''; // 清空缓存,准备接收新的不完整数据

    // 按换行分割数据,过滤出以data:开头的有效行(DeepSeek流式格式)
    // DeepSeek流式返回每行格式:data: {"id":"xxx","choices":[{"delta":{"content":"喜"}}]}
    const lines = chunkValue.split('\n').filter(line => line.startsWith('data: '))
    
    // 遍历每一行有效数据
    for (const line of lines) {
      // 去掉"data: "前缀,获取纯JSON字符串
      const incoming = line.slice(6); 
      
      // 流结束标记:[DONE](DeepSeek约定的结束符)
      if (incoming === '[DONE]') {
        done = true;
        break;
      }

      try {
        // 解析JSON数据(获取AI生成的单个token)
        const data = JSON.parse(incoming);
        // 核心:获取逐字生成的内容(delta.content是单个字符/词语)
        const delta = data.choices[0].delta.content;
        // 拼接内容到响应式数据(模板自动更新,实现逐字渲染)
        if (delta) {
          content.value += delta;
        }
      } catch(err) {
        // 解析失败:说明当前分片不完整,缓存起来下次拼接
        // 例如:某次读取的JSON只到一半,缓存后下次和新数据拼接再解析
        buffer += `data: ${incoming}`
      }
    }
  }
}

流式输出核心逻辑总结

  1. getReader()逐块读取二进制流;
  2. TextDecoder解码为字符串;
  3. buffer缓存不完整的JSON分片(解决截断问题);
  4. 过滤data: 开头的行,解析出逐字内容并拼接;
  5. 遇到[DONE]标记流结束。

4. 非流式输出处理(对比理解)

} else {
  // 非流式:直接解析完整JSON响应(传统方式)
  const data = await response.json();
  console.log(data);
  // 赋值到响应式数据,模板自动更新
  content.value = data.choices[0].message.content;
}

非流式输出是一次性获取完整结果,适合对实时性要求不高的场景,代码更简单,但用户体验不如流式。

(二)模板部分:视图渲染(<template>

模板是Vue3的声明式视图,通过指令绑定数据和事件,无需手动操作DOM。

<template>
  <div class="container">
    <!-- 输入区域 -->
    <div>
      <label>输入:</label>
      <!-- v-model:双向绑定question响应式数据 -->
      <!-- 输入框内容变化 → question.value自动更新;question.value变化 → 输入框内容自动更新 -->
      <input class="input" v-model="question"/>
      <!-- 点击事件绑定:调用askLLM方法 -->
      <button @click="askLLM">提交</button>
    </div>
    
    <!-- 输出区域 -->
    <div class="output">
      <div>
        <!-- v-model:双向绑定stream开关(复选框) -->
        <label>Streaming</label>
        <input type="checkbox" v-model="stream" />
        <!-- 插值表达式:单向绑定content,数据变则视图变 -->
        <div>{{content}}</div>
      </div>
    </div>
  </div>
</template>

核心指令解析

  • v-model:双向数据绑定,适用于表单元素(输入框、复选框等),是Vue3「数据驱动视图」的核心体现;
  • @click:事件绑定语法糖(等价于v-on:click),点击按钮触发askLLM方法;
  • {{content}}:插值表达式,将响应式数据渲染到DOM中,数据更新时自动刷新。

(三)样式部分:页面美化(<style scoped>

<style scoped>
* {
  margin: 0;
  padding: 0;
}
.container {
  display: flex;
  flex-direction: column;
  /* 主轴(垂直):靠左对齐,次轴(水平):顶部对齐 */
  align-items: start;
  justify-content: start;
  height: 100vh; /* 容器高度占满视口 */
  font-size: 0.85rem;
}
.input {
  width: 200px;
}
button {
  padding: 0 10px;
  margin-left: 6px;
}
.output {
  margin-top: 10px;
  min-height: 300px; /* 保证输出区域高度 */
  width: 100%;
  text-align: left;
}
</style>

关键样式说明

  • scoped:样式仅作用于当前组件,避免全局样式污染(Vue3的样式隔离方案);
  • display: flex:弹性布局,快速实现垂直排列的页面结构,无需繁琐的浮动/定位;
  • 100vh:让容器占满整个视口高度,保证页面布局完整。

三、运行与测试(新手友好版)

1. 配置API Key

在项目根目录新建.env文件,添加你的DeepSeek API Key(需先在DeepSeek官网申请):

VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

2. 启动项目

npm run dev

访问http://127.0.0.1:5173/,即可看到效果:

  • 勾选「Streaming」:点击提交,内容逐字生成(流式输出);
  • 取消勾选:点击提交,等待完整结果后一次性展示。

四、核心知识点总结(新手必记)

知识点核心作用
Vue3 ref为基本数据类型创建响应式对象,数据变 → 视图自动更
v-model表单元素与响应式数据双向绑定
Fetch API发送异步请求,支持流式响应处理
ReadableStream逐块读取流式响应,实现实时渲染
TextDecoder二进制流解码为字符串,解决编码问题
Vite环境变量安全管理敏感信息(如API Key)

五、扩展与优化方向

  1. 异常处理增强:增加网络错误、API Key失效、接口限流等场景的提示;
  2. 样式美化:模拟聊天框样式,区分用户消息和AI消息;
  3. 历史记录:用数组存储对话历史,支持多轮对话;
  4. 加载动画:流式输出时添加打字机光标效果。

六、总结

本文基于Vue3 + DeepSeek API,从Vue3响应式基础、API请求配置到流式数据解析,完整拆解了AI流式输出Demo的核心逻辑。重点掌握:

  1. ref 响应式数据的使用(脚本.value,模板直接用);
  2. v-model 双向绑定表单数据;
  3. ReadableStream流式数据的解析(二进制流 → 字符串 → 逐字渲染);
  4. 环境变量的配置(保护敏感信息)。

这个Demo是前端对接AI接口的典型场景,掌握这些知识点后,你可以轻松适配OpenAI等其他大模型的流式接口,也能理解Vue3数据驱动视图的核心思想,从写代码到懂原理。

希望这篇文章能帮你吃透Vue3流式开发的底层逻辑,新手也能快速上手AI应用开发!