大疆无人机:如何革新雪崩救援行动
2026-07-14 3398501
2026-07-14 0
使用过早期 AI 产品的同学一定有过这种体验:问一个问题,页面卡住,几十秒后"啪"地一下甩出一大段文字。这种一次性返回的模式有三个致命问题:

对于复杂的推理计算(比如处理长文本、代码生成、多步推理),LLM 的耗时可能长达数十秒甚至数分钟。作为前端工程师,我们有责任把这段等待时间变成一个渐进式、可感知的交互过程——这就是流式输出(Streaming)的价值。
流式输出的核心思想非常朴素:
复制代码 传统方式:服务端生成完整回答 → 一次性返回 → 客户端渲染
流式方式:服务端每生成一个 token → 立即发送 → 客户端逐个拼接渲染
打个比方:传统方式就像等一桶水接满后一次性提走,而流式输出就像拧开水龙头,水(token)源源不断地流向客户端。
在技术实现上,这要求服务端和客户端达成一个共同约定——请求体中设置 stream: true。服务端收到这个标记后,不再等所有 token 生成完毕才返回一个完整的 JSON,而是每生成一个 token 就立即以 SSE(Server-Sent Events)或分块传输的方式推送给客户端。
服务端返回的不再是一个 JSON 对象,而是一个 ReadableStream(可读流)。客户端需要:
response.body.getReader() 拿到流的读取器;TextDecoder 将二进制数据解码为文本;data: {...}nn 的格式,需要逐行解析;整个过程可以用一个 while 循环来完成——只要流没有关闭(done !== true),就不停地读取、解码、拼接、渲染,直到收到 [DONE] 标记。
下面我们基于 Vite + Vue 3 搭建一个最小化的流式输出 Demo,对接 DeepSeek API。
复制代码stream-demo/
├── index.html # 入口 HTML,提供 #app 挂载点
├── src/
│ ├── main.js # Vue 应用初始化
│ ├── App.vue # 核心组件(聊天界面 + 流式逻辑)
│ └── style.css # 全局样式
└── .env.local # 存放 VITE_DEEPSEEK_API_KEY
复制代码<div id="app">div>
<script type="module" src="/src/main.js">script>
非常简单——一个 #app 挂载点,加上 ES Module 方式引入入口脚本。Vite 的开发服务器(默认 :5173)会将 / 映射到 index.html,之后完全由 Vue 接管 DOM。
复制代码import { createApp } from 'vue'
import App from './App.vue'createApp(App).mount('#app')
createApp 创建一个 Vue 应用实例,以 App.vue 作为根组件,挂载到 #app 节点上。这就是整个 Vue 应用的起点。
这是整个 Demo 的灵魂所在,我们拆成三部分来看。
复制代码<template>
<div class="container">
<div>
<label>输入:label>
<input type="text" class="input" v-model="question">
<button @click="update">提交button>
div>
<div class="output">
<input type="checkbox" v-model="stream" />
<label>Streaminglabel>
<div>{{ content }}div>
div>
div>
template>
这里展示了 Vue 的两个核心能力:
v-model:双向数据绑定。表单输入是单向数据流的特例——用户输入需要回流到数据层,v-model 让 question 变量和 的 value 始终保持同步。{{ }} 插值表达式:将 script 中的数据响应式地渲染到页面上。content 变化,页面自动更新,完全不需要手动操作 DOM。 复制代码import { ref } from 'vue'const question = ref('讲一个关于中国龙的故事')
const stream = ref(false)
const content = ref('')const update = async () => {
if (!question.value) return
content.value = '思考中....' const response = await fetch('https://api.deepseek.com/chat/completions', {
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 while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
if (value) {
// 解码二进制 → 文本 → 解析 SSE → 拼接 token
const chunk = decoder.decode(value, { stream: true })
// 解析 data: {...} 行,提取 delta content
// 追加到 content.value
}
}
} else {
// === 非流式路径 ===
const data = await response.json()
content.value = data.choices[0].message.content
}
}
这里的核心设计是:
ref() 响应式数据:question、stream、content 都是响应式状态。Vue 3 的 ref 包装后,.value 的修改会自动触发模板重新渲染。这就是数据驱动视图——你只需要关心数据,不用碰 DOM。
API Key 管理:通过 import.meta.env.VITE_DEEPSEEK_API_KEY 读取环境变量。Vite 会自动加载 .env.local 中以 VITE_ 为前缀的变量。把 Key 放在环境变量里,既安全又方便切换。
stream 参数分叉:代码通过 if (stream.value) 优雅地分出了两条路径——勾选 Streaming 走流式读取,不勾选走传统 response.json() 一次性解析。这让 Demo 可以直观对比两种模式。
ReadableStream 消费:response.body.getReader() 返回一个 ReadableStreamDefaultReader,reader.read() 每次返回 { value: Uint8Array, done: boolean }。循环读取直到 done === true。
样式层保持了极简风格,但在实际 AI 产品中,这里大有可为——打字机光标动画、逐字淡入效果、Markdown 实时渲染……这些都是在流式输出的基础上叠加的体验增强。
整个 Demo 体现了一个重要的现代前端理念——组件化。
一个 .vue 文件就是一个组件,它将 Template(结构) + Script(逻辑) + Style(样式) 封装在一起,形成一个独立、可复用、可维护的业务单元。
想象一下 Facebook 的网页由超过一万个组件构成——就像用乐高积木搭城堡,每个积木块独立且可替换,但组合起来就能构建出复杂的应用界面。
流式输出看起来只是"一个字一个字蹦出来",但它其实解决了 AI 产品中一个本质的用户体验问题——在不可消除的延迟面前,让用户感知到进展,而不是在空白中焦虑。
实现上,它的技术链路是:
复制代码服务端 stream:true → ReadableStream → getReader() → TextDecoder → SSE 解析 → 逐 token 拼接渲染
前端工程师掌握流式输出,几乎是做 LLM 应用的必修课,也是面试中的高频考点。搞懂了它,你就抓住了 AI 产品用户体验的第一把钥匙。
本文 Demo 源码基于 Vite + Vue 3 + DeepSeek API,完整实现了从 API 调用、流式读取、到界面响应式渲染的全链路。希望这篇文章能帮助你理解流式输出的原理和实现方式,也欢迎在评论区交流你的 AI 产品开发经验!