使用 Vue 3 实现大模型流式输出:从零搭建一个简易对话 Demo
在当前 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 }]
})
})
关键细节:
-
环境变量安全:API Key 通过
import.meta.env.VITE_DEEPSEEK_API_KEY引入。Vite 要求以VITE_开头的环境变量才能在客户端暴露,这是一种安全实践。 - 请求体结构:符合 OpenAI 兼容 API 标准,指定模型、是否流式、消息历史。
- 用户体验优化:请求发起后立即显示“思考中...”,避免界面卡顿感。
六、流式输出(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}`
}
}
}
}
流式输出的工作流程:
-
获取可读流:
response.body.getReader()返回一个ReadableStreamDefaultReader。 -
逐块读取:每次
reader.read()返回一个{ value, done }对象,value是Uint8Array(二进制数据)。 -
解码为字符串:使用
TextDecoder将二进制转为文本。注意传入{ stream: true }避免 UTF-8 截断问题。 -
按行解析 SSE(Server-Sent Events) :
- 服务端返回格式为多行
data: {...}\n。 - 每行以
data:开头,末尾可能有\n\n。 - 遇到
[DONE]表示流结束。
- 服务端返回格式为多行
-
拼接增量内容:
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 + 前端的创意!