普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月7日首页

使用 Vue 3 实现大模型流式输出:从零搭建一个简易对话 Demo

作者 ohyeah
2025年12月7日 13:39

在当前 AI 应用快速发展的背景下,前端开发者越来越多地需要与大语言模型(LLM)进行交互。本文将基于你提供的 App.vue 代码和学习笔记,带你一步步理解如何使用 Vue 3 + Composition API 构建一个支持 流式输出(Streaming) 的 LLM 对话界面。我们将重点解析代码结构、响应式原理、流式数据处理逻辑,并确保内容通俗易懂,适合初学者或希望快速上手的开发者。


一、项目初始化与技术选型

项目是通过 Vite 初始化的:

npm init vite

选择 Vue 3 + JavaScript 模板。Vite 作为新一代前端构建工具,以其极速的冷启动和热更新能力,成为现代 Vue 项目的首选脚手架。

生成的项目结构简洁清晰,核心开发文件位于 src/ 目录下,而 App.vue 就是整个应用的根组件。


二、Vue 3 的“三明治”结构

.vue 文件由三部分组成:

  • <script setup>:逻辑层(使用 Composition API)
  • <template>:模板层(声明式 UI)
  • <style scoped>:样式层(作用域 CSS)

这种结构让代码职责分明,也便于维护。


三、响应式数据:ref 的核心作用

<script setup> 中,你使用了 ref 来创建响应式变量:

import { ref } from 'vue'

const question = ref('讲一个喜羊羊和灰太狼的小故事,不低于20字')
//控制是否启用流式输出(streaming) 默认开启
const stream = ref(true)
//声明content 单向绑定 用于呈现LLM输出的值
const content = ref('')

什么是 ref

  • ref 是 Vue 3 Composition API 提供的一个函数,用于创建响应式引用对象
  • 它内部包裹一个值(如字符串、数字等),并通过 .value 访问或修改。
  • 在模板中使用时,Vue 会自动解包 .value,所以你只需写 {{ content }} 而非 {{ content.value }}

关键点:当 ref 的值发生变化时,模板会自动重新渲染——这就是“响应式”的核心。

例如:

let count = ref(111)//此时count就为响应式对象
setTimeout(() => {
  count.value = 222 // 模板中绑定 {{ count }} 会自动更新为 222
}, 2000)

这避免了传统 DOM 操作(如 getElementById().innerText = ...),让开发者更专注于业务逻辑。


四、双向绑定:v-model 的妙用

在输入框中,你使用了 v-model

<input type="input" v-model="question" />

v-model 是什么?

  • 它是 Vue 提供的双向数据绑定指令。
  • 输入框的值与 question.value 实时同步:用户输入 → question 更新;question 变化 → 输入框内容更新。
  • 如果改用 :value="question",则只能单向绑定(数据 → 视图),无法实现用户输入自动更新数据。

这使得表单处理变得极其简单。


五、调用大模型 API:异步请求与流式处理

核心功能在 askLLM 函数中实现:

//调用大模型 async await 异步任务同步化
const askLLM = async () => {
  if (!question.value) {
    console.log('question 不能为空!')
    return
    //校验question.value 为空直接return 避免无意义地进行下一步操作
  }
  //提升用户体验 先显示'思考中...' 表示正在处理
  content.value = '思考中...'
  
  //发生请求的时候 首先发送 请求行(方法 url 版本)
  const endpoint = 'https://api.deepseek.com/chat/completions'
  const headers = {
    'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
    'Content-Type': 'application/json'
  }
  
  const response = await fetch(endpoint, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      model: 'deepseek-chat',
      stream: stream.value,//启用流式输出
      messages: [{ role: 'user', content: question.value }]
    })
  })

关键细节:

  1. 环境变量安全:API Key 通过 import.meta.env.VITE_DEEPSEEK_API_KEY 引入。Vite 要求以 VITE_ 开头的环境变量才能在客户端暴露,这是一种安全实践。
  2. 请求体结构:符合 OpenAI 兼容 API 标准,指定模型、是否流式、消息历史。
  3. 用户体验优化:请求发起后立即显示“思考中...”,避免界面卡顿感。

