Vue 3 实现 LLM 流式输出的完整 Demo 教程
想象一下:你问 AI 一个问题,它不是沉默几秒后“砰”地甩出一大段答案,而是像一位沉思的作家,一个字、一个词、一句话地在你眼前“写”出来——文字如溪流般缓缓流淌,思绪仿佛触手可及。这种边想边说的体验,就是“流式输出”(Streaming Output)的魅力所在。
在 AI 应用遍地开花的今天,用户体验早已不只是功能是否实现,更是等待是否值得、交互是否流畅。而流式输出,正是让 AI 回答从“冷冰冰的结果”变成“有温度的对话”的关键一步。本文将带你用 Vue 3 + Vite,亲手打造一个能“看着 AI 写字”的前端 Demo,全程代码清晰、原理透彻、效果惊艳!
一、搭好舞台:用 Vite 三分钟起个 Vue 3 项目
开发就像演戏,得先搭好舞台。Vite 就是那个又快又稳的舞台搭建师——它不用打包,直接用浏览器原生支持的 ES 模块,启动速度飞快。
打开终端,敲下:
npm init vite
接着按提示操作:
- 项目名?随便起,比如
ai-stream-demo - 框架选 Vue
- 语言选 JavaScript
回车!几秒后,一个崭新的 Vue 3 项目就躺在你面前了。进入目录,装依赖,跑起来:
cd ai-stream-demo
npm install
npm run dev
浏览器自动打开,一个干净的 Vue 起始页跃然眼前。我们的主角 App.vue 就在这里,它将承载整个流式对话的魔法。
二、让数据“活”起来:Vue 3 的响应式魔法
在传统 JS 里,你要改页面文字,得先 getElementById,再 .innerText = ...,像拧螺丝一样机械。但 Vue 说:“别管 DOM,只管数据!”
我们用 ref() 声明一个“会呼吸”的变量:
import { ref } from 'vue'
const content = ref('') // 初始是空,但它“活”着!
只要 content.value 一变,模板里 {{ content }} 的地方就会自动更新——就像给数据装了神经,一动全身都知。
这正是我们要的效果:AI 每吐出一个字,我们就往 content 里塞一点,页面立刻跟上,用户就能看到文字“生长”的过程。
三、流式输出:为什么它让人“上头”?
普通 API 调用像点外卖:下单 → 等 → 一次性端上整桌菜。
流式输出则像看厨师现场烹饪:切菜、爆香、翻炒……每一步都看得见。
对 LLM 来说,生成 100 个字可能需要 5 秒。非流式模式下,用户前 4.9 秒面对的是空白或“加载中”;而流式模式,第 0.5 秒就能看到第一个词,心理感受天差地别。
开启流式,只需在请求体加一行:
{ "stream": true }
服务端就会用 SSE(Server-Sent Events) 协议,把回答切成小块,像发短信一样一条条推过来,格式如下:
data: {"choices": [{"delta": {"content": "今"}}]}
data: {"choices": [{"delta": {"content": "天"}}]}
...
data: [DONE]
我们的任务,就是把这些碎片拼成完整的句子,并实时显示。
四、动手实现:让 AI 在你眼前“写字”
下面是完整的 App.vue 代码,每一行都经过精心打磨:
<script setup>
import { ref } from 'vue'
// 用户输入的问题
const question = ref('讲一个喜羊羊和灰太狼的搞笑故事,不少于30字')
// 是否启用流式
const stream = ref(true)
// AI 正在写的“草稿”
const content = ref('')
const askLLM = async () => {
if (!question.value.trim()) return alert('问题不能为空哦!')
// 先清空上次结果
content.value = stream.value ? '🧠 正在思考...' : ''
const res = await fetch('https://api.deepseek.com/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'deepseek-chat',
stream: stream.value,
messages: [{ role: 'user', content: question.value }]
})
})
// 🌊 流式处理:逐字接收
if (stream.value && res.body) {
content.value = '' // 开始写字!
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = '' // 用于拼接跨 chunk 的数据
while (true) {
const { done, value } = await reader.read()
if (done) break
// 把二进制转成字符串,并保留未完成的部分
const chunk = decoder.decode(value, { stream: true })
const lines = (buffer + chunk).split('\n').filter(line => line.startsWith('data: '))
buffer = ''
for (const line of lines) {
const payload = line.slice(6) // 去掉 "data: "
if (payload === '[DONE]') return
try {
const data = JSON.parse(payload)
const text = data.choices?.[0]?.delta?.content
if (text) content.value += text // ✨ 关键!追加文字
} catch (e) {
// 如果 JSON 解析失败(比如被截断),暂存到 buffer
buffer += line + '\n'
}
}
}
}
// 📦 非流式:一次性拿完整答案
else {
const data = await res.json()
content.value = data.choices?.[0]?.message?.content || '哎呀,AI 罢工了~'
}
}
</script>
<template>
<div class="container">
<h1>✨ 看 AI 写字 ✨</h1>
<div class="input-area">
<input v-model="question" placeholder="输入你的问题..." />
<button @click="askLLM">提问</button>
<label>
<input type="checkbox" v-model="stream" />
启用流式输出(推荐!)
</label>
</div>
<div class="output-box">
<p>{{ content || '等待你的问题...' }}</p>
</div>
</div>
</template>
<style scoped>
.container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
font-family: 'Segoe UI', sans-serif;
}
.input-area {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
width: 300px;
}
button {
padding: 10px 16px;
background: #4f46e5;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.output-box {
min-height: 200px;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
white-space: pre-wrap;
line-height: 1.6;
}
</style>
别忘了在项目根目录创建 .env 文件,填入你的 DeepSeek API Key:
VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
💡 提示:
.env文件不要提交到 Git!把它加入.gitignore。
五、背后的技术细节:如何优雅处理“数据碎片”?
流式输出最棘手的问题是:网络传输不分 JSON 边界。一个 {"content":"你好"} 可能被切成 {"cont 和 ent":"你好"} 两块。
我们的解决方案是:
- 用
buffer缓存不完整的行; - 每次新数据到来,先和 buffer 拼接;
- 按
\n分割,只处理以data:开头的完整行; - 解析失败的,重新塞回 buffer,等下一块来“补全”。
这种“拼图式”处理,确保了即使在网络抖动下,也能稳定还原原始数据。
六、结语:让技术有温度
这个 Demo 不只是代码,更是一种产品思维的体现:技术不该让用户等待,而应让用户参与过程。当文字在屏幕上逐字浮现,AI 不再是黑箱,而成了一个“正在思考的朋友”。
你可以在此基础上继续拓展:
- 添加自动滚动到底部
- 支持多轮对话历史
- 加入打字机动画
- 用 Web Worker 防止主线程卡顿
愿你在构建 AI 应用的路上,不仅追求智能,更传递温度。现在,去运行你的项目,亲眼看看 AI 是如何“写字”的吧!✍️