为什么大厂疯狂考这道题?💼
你问为什么2025年大厂面试必考流式输出?
因为用户等不起啊!
想象你在和AI聊天机器人对话:
- 🚫 传统模式:等它"思考完1000个token"再给你答案,像等外卖等半小时
- ✅ 流式模式:后端边生成答案边推送,像快递员分批送包裹📦,你立马看到"我是你的assistant"开头
小白提示:每个token都是钱💰(LLM按token计费),流式输出还能帮你省钱!
行业趋势背后的逻辑
- AI Agent革命 :2024年推理优化→2025年Agent交互升级
- 聊天机器人不再是"黑盒",而是实时响应的智能助手
- 流式输出是Agent感知延迟的核心指标(<100ms体验阈值)
- 用户体验军备竞赛 :
- 短视频平台用流式加载实现"无限滚动"
- 在线教育系统用实时字幕提升学习专注度
前端的障眼法:SSE魔法揭秘 🎩✨

你以为后端瞬间生成答案?其实它在偷偷玩"分段传送"!
代码实战:前端如何接收魔法流?
html
<!-- index.html -->
<script>
// 打开魔法通道🚪
const source = new EventSource("/sse");
source.onmessage = function(event){ // 收到消息就像拆礼物🎁
document.getElementById("messages").innerHTML += event.data + "<br>";
}
</script>
技术解密 :
EventSource
是HTML5的SSE接口,像装了个"接收站"📡,后端每发一个token,它就立刻显示在页面上!
前端的隐藏技能树
-
事件类型扩展
javascriptsource.addEventListener("custom-event", function(e) { console.log("收到特殊消息:", e.data); });
-
可定义多种事件类型(如
error
、open
) -
消息格式示例:
textevent: custom-event data: {"type":"system","content":"重新连接"}
-
-
自动重连魔法
javascriptsource.addEventListener("error", function(e) { if (e.eventPhase === EventSource.CLOSED) { setTimeout(() => source.close(), 5000); // 5秒后重试 } });
- 默认3秒自动重连(可配置
retry:
字段) - 适用于网络波动场景(地铁/电梯等信号差环境)
- 默认3秒自动重连(可配置
后端的魔法盒子:Node.js实现Server Push 🧙♂️

你以为后端只能被动等待请求?错!它还能主动推送消息!
代码实战:后端如何变身快递员?
javascript
// index.js
app.get("/sse", (req, res) => {
// 设置魔法通行证🔐
res.set({
"Content-Type": "text/event-stream", // 标识魔法流
"Cache-Control": "no-cache", // 禁用缓存
"Connection": "keep-alive" // 保持长连接
});
res.flushHeaders(); // 启动魔法开关⚡
// 每隔1秒发送消息,像快递员定时送货📦
setInterval(() => {
const message = `Current time: ${new Date().toLocaleTimeString()}`;
res.write(`data: ${message}\n\n`); // 格式固定:data: 内容 + 换行符
}, 1000);
});
小白提示:这段代码就像给后端装了个"广播喇叭"📢,每隔1秒就喊:"又有新消息啦!"
后端的进阶魔法
-
消息ID追踪
javascriptlet id = 0; setInterval(() => { res.write(`id: ${++id}\n`); res.write(`data: ${JSON.stringify({time: new Date()})}\n\n`); }, 1000);
- 客户端可通过
event.lastEventId
获取断连前的ID - 用于消息去重和状态同步
- 客户端可通过
-
HTTP/2 Server Push黑科技
javascript// 需要启用HTTP/2服务器 const http2 = require('http2').createServer(); http2.on('stream', (stream, headers) => { if (headers[':path'] === '/sse') { stream.respondWithPushStream('/style.css', (err, pushStream) => { pushStream.end('body { color: red; }'); }); } });
- 同时推送CSS/JS资源(无需额外HTTP请求)
- 减少首屏加载时间(关键路径优化)
技术要点:流式输出的四大魔法咒语 📜

-
SSE协议
- 单向通信(服务器→客户端)
- 自动重连机制(网络断了?自动续上!🔁)
- 局限性 :
- 不支持跨域(需CORS配置)
- 无法实现双向通信(需配合WebSocket)
-
响应头设置
jsres.set({ "Content-Type": "text/event-stream", // 标识魔法流 "Cache-Control": "no-cache", // 禁用缓存 "Connection": "keep-alive" // 保持连接 });
-
数据格式
- 每条消息必须以
data:
开头 - 以
\n\n
结尾表示消息结束 - 示例:
data: 我是你的assistant\n\n
- 每条消息必须以
-
启动方式
bashnode index.js # 启动后访问 http://localhost:1314
实战场景:流式输出的四大妙用 💡

场景 | 魔法效果 |
---|---|
LLM聊天机器人 | 边打字边显示回答,用户不焦虑 😌 |
实时股票行情 | 每秒更新价格,像装了雷达 📈 |
进度条更新 | 显示"加载中..."而不是空白屏 🔄 |
通知系统 | 新消息立刻弹出,不等用户刷新 🚨 |
进阶场景:AI Agent交互设计
-
多轮对话流式 :
javascript// 模拟大模型分段生成 const generateResponse = async (query) => { for (const token of await model.generate(query)) { res.write(`data: ${token}\n\n`); await sleep(50); // 模拟token生成延迟 } };
-
用户中断机制 :
javascriptsource.addEventListener("abort", () => { clearInterval(timer); // 终止后端生成任务 res.end(); // 断开连接 });
技术对比:SSE vs WebSocket vs HTTP/2 Server Push 🤝
特性 | SSE | WebSocket | HTTP/2 Server Push |
---|---|---|---|
通信方向 | 单向(服务器→客户端) | 双向 | 单向(服务器→客户端) |
连接保持 | Long Polling模拟 | 持久TCP连接 | 多路复用 |
协议支持 | HTTP/1.1+ | 自定义协议 | HTTP/2+ |
适用场景 | 实时通知/日志流 | 在线游戏/IM聊天 | 静态资源预加载 |
选择建议:
- 需要双向通信 → WebSocket
- 需要兼容IE → 长轮询
- 需要极致性能 → HTTP/2 Server Push
从0到1跑通示例:三步搞定!🚀
-
初始化项目
bashnpm init -y && npm i express
-
创建文件
index.js
(后端)index.html
(前端)
-
启动服务
bashnode index.js # 访问 http://localhost:1314
总结:流式输出的核心知识点 ✅
- ✅ 必要性:提升用户体验 + 节省token成本
- ✅ 前端魔法 :
EventSource
+ 监听onmessage
- ✅ 后端咒语 :SSE响应头 +
res.write()
推送 - ✅ 应用场景:聊天机器人/实时通知/进度条
现在就去跑一遍代码吧!🚀
你会发现:流式输出就像给你的网页装上了"实时心跳"💓,用户再也不用干等了!
附录:常见问题解答 📚
Q1: 如何防止SSE连接被浏览器缓存?
A: 设置Cache-Control: no-cache
响应头,并在URL加随机参数(如/sse?rand=${Math.random()}
)
Q2: 为什么我的SSE连接经常断开?
A: 检查:
- 后端是否忘记调用
res.flushHeaders()
- 是否在
setInterval
中错误关闭了response - 服务器防火墙是否限制了长连接
Q3: 如何实现跨域SSE?
A: 配置CORS响应头:
javascript
res.set({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true"
});