六、流式输出(Streaming)的实现原理

这是本文的重点。当 stream.value === true 时,采用流式处理:

if (stream.value) {
  content.value = ''//先把上一次的输出清空
  //html5 流式响应体 getReader() 响应体的读对象
  const reader = response.body?.getReader()
  const decoder = new TextDecoder()
  let done = false //用来判断流是否结束
  let buffer = ''

  while (!done) {
  //只要流还未结束 就一直拼接buffer
  //解构的同时  重命名 done-> doneReading
    const { value, done: doneReading } = await reader?.read()
    done = doneReading //当数据流结束后 赋值给外部的done 结束while
    const chunkValue = buffer + decoder.decode(value, { stream: true })
    buffer = ''

    const lines = chunkValue.split('\n').filter(line => line.startsWith('data: '))
    for (const line of lines) {
      const incoming = line.slice(6) // 去掉 "data: "  去除数据标签
      if (incoming === '[DONE]') {
        done = true //将外部done改为true 结束循环
        break
      }
      try {
        const data = JSON.parse(incoming)
        const delta = data.choices[0].delta.content
        if (delta) {
          content.value += delta
        }
      } catch (err) {
      //JSON.parse解析失败  拿给下一次去解析
        buffer += `data: ${incoming}`
      }
    }
  }
}

流式输出的工作流程:

  1. 获取可读流response.body.getReader() 返回一个 ReadableStreamDefaultReader

  2. 逐块读取:每次 reader.read() 返回一个 { value, done } 对象,valueUint8Array(二进制数据)。

  3. 解码为字符串:使用 TextDecoder 将二进制转为文本。注意传入 { stream: true } 避免 UTF-8 截断问题。

  4. 按行解析 SSE(Server-Sent Events)

    • 服务端返回格式为多行 data: {...}\n
    • 每行以 data: 开头,末尾可能有 \n\n
    • 遇到 [DONE] 表示流结束。
  5. 拼接增量内容delta.content 是当前 token 的文本片段,不断追加到 content.value,实现“打字机”效果。

💡 为什么需要 buffer?
因为网络传输的 chunk 可能不完整(比如一个 JSON 被切成两半),所以未解析成功的部分暂存到 buffer,下次循环再拼接处理。


七、非流式模式的简化处理

如果不启用流式(stream = false),则直接等待完整响应:

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

这种方式简单直接,但用户体验较差——用户需等待全部内容生成完毕才能看到结果。


八、模板与样式:简洁直观的 UI

<template>
  <div class="container">
    <div>
      <label>输入: </label>
      <input v-model="question" />
      <button @click="askLLM">提交</button>
    </div>
    <div class="output">
      <label>Streaming</label>
      <input type="checkbox" v-model="stream" />
      <div>{{ content }}</div>
    </div>
  </div>
</template>
  • 用户可切换流式/非流式模式。
  • 输出区域实时展示 LLM 的回复。

样式使用 flex 布局,确保在不同屏幕下良好显示。


九、总结与延伸

通过这个 Demo,我们实现了:

✅ 使用 ref 管理响应式状态
✅ 利用 v-model 实现表单双向绑定
✅ 调用 DeepSeek API 发起聊天请求
✅ 支持流式与非流式两种输出模式
✅ 处理 SSE 流式响应,实现逐字输出效果


结语

这个项目虽小,却涵盖了 Vue 3 响应式、异步请求、流式处理等核心概念。正如笔记所说:“我们就可以聚焦于业务,不用写 DOM API 了”。这正是现代前端框架的价值所在——让我们从繁琐的 DOM 操作中解放出来,专注于创造更好的用户体验。

希望这篇解析能帮助你在稀土掘金的读者快速理解代码逻辑,并激发更多关于 AI + 前端的创意!

昨天以前首页
❌
❌