📝摘要
在 AI 应用、智能 Agent 开发中,流式输出已经是对话界面标配交互。不用等待大模型完整生成全部内容再一次性渲染,借助浏览器原生
ReadableStream,搭配 DeepSeek 大模型接口,轻松实现边生成、边展示的打字机动画。本文从底层原理、工程思路逐层讲解,附 Vue3<script setup>实战代码,无需全盘复制代码,吃透核心逻辑就能迁移到任意前端项目。
🤖 一、Agent 时代,我们该如何调整开发思路
当下大模型 Agent 持续迭代,能力越来越贴近自主完成复杂任务,也是迈向 AGI 的重要方向。一套全新的人机协作模式正在成型:把 AI 擅长的标准化、重复性工作交给 Agent,人类聚焦决策、审核与核心业务把控。
落实到前端开发,有两条极具实践价值的思路:
- 拒绝从零搭建项目骨架不必每次新建项目就手写
index.html、App.vue等基础文件,可以直接前往 Github 拉取成熟模板,在模板基础上迭代业务。 - 合理划分人与 AI 工作边界工程初始化、样板代码、通用基础组件搭建交给 Agent;复杂交互逻辑、边界异常处理、核心业务规则由开发者主导把控。
⚡ 二、Vite 热更新:调试流式项目的效率神器
本次 Demo 基于 Vite + Vue3 开发,Vite 自带的 热更新(Hot Reload 是本地开发的利器。
- 传统打包模式:修改代码 → 整页刷新 → 页面所有运行状态全部丢失试想调试流式对话,每次改动代码都要重新输入提问,大量时间被无效消耗。
- 热更新机制:仅更新被修改的模块,保留页面当前数据与运行状态,实现局部刷新。调试 LLM 流式逻辑时,能够省去大量重复操作。
🌊 三、流式输出底层原理:二进制 ReadableStream
传统一次性请求模式
前端发起请求 → 大模型服务器完整计算全部答案 → 返回完整 JSON → 页面一次性渲染。当 AI 输出长文本时,用户长时间只能看到 “加载中”,交互体验很差。
Stream 流式模式
大模型一边生成 Token,一边持续把增量文本分片下发至前端。前端收到分片数据立刻追加渲染,也就是大家熟悉的打字机效果。
浏览器层面,LLM 返回的流式响应本质就是二进制数据流 ReadableStream,可以用水管模型通俗理解:
response.body:服务端返回的管道 ReadableStream;.getReader():获取读取器 reader,持续从管道中取数据;reader.read():每次取出一块Uint8Array二进制数组(数值范围 0~255 无符号整数);TextDecoder:二进制字节无法直接转换成可读文本,依靠解码器完成翻译。
📡 四、LLM 流式通信规范:类 SSE 分片格式
DeepSeek、OpenAI 等主流大模型流式接口,统一遵循类似 SSE 的数据分片规则:
- 每一条有效数据行以
data:开头; - 全部内容推送完成,服务端下发
data: [DONE]作为结束标记。
示例原始流片段:
plaintext
data: {"choices":[{"delta":{"content":"从"}}]}
data: {"choices":[{"delta":{"content":"前"}}]}
data: [DONE]
前端标准处理流程:
- 将解码后的字符串按照换行分割;
- 过滤筛选出以
data:开头的数据行; - 过滤结束标记
[DONE]; - JSON 解析分片,取出
delta.content增量文本; - 持续拼接文本,更新页面视图。
⚠️ 两个新手高频踩坑点:
- JS 原生字符串方法是
startsWith,极易手误写成startWith,直接抛出函数不存在报错;- 单次读取的二进制分片有可能截断 JSON 字符串,必须设置
buffer缓冲区,保存残缺片段留给下一轮循环解析。
💻 五、Vue3 核心实战思路(重点!不必全盘抄代码)
采用 Vue3 <script setup>组合式 API,对比 Vue2 选项式 API 优势明显:同一业务相关逻辑收拢在一处,代码内聚性更强,方便维护。
基础响应式变量
js
运行
import { ref } from 'vue';
// 用户提问
const question = ref('讲一个中国龙的故事');
// AI输出内容
const content = ref('');
// 开关:是否启用流式输出
const stream = ref(true);
请求主干逻辑
js
运行
const update = async () => {
if (!question.value) return;
content.value = '思考中...';
const endpoint = 'https://api.deepseek.com/chat/completions';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
},
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;
let buffer = ''; // 缓存残缺JSON字符串
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
if(!value) continue;
// 二进制解码,拼接缓存
const chunk = buffer + decoder.decode(value);
buffer = '';
const lines = chunk.split('\n');
for(const line of lines){
const trimLine = line.trim();
if(!trimLine.startsWith('data: ')) continue;
const dataStr = trimLine.replace('data: ','');
if(dataStr === '[DONE]') continue;
try{
const res = JSON.parse(dataStr);
const text = res.choices[0].delta.content;
if(text) content.value += text;
}catch{
// JSON不完整,存入buffer,下一轮继续解析
buffer = line;
}
}
}
}else{
// 非流式:一次性接收完整结果
const data = await response.json();
content.value = data.choices[0].message.content;
}
}
✅ 需要重点掌握 3 块核心,其余代码可按需删减改造:
while循环持续调用reader.read()拉取二进制分片;buffer缓冲区容错,解决 JSON 被截断问题;- 不断追加增量文本,利用 Vue 响应式自动更新页面。
极简模板参考:
vue
<template>
<div class="container">
<div>
<label>输入:</label>
<input v-model="question"/>
<button @click="update">提交</button>
</div>
<div>
<label>Streaming</label>
<input type="checkbox" v-model="stream"/>
</div>
<div>{{ content }}</div>
</div>
</template>
🚨 六、上线前必须重视的 3 个风险
- CORS 跨域限制浏览器直接在前端调用 DeepSeek 公网接口会触发跨域。本地开发可以使用 Vite 代理;生产环境必须增加后端中转层。
- API 密钥泄露前端项目内的密钥无法做到保密,用户打开浏览器开发者工具就能抓取。线上项目禁止前端直接请求大模型接口。
- 网络异常处理断流、超时、分片解析失败都需要兜底捕获,
buffer缓冲区不能随意删除,是流式稳定运行的关键。
🧠 七、拓展:流式输出在 Agent 开发中的价值
流式输出不只是实现好看的打字动画。在智能 Agent 场景下,AI 会分步完成思考、工具调用、结果总结。依托流式推送,我们可以实时展示 Agent 完整思考链路、工具调用日志,让使用者清晰看见 AI 如何拆分任务、执行动作。
掌握浏览器数据流处理,是开发 AI 对话、智能 Agent 前端界面的必备基础能力