从 Buffer 到响应式流:Vue3 实现 AI 流式输出的完整实践
在现代 Web 开发中,AI 聊天机器人、智能助手等应用越来越普及。为了提升用户体验,流式输出(Streaming Output)成为一种重要的交互方式------用户无需等待模型生成全部内容,而是随着 token 的生成实时看到结果,仿佛"打字机"般逐字呈现。本文将结合 HTML5 的 Buffer 概念与 Vue3 响应式机制,深入讲解如何实现一个简洁高效的 AI 流式输出前端实例。
一、理解 Buffer:流式数据的基础
在开始构建 Vue 应用之前,有必要先理解 Buffer(缓冲区) 的作用。任何网络传输或文件读写本质上都是以二进制形式进行的。JavaScript 提供了 ArrayBuffer 作为底层内存容器,配合 TypedArray(如 Uint8Array)进行操作。
HTML5 引入了两个关键 API:
TextEncoder:将字符串编码为 UTF-8 格式的Uint8ArrayTextDecoder:将Uint8Array解码回字符串
xml
<!-- 示例:Buffer 编解码 -->
<script>
const encoder = new TextEncoder();
const myBuffer = encoder.encode("你好 HTML5"); // Uint8Array [228, 189, 160, ...]
const buffer = new ArrayBuffer(12);
const view = new Uint8Array(buffer);
for (let i = 0; i < myBuffer.length; i++) {
view[i] = myBuffer[i];
}
const decoder = new TextDecoder();
const originalText = decoder.decode(buffer); // "你好 HTML5"
</script>
这个过程揭示了:所有文本在网络中传输时,都以二进制流(Buffer)的形式存在 。当我们通过 fetch 接收 AI 模型的流式响应时,返回的正是这种原始字节流,需要通过 TextDecoder 逐步解码为可读文本。
二、为什么需要流式输出?
传统 API 调用通常采用"请求-等待-返回全部结果"的模式。但对于大语言模型(LLM),生成一段长文本可能耗时数秒。若等到全部生成完毕才返回,用户会感到卡顿、无反馈,体验极差。
而 流式输出(Streaming) 允许后端一边生成 token,一边向前端推送。前端则可以即时渲染,让用户感受到"正在思考"的动态过程,极大提升交互流畅度和心理预期。
主流 LLM API(如 DeepSeek、OpenAI)均支持 stream: true 参数,返回格式为 Server-Sent Events (SSE) ,每条消息以 data: {...} 开头,结束时发送 data: [DONE]。
下面是一个Streaming 的实例的展示:

三、Vue3 + Vite 快速搭建流式聊天界面
使用 Vite 初始化 Vue3 项目是当前最高效的开发方式:
csharp
npm init vite@latest
# 选择 Vue + JavaScript
核心逻辑集中在 <script setup> 中,利用 Vue3 的 ref 实现响应式数据绑定。
1. 响应式状态管理
csharp
import { ref } from 'vue'
const question = ref('讲一个风与风铃的故事,不低于200字')
const stream = ref(true)
const content = ref("")
question:用户输入的问题stream:是否启用流式输出(可切换调试)content:AI 生成的内容,模板中直接绑定,自动更新
2. 调用 DeepSeek API
javascript
const askLLM = async () => {
if (!question.value) return
content.value = "烧烤中..." // 用户友好提示
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({
stream: stream.value,
model: 'deepseek-chat',
messages: [{ role: 'user', content: question.value }]
})
})
注意:API Key 通过 .env 文件配置(VITE_DEEPSEEK_API_KEY),避免硬编码泄露。
四、处理流式响应:逐块解析 SSE 数据
当 stream: true 时,response.body 是一个可读流(ReadableStream)。我们需要通过 getReader() 获取 reader,并循环读取数据块。
ini
if (stream.value) {
content.value = ""
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let done = false
let buffer = '' // 用于拼接不完整的 chunk
while (!done) {
const { value, done: doneReading } = await reader?.read()
done = doneReading
// 将新 chunk 与缓冲区拼接
const chunkValue = buffer + decoder.decode(value, { stream: true })
buffer = ''
// 按行分割,过滤出 data 行
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
break
}
try {
const data = JSON.parse(incoming)
const delta = data.choices[0].delta.content
if (delta) {
content.value += delta // 响应式更新!
}
} catch (err) {
// 若 JSON 解析失败,说明该行不完整,暂存到 buffer
buffer += `data: ${incoming}\n`
}
}
}
}
关键细节解析:
decoder.decode(value, { stream: true })
告诉解码器这不是最后一块数据,避免因 UTF-8 多字节字符被截断而报错。- 缓冲区
buffer的作用
网络传输可能将一行data: {...}拆成多个 chunk。若某次读取只收到一半,需暂存到buffer,下次拼接后再解析。 - 错误处理
使用try...catch捕获JSON.parse异常,防止因不完整数据导致程序崩溃。 - 响应式更新
content.value += delta会自动触发 Vue 模板重渲染,无需手动操作 DOM。
五、模板与样式:简洁直观的 UI
xml
<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>
通过 v-model 实现双向绑定,用户可随时切换流式/非流式模式,便于对比体验差异。
六、总结:技术融合带来卓越体验
本项目虽小,却融合了多项现代 Web 技术:
- HTML5 Buffer API:理解底层数据流
- Fetch + ReadableStream:处理实时网络流
- Vue3 响应式系统:简化状态管理与 DOM 更新
- LLM Streaming 协议:对接 AI 能力
流式输出不仅是技术实现,更是对用户体验的尊重。它让 AI 不再是"黑箱",而是有呼吸、有节奏的对话伙伴。
未来,可进一步优化:
- 添加加载动画
- 支持多轮对话
- 自动滚动到底部
- 错误重试机制
但核心思想不变:用 Buffer 理解数据,用响应式拥抱变化,用流式传递温度。
代码即思想,体验即产品。