从前有个急性子的用户,每次点击按钮后就开始数秒:"1...2...3...再不出来我就刷新!" 直到他遇到了流式输出——这感觉就像看魔术师从帽子里源源不断拉出彩带,而不是等三个小时才看到一只鸽子扑腾出来。
为什么服务器也爱"挤牙膏"?
在传统Web交互中(如图1),服务器就像个固执的厨师:
而流式输出(Streaming Output)则像日料店的板前料理(图2):
主打的就是立即且高效,就像水流一样,持续不断的输出。大家有没有发现现在的大模型采用都是流式输出,当然你要我等个一分钟才有内容反馈给我,谁看啊
这种"挤牙膏式"的数据传输有什么魔力?让我们翻开武林秘籍
流式输出的三大内功心法
-
降龙十八掌·体验优化版
"边生成边输出"是核心奥义。当LLM大模型生成"你好"需要2秒,生成"你好啊"需要4秒时,流式输出能让你在第2秒就先看到"你好",而不是干等4秒——这就像吃火锅时服务员先上毛肚再上牛肉,而不是等所有菜配齐才开火。 -
凌波微步·付费轻功
大模型按token收费(每个词都是钱啊!)。流式输出相当于"分期付款":用户先消费已生成的部分内容,模型后台继续"打工还债"。这招在付费API场景下堪称省钱绝学。 -
读心术·用户心理学
前端工程师最懂人类大脑的bug:- 等待2秒 + 立即展示完整内容 = 😠
- 等待4秒但每秒都有新文字 = 😄
这就是著名的"进度条安慰剂效应"——哪怕总时间更长,用户反而觉得更快!
2025大厂必考题的玄机
为什么这道题能登上大厂考题C位?
- LLM 聊天机器人(23年的AI爆款 -> 24年 推理 -> 25 年 AI Agent 年)
- 流式输出,属于优化用户体验,前端职责
这揭示了一个技术趋势演变:
- 2023:能说话的木偶(基础聊天)
- 2024:会思考的鹦鹉(逻辑推理)
- 2025:有手有脚的管家(AI Agent)
当AI管家要给你念《三体》全集时,流式输出就是避免你睡着的关键技术——毕竟谁也不想听完"地球往事"四个字就等三小时!
前端 VS 后端的"牙膏战争"
前端的障眼法
<h1>流式输出</h1>
<div id="message"></div>
<script>
// 创建SSE连接(获得魔法水管)
const source = new EventSource('/sse')
// 水管来水时的处理姿势
source.onmessage = function (event) {
document.querySelector('#message').innerHTML += event.data
}
</script>
前端在这里扮演"水管工"角色:
- 接上
/sse
这根魔法水管 - 每滴水(data)到来就拼接到页面上
- 全程保持"哇塞又来一滴"的惊喜表情
后端的挤牙膏术
app.get('/sse', (req, res) => {
// 设置SSE专属响应头(启动牙膏管)
res.set({
'Content-Type': 'text/event-stream', // 声明是事件流
'Cache-Control': 'no-cache', // 禁止偷藏牙膏
'Connection': 'keep-alive' // 保持牙膏管畅通
})
// 开始定期挤牙膏
setInterval(() => {
const message = `Current time is ${new Date().toLocaleTimeString()}`
res.write(`data: ${message}\n\n`) // 关键格式!
}, 1000)
})
后端在此化身为"牙膏厂工人":
- 用
text/event-stream
头声明:"注意!我要开始挤牙膏了!" no-cache
确保每次都是新鲜出炉的牙膏,不缓存keep-alive
保持牙膏管持续畅通(避免被HTTP协议自动切断)- 每秒钟用
res.write
挤出一段数据(注意必须data: 然后用\n\n
结尾!)
SSE:单向传情的HTTP恋人
Server-Sent Events(SSE)的精妙之处在于:
- 单相思模式:只允许服务器→客户端的单向通信(像极了暗恋)
- HTTP协议:无需WebSocket那样的复杂婚约,普通HTTP就能谈恋爱
- 自动重连:网络中断时会自动恢复连接(堪称技术界最持久的舔狗)
对比其他技术:
技术 | 方向 | 协议 | 复杂度 | 适用场景 |
---|---|---|---|---|
SSE | 单向(服务端→客户端) | HTTP | ⭐ | 实时通知、数据流 |
WebSocket | 双向 | 独立 | ⭐⭐⭐ | 聊天室、游戏 |
长轮询 | 伪双向 | HTTP | ⭐⭐ | 兼容旧浏览器 |
手把手打造"牙膏工厂"
让我们启动这个神奇项目:
让我们启动这个神奇项目:
第一步:创建工厂地基
npm init -y # 获得建厂许可证
npm i express # 购买名为Express的施工队(引入express框架)
第二步:建造流水线(index.js)
const express = require('express')
const app = express()
// 首页配送车间
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html') // 发货HTML文件
})
// SSE秘密生产线
app.get('/sse', (req, res) => {
res.set({ /* 设置响应头 */ })
setInterval(() => {
res.write(`data: ${new Date().toLocaleTimeString()}\n\n`)
}, 1000)
})
// 启动工厂电源,启动http服务
require('http').Server(app).listen(1314, () => {
console.log('牙膏厂开始营业!传送带端口:1314')
})
第三步:设计展示柜台(index.html)
<body>
<h1>流式输出体验馆</h1>
<div id="message"></div>
<script>
const source = new EventSource('/sse') // 连接生产线
source.onmessage = (event) => {
// 把新鲜到货的牙膏展示在柜台
document.getElementById('message').innerHTML += event.data + '<br>'
}
</script>
</body>
第四步:见证奇迹
- 运行
node index.js
启动工厂 - 访问
http://localhost:1314
- 每秒都会看到新的时间戳冒出来: 我们来看看效果:
是不是就像流水一样,只要我们生成内容,就好一点点渲染到页面,而不是等他生成所有的内容再渲染,更符合用户的体验
踩坑预警:SSE的特殊仪式
- 必须双换行符:每条消息必须以
\n\n
结尾(这是SSE的摩尔斯电码) - 数据前缀:消息格式必须是
data: 内容\n\n
(少了前缀消息会迷路) - 连接保活:浏览器默认在连接断开后3秒重连(贴心但可能造成重复连接)
解决方案示例:
// 正确挤牙膏姿势
res.write(`data: 第一段内容\n\n`)
// 错误示范(会导致牙膏堵住)
res.write('忘了加data前缀!')
res.write('少了一个换行符\n')
我刚开始接触,以为消息格式data: 内容\n\n
并不重要,导致debug老久了,这是SSE必须的格式,不能变
流式输出的江湖地位
当你在这些场景下遇到它,请抱拳说声"久仰":
- LLM聊天:ChatGPT一个字一个字"蹦"出来的答案
- 日志监控:实时滚动的服务器日志面板
- 股票行情:分秒变动的股价瀑布流
- 文件下载:看到进度条像蜗牛一样前进时(那也是种流!)
尾声:全栈工程师的牙膏哲学
这个小小项目揭示了大厂全栈能力的要求:
- 后端要掌握"挤牙膏"的力度(数据分块)
- 前端要设计"接牙膏"的姿势(渐进渲染)
- 协议要确保"牙膏管"的畅通(SSE规范)
所以下次面试被问"如何优化长时间任务体验"时,请自信地回答:"咱们来挤牙膏吧!" 这可比说"使用流式输出技术实现服务端推送"有趣多了——毕竟技术界的幽默感,就是把复杂的事情说得像挤牙膏一样简单。