SSE 实现 AI 对话中的流式输出
日常使用 deepseek,经常看到聊天机器人的流式输出,感觉很赞,想尝试下自己实现一个类似效果
各个大模型平台,api 调用都支持流式输出,如果用 node.js 如何实现一个流式输出效果呢
整体内容包含
服务端实现
和客户端实现
整体效果

SSE 技术原理
Server-Sent Events (SSE) 是一种基于 HTTP 的服务器向客户端推送数据的技术。与 WebSocket 不同,SSE 是单向的,仅支持服务器向客户端推送数据,但实现简单,且天然支持断线重连。
- 单向通信,由服务器发送数据,客户端接收数据
- 自动重连
- 轻量级,相比 websocket,开销更小
- 主要传输文本数据,适合 JSON 等结构化数据
- sse 的消息结构
- id: 事件 id
- event: 事件名称 如果不传默认为 message,如果设置其他名称,前端也需要修改成对应的名称,进行消息接受
- data: 事件数据
- retry: 重连时间
服务端实现
- node.js 的 koa2 框架
实现步骤
- 设置 SSE 响应头 响应内容类型,客户端不缓存,保持长连接
- 防止 koa 自动处理响应,手动设置响应状态码为 200
- 定时发送消息模拟推流,通过
ctx.res.write(data: msg)
,也可以传入id, event
补充消息内容。 - 通过
ctx.req.on(eventName, callback)
进行连接监听,处理连接关闭,连接失败
js
aiRouer.get("/stream", (ctx) => {
console.log("进入stream");
// 设置SSE响应头
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
});
// 关键修复:防止Koa自动处理响应
ctx.respond = false;
ctx.status = 200;
// 发送初始连接成功消息
ctx.res.write(`data: ${JSON.stringify({ message: "Connected to Koa2 SSE server" })}\n\n`);
// 设置定时器,定期发送消息
let counter = 0;
const timer = setInterval(() => {
counter++;
const data = {
message: `Server time: ${new Date().toLocaleTimeString()}`,
counter: counter,
};
// 检查连接是否仍然有效
if (ctx.res.writable) {
// 按照SSE格式发送数据
ctx.res.write(`id: ${counter}\n`);
ctx.res.write(`event: message\n`);
ctx.res.write(`data: ${JSON.stringify(data)}\n\n`);
if (counter > 10) {
ctx.res.end();
}
} else {
// 如果连接已关闭,清理资源
clearInterval(timer);
console.log("SSE connection closed due to client disconnect");
}
}, 1000);
// 处理连接关闭
ctx.req.on("close", () => {
clearInterval(timer);
console.log("SSE connection closed");
});
// 处理错误
ctx.req.on("error", (err) => {
clearInterval(timer);
console.error("SSE connection error:", err);
});
});
客户端实现
- vue.js
实现步骤
- 触发 SSE 推送,创建一个 EventSource 对象,并监听服务端的推送消息。
- 通过
inputStr
接受消息,设置连接关闭条件,并结束定时器。 - 定时取数据,输出到
outStr
html
<template>
<div class="steam">
<h3>服务端回答的消息</h3>
<div ref="outRef" class="out-textarea" v-if="outStr">{{ outStr }}</div>
<div>
<n-button type="info" @click="onSend">发送</n-button>
</div>
</div>
</template>
js
import { NInput, NButton } from "naive-ui";
import { reactive, toRefs } from "vue";
const state = reactive({
outRef: null,
outStr: "",
inputStr: "",
inputFinish: false,
});
const { outRef, outStr, inputStr, inputFinish } = toRefs(state);
/**
* 流式输出
*/
function outStream() {
let i = 0;
let timer = setInterval(() => {
// 一直等待输出,直到服务端停止输出
if (inputFinish.value && i >= inputStr.value.length) {
clearInterval(timer);
timer = null;
return;
}
if (i < inputStr.value.length) {
state.outStr += inputStr.value[i];
i++;
}
}, 100);
}
function onSend() {
let eventSource = new EventSource("/api/ai/stream");
inputStr.value = "";
outStr.value = "";
inputFinish.value = false;
eventSource.onmessage = function (e) {
const data = JSON.parse(e.data);
inputStr.value += data.message;
if (data.counter > 10) {
eventSource.close();
eventSource = null;
inputFinish.value = true;
}
};
outStream();
}
css
.out-textarea {
display: inline-block;
background-color: rgba(0, 0, 0, 0.06);
padding: 12px 16px;
border-radius: 8px;
position: relative;
box-sizing: border-box;
min-width: 0;
max-width: 100%;
color: rgba(0, 0, 0, 0.88);
font-size: 14px;
line-height: 1.5714285714285714;
min-height: 46px;
word-break: break-word;
margin-top: 24px;
margin-bottom: 24px;
scrollbar-color: rgba(0, 0, 0, 0.45) transparent;
}
ai 组件库
- 目前大厂都有成熟的 ai 组件库,其中就包括对话流式输出组件
- antd-design-x-vue 对话气泡框
- RICH 设计范式思考
- Role 【角色】以后产品和人交互,更像是一个人。可以通过角色外观,声音,情绪,专业领域知识
- Intention 【意图】 以前收集用户需求,通过输入框,按钮,鼠标,触摸动作 以后 ai 会做的更多,比如通过对话、语音、结合少量原先图形化交互、更加准确和简单
- Conversation 【对话】人与 ai 的会话规则 开始/追问/提示/确认/错误/结束
- Hybrid UI 【混合界面】 Do 为主/Do + Chat 均衡/Chat 为主