前言
做AI对话页面时,你是不是踩过这个大坑:
发送提问后页面空白转圈,复杂推理要等十几秒才一次性吐出全部文字,用户流失率巨高。
市面上很多教程只给简单demo,没处理网络分片截断、环境变量配置、双向绑定、流结束标记等真实业务问题。
读完本文你能收获:
- 彻底搞懂LLM流式输出底层原理,不用等整段返回,逐字实时渲染
- Vite+Vue3完整可运行代码,兼容DeepSeek官方流式接口
- 解决分片JSON截断、API密钥管理、SSE解析4个高频踩坑点
- 吃透Vue3组件、ref响应式、v-model双向绑定核心知识点
一、什么是流式输出?为什么必须做?
1.1 普通一次性请求的痛点
常规接口请求逻辑:
客户端完整发送请求 → LLM完整推理全部token → 一次性返回完整文本
- 推理耗时久,页面长时间空白,用户极易退出
- 大篇幅回答等待时间成倍拉长,体验极差
1.2 流式输出核心逻辑
把服务器和前端比作一根水管:
LLM每生成一个文字token,立刻通过HTTP分块传输(SSE)推送到前端
前端持续拼接文字,实现打字机逐字蹦字效果
简单理解:
一次性返回 = 接满一桶水再端给你
流式输出 = 水管持续流水,边流边看
1.3 前后端约定规则
- 客户端请求参数携带
stream: true开启流式模式 - 服务端返回SSE格式数据流,每行以
data:开头 - 全部输出完毕,服务端推送
[DONE]标记流结束
二、前置环境准备(Vite项目)
2.1 环境变量配置(安全存放API Key)
项目根目录新建.env.local文件,写入DeepSeek密钥:
# .env.local VITE_DEEPSEEK_API_KEY=sk-你的DeepSeek密钥关键规则(必看,90%人踩坑)
- Vite仅识别**VITE_**前缀的环境变量,无前缀无法在前端读取
.env.local加入.gitignore,禁止上传到代码仓库,避免密钥泄露- 前端直接存放密钥仅适用于本地demo,生产环境需后端代理中转接口
2.2 项目基础依赖
# 创建vue3 vite项目npmcreate vite@latest ai-stream-demo ----templatevuecdai-stream-demonpminstallnpmrun dev三、完整实战代码(App.vue 可直接复制运行)
<template> <div class="container"> <!-- 输入区域 v-model双向绑定提问 --> <div class="input-box"> <label>用户提问:</label> <input type="text" class="input" v-model="question" placeholder="请输入你的问题" /> <button @click="update">提交提问</button> </div> <!-- 流式开关控制 --> <div class="stream-switch"> <label>开启流式输出:</label> <input type="checkbox" v-model="stream" /> <span v-if="stream">当前打字机模式已启用</span> </div> <!-- AI输出区域 --> <div class="output-box"> <label>AI回答:</label> <div class="output-content">{{ content }}</div> </div> <!-- Vue响应式演示 --> <div class="count-demo"> <p>响应式计数:{{ count }}</p> <button @click="count++">点击数字+1</button> </div> </div> </template> <script setup> // Vue3核心响应式API import { ref } from 'vue' // 双向绑定变量 const stream = ref(false) // 是否开启流式 const content = ref('') // AI返回内容 const question = ref('讲一个关于奶龙的小故事') // 用户提问 const count = ref(0) // 响应式数字演示 // 5秒后自动修改数字,验证响应式自动更新 setTimeout(() => { count.value = 100 }, 5000) // 提交请求核心方法 const update = async () => { // 空提问拦截 if (!question.value.trim()) return content.value = 'AI正在思考中...' // DeepSeek官方接口地址 const endpoint = 'https://api.deepseek.com/v1/chat/completions' const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}` } // 发起POST请求 const response = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify({ model: 'deepseek-v4-flash', messages: [{ role: 'user', content: question.value }], stream: stream.value // 流式开关传递给大模型接口 }) }) // 分支:流式输出 / 一次性输出 if (stream.value) { content.value = '' // 获取二进制可读流读取器 const reader = response.body?.getReader() // 二进制转文本解码器 const decoder = new TextDecoder() let done = false // 缓存分片半截数据,解决JSON截断问题 let buffer = '' // 循环读取流,直到服务端推送结束标记 while (!done && reader) { const result = await reader.read() done = result.done // 解码本次二进制数据,拼接缓存 buffer += decoder.decode(result.value, { stream: !done }) // 按换行分割SSE单行数据 const lines = buffer.split('\n') // 未完成的半截数据留在buffer,下一轮拼接 buffer = lines.pop() || '' // 遍历每一行有效消息 for (const line of lines) { // 过滤非data开头的无效行 if (!line.startsWith('data:')) continue const data = line.slice(5).trim() // DeepSeek流结束标识,终止循环 if (data === '[DONE]') { done = true break } // 解析增量文本,追加到页面 const chunk = JSON.parse(data) content.value += chunk.choices[0]?.delta?.content || '' } } } else { // 非流式:一次性接收完整返回 const data = await response.json() content.value = data.choices[0].message.content } } </script> <style scoped> .container { width: 90%; max-width: 800px; margin: 30px auto; } .input-box, .stream-switch, .output-box, .count-demo { margin-bottom: 20px; } .input { width: 70%; padding: 8px 12px; margin: 0 10px; } .output-content { margin-top: 8px; padding: 12px; border: 1px solid #eee; border-radius: 6px; min-height: 120px; white-space: pre-wrap; } </style>四、代码核心模块拆解(边学Vue边懂流式)
4.1 Vue3 .vue组件三大核心部分
每一个.vue文件由三块组成,也是前端工程化基础:
template 模板
写页面结构,支持{{}}单向数据渲染、v-model双向绑定、@click事件绑定
数据驱动页面,无需手动操作DOM,数据改变页面自动刷新script setup
Vue3语法糖,自动导出变量,模板可直接使用内部ref变量ref()定义响应式基础数据,修改.value触发页面更新style scoped
scoped限定样式仅作用于当前组件,避免全局样式污染
4.2 双向绑定 v-model 原理
:value:单向绑定,仅把数据渲染到输入框,用户输入无法同步回变量v-model:语法糖,同时绑定value+监听输入事件,实现数据↔界面双向同步
对话输入框、流式开关都依赖v-model实现交互
4.3 流式输出核心逻辑解析
response.body.getReader():获取HTTP分块二进制流读取器TextDecoder:把Uint8Array二进制数据转成可读字符串buffer缓存变量(重中之重)
网络传输会把完整JSON拆成半截数据包,直接解析会报错。
所有不完整数据存入buffer,下一次接收数据拼接完整后再解析- 过滤
data:前缀、识别[DONE]结束标记,逐段取出delta增量文字追加
五、开发必踩4个坑&解决方案
坑1:直接解析分片数据,JSON.parse报错
现象:开启流式后控制台频繁抛出JSON语法错误
原因:网络分包截断,单行data不完整
解决:新增buffer缓存,分割行后剩余半截数据留存到下一轮
坑2:Vite读取不到API密钥
现象:import.meta.env.VITE_DEEPSEEK_API_KEY为undefined
解决:
- 变量必须以
VITE_开头 - 重启vite开发服务,环境变量修改后不会热更新
- 文件名为
.env.local,放置项目根目录
坑3:忘记判断[DONE],循环无限执行
现象:AI输出完成后代码持续解析,页面疯狂报错
解决:读到data === '[DONE]'时手动修改done为true,终止while循环
坑4:前端明文存储API密钥,存在泄露风险
现象:抓包可直接看到完整密钥,容易被盗刷额度
解决方案:
- 本地学习demo可用.env.local临时存储
- 线上生产环境:新增后端接口做代理,密钥存服务端环境变量,前端只请求自家后端
六、低代码对比反思(延伸思考)
很多团队想用低代码平台快速做AI对话页面,但实际落地问题极多:
- 复杂流式逻辑无法纯配置实现,必须手写大量自定义JS钩子
- schema配置无语法校验,字段名改动直接整页崩溃
- 多人协作时,产品拖拽生成千行无注释配置,前端大量时间用来调试schema
结论:
简单CRUD后台、运营落地页适合低代码;
AI流式对话、复杂交互页面,原生Vue开发更易维护、性能更好。
七、全文总结
- 流式输出核心是SSE分块传输,前端通过ReadableStream逐段接收文字,大幅降低用户等待焦虑
- Vue3 + Vite是AI前端页面最优技术栈,ref响应式、v-model双向绑定简化交互开发
- 流式代码必须处理buffer分片缓存、流结束标记、环境变量三大基础问题,否则线上必出bug
- 简单页面可选低代码,AI聊天这类高交互场景,原生组件开发更稳定易维护