前端SSE手把手实现流式对话Agent(附完整可复制代码)
到这里,你已经学会了用 SSE 实现 AI 流式对话 Agent,总结一下核心知识点:SSE 是前端原生的服务器单向推送技术,适合 AI 流式回复、通知等场景,比 WebSocket 更轻量、更容易上手。前端用建立 SSE 连接,通过onmessage接收服务器推送的数据,实现打字机效果。后端需要设置 SSE 响应头,按照data: 数据\n\n的格式逐字推送数据。整个项目只有 2 个文件,代码全
大家好~ 今天给大家带来一篇「零基础也能上手」的 SSE 教学,核心目标是让你学会用 SSE 实现 AI 流式对话 Agent(就是 ChatGPT 那种“打字机”实时输出效果)。
整篇文章全程无晦涩概念,所有代码都附带详细注释,复制就能运行,不管你是刚入门前端的新手,还是想快速落地流式对话功能的开发者,都能看懂、会用。话不多说,直接开干!
一、先搞懂:SSE 到底是什么?
在学代码之前,我们先搞清楚核心技术 SSE 是什么,不用记复杂定义,记住一句话就够了:
SSE(Server-Sent Events,服务器发送事件)是 HTML5 原生支持的技术,作用是「客户端连接服务器后,服务器能主动、持续地向客户端推送数据」,不用客户端反复发请求。
1. 为什么 AI 流式对话首选 SSE?
我们平时用的 AI 对话(比如 ChatGPT),不是等所有内容生成完再一次性返回,而是逐字、逐句实时输出,这种效果用 SSE 实现最适合,原因有 4 个:
-
轻量:基于 HTTP 协议,不用像 WebSocket 那样升级协议,开发成本极低
-
原生:前端自带
EventSourceAPI,不用装任何第三方库 -
适配:单向通信(服务器 → 客户端),刚好匹配 AI 流式输出的场景(只需要服务器推数据给前端)
-
省心:自带自动重连、心跳机制,不用自己手写复杂的重连逻辑
2. SSE vs 轮询 vs WebSocket
很多人会把这三个技术搞混,这里做一个直观对比,帮你快速分清适用场景:
|
技术 |
核心原理 |
适用场景 |
学习难度 |
|---|---|---|---|
|
SSE |
服务器单向推送,基于 HTTP |
AI 流式回复、系统通知、实时日志 |
⭐(最低) |
|
轮询 |
客户端定时向服务器发请求,问“有没有新数据” |
简单数据更新(如定时刷新公告) |
⭐⭐ |
|
WebSocket |
客户端与服务器双向通信,需升级协议 |
实时聊天、游戏、直播弹幕 |
⭐⭐⭐(最高) |
总结:做 AI 流式对话,SSE 是性价比最高、最容易上手的选择,不用多学复杂的双向通信逻辑。
二、前置准备
不用准备复杂的环境,只要 3 样东西,5 分钟就能搞定:
-
代码编辑器:推荐 VS Code(免费、好用,新手友好)
-
基础储备:懂一点点 HTML、CSS、JS(不用精通,能看懂简单代码就行)
-
Node.js 环境:用于运行我们的简易后端(模拟 AI 流式输出),官网下载安装即可(安装时一路下一步,不用额外配置)
⚠️ 注意:本文全程用「原生 JS」编写,不依赖 Vue、React 等任何前端框架,新手可以直接跟着敲代码。
三、项目结构(极简,2 个文件搞定)
我们整个项目只有 2 个文件,结构非常简单,新建一个文件夹(比如叫 sse-ai-agent),里面放这两个文件即可:
sse-ai-agent/
├─ index.html # 前端页面(核心:聊天界面 + SSE 逻辑)
└─ server.js # 简易后端(模拟 AI 流式输出,新手可直接复制)
解释:前端负责显示聊天界面、发起 SSE 连接、接收服务器推送的流式数据;后端负责模拟 AI 生成内容,逐字推送给前端。
四、核心步骤 1:编写前端代码(完整可运行,带详细注释)
这是前端的核心文件,包含「聊天界面 + 输入框 + 发送/中断按钮 + SSE 连接 + 打字机效果 + 错误处理」,所有代码都加了注释,新手能看懂每一行的作用。新建 index.html 文件,复制下面的代码,直接保存即可:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE 流式 AI 对话 Agent(新手版)</title>
<style>
/* 全局样式:重置默认样式,让界面更美观 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
max-width: 800px; /* 限制页面宽度,避免在宽屏上太分散 */
margin: 0 auto; /* 水平居中 */
padding: 20px;
background: #f5f7fa; /* 浅灰色背景,更舒适 */
}
/* 聊天容器:显示所有消息,可滚动 */
.chat-container {
height: 600px; /* 固定高度,方便查看历史消息 */
background: white;
border-radius: 12px; /* 圆角,更美观 */
padding: 20px;
overflow-y: auto; /* 消息过多时可滚动 */
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05); /* 轻微阴影,增加层次感 */
}
/* 消息气泡通用样式 */
.message {
margin-bottom: 16px; /* 消息之间的间距 */
padding: 12px 16px;
border-radius: 8px;
max-width: 75%; /* 消息最大宽度,避免占满整个屏幕 */
line-height: 1.5; /* 行高,提升可读性 */
}
/* 用户消息:右侧显示,绿色背景 */
.user-message {
background: #009688;
color: white;
margin-left: auto; /* 右对齐 */
}
/* AI 消息:左侧显示,灰色背景 */
.ai-message {
background: #e9ecef;
color: #333;
margin-right: auto; /* 左对齐 */
white-space: pre-wrap; /* 保留空白和换行,让 AI 回复格式更规范 */
}
/* 输入框区域:固定在底部,方便输入 */
.input-area {
display: flex; /* 弹性布局,让输入框和按钮在同一行 */
gap: 10px; /* 输入框和按钮之间的间距 */
}
#userInput {
flex: 1; /* 输入框占满剩余空间 */
padding: 14px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 16px;
outline: none; /* 取消默认聚焦边框 */
}
/* 输入框聚焦时,边框变色,提示用户当前处于输入状态 */
#userInput:focus {
border-color: #009688;
}
button {
padding: 14px 24px;
background: #009688;
color: white;
border: none;
border-radius: 8px;
cursor: pointer; /* 鼠标悬浮时显示手型,提示可点击 */
font-size: 16px;
}
/* 按钮悬浮时,轻微变浅,提升交互体验 */
button:hover {
opacity: 0.9;
}
/* 禁用状态的按钮:灰色,不可点击 */
button:disabled {
background: #ccc;
cursor: not-allowed; /* 禁用时显示“禁止”鼠标样式 */
}
/* 加载提示:AI 思考时显示,提示用户正在处理 */
.loading {
color: #666;
font-style: italic; /* 斜体,区分普通文本 */
padding: 8px 0;
}
</style>
</head>
<body>
<!-- 聊天容器:显示用户和 AI 的消息 -->
<div class="chat-container" id="chatContainer"></div>
<!-- 输入区域:输入框 + 发送/中断按钮 -->
<div class="input-area">
<input type="text" id="userInput" placeholder="请输入你的问题..." autocomplete="off">
<button id="sendBtn">发送</button>
<button id="abortBtn" disabled>中断</button>
</div>
<script>
// ==================== 1. 获取页面元素(DOM 操作)====================
// 聊天容器:用于渲染消息
const chatContainer = document.getElementById('chatContainer');
// 用户输入框:获取用户输入的问题
const userInput = document.getElementById('userInput');
// 发送按钮:点击发送问题
const sendBtn = document.getElementById('sendBtn');
// 中断按钮:点击中断 SSE 连接(停止 AI 输出)
const abortBtn = document.getElementById('abortBtn');
// 存储 SSE 连接对象:全局变量,方便后续中断连接
let eventSource = null;
// ==================== 2. 渲染消息到页面(通用函数)====================
// 功能:将用户/AI 的消息添加到聊天容器中
// 参数1:message 消息内容
// 参数2:type 消息类型(user:用户消息;ai:AI 消息)
function addMessage(message, type) {
// 1. 创建消息元素(div 标签)
const messageDom = document.createElement('div');
// 2. 给消息元素添加类名(用于区分用户/AI 消息,设置不同样式)
messageDom.className = `message ${type}-message`;
// 3. 设置消息内容
messageDom.textContent = message;
// 4. 将消息元素添加到聊天容器中
chatContainer.appendChild(messageDom);
// 5. 自动滚动到底部,让用户能看到最新消息
chatContainer.scrollTop = chatContainer.scrollHeight;
// 6. 返回创建的消息元素(后续 AI 流式输出时,需要不断追加内容)
return messageDom;
}
// ==================== 3. 发送消息 + 建立 SSE 连接(核心逻辑)====================
function sendMessage() {
// 1. 获取用户输入的问题,去除首尾空格(避免用户输入空内容)
const userMessage = userInput.value.trim();
if (!userMessage) {
alert('请输入你的问题哦~'); // 提示用户输入内容
return;
}
// 2. 渲染用户消息到页面(让用户看到自己发送的内容)
addMessage(userMessage, 'user');
// 3. 清空输入框,方便用户输入下一个问题
userInput.value = '';
// 4. 禁用发送按钮、启用中断按钮(防止重复发送,同时允许中断)
sendBtn.disabled = true;
abortBtn.disabled = false;
// 5. 创建 AI 消息占位(先显示“加载中”,后续用流式数据填充)
const aiMessageDom = addMessage('', 'ai');
aiMessageDom.innerHTML = '<span class="loading">AI 思考中...</span>';
// ==================== 关键:建立 SSE 连接 ====================
// 后端接口地址:携带用户的问题(encodeURIComponent 处理中文,避免乱码)
const apiUrl = `http://localhost:3000/chat?msg=${encodeURIComponent(userMessage)}`;
// 创建 SSE 实例(前端原生 API,不用装任何库)
// withCredentials: false 表示不携带 cookie(跨域时常用,新手不用改)
eventSource = new EventSource(apiUrl, { withCredentials: false });
// ------------------- SSE 事件监听(3 个核心事件)-------------------
// 1. onmessage:收到服务器推送的消息时触发(最核心的事件)
eventSource.onmessage = function (event) {
// event.data:服务器推送过来的具体数据(字符串格式)
const pushData = event.data;
// 约定:如果服务器推送 [DONE],表示 AI 内容生成完成
if (pushData === '[DONE]') {
eventSource.close(); // 关闭 SSE 连接(节省资源)
sendBtn.disabled = false; // 恢复发送按钮(允许用户发送下一个问题)
abortBtn.disabled = true; // 禁用中断按钮(已经完成,无需中断)
aiMessageDom.querySelector('.loading')?.remove(); // 移除“加载中”提示
return;
}
// 移除“加载中”提示,开始逐字追加 AI 内容(实现打字机效果)
aiMessageDom.querySelector('.loading')?.remove();
aiMessageDom.textContent += pushData; // 追加内容(关键:流式效果的核心)
// 自动滚动到底部,让用户看到最新的 AI 回复
chatContainer.scrollTop = chatContainer.scrollHeight;
};
// 2. onopen:SSE 连接建立成功时触发
eventSource.onopen = function () {
console.log('SSE 连接已建立,等待 AI 回复...'); // 控制台打印,方便调试
};
// 3. onerror:SSE 连接出错时触发(比如后端崩溃、网络中断)
eventSource.onerror = function (error) {
console.error('SSE 连接出错:', error); // 控制台打印错误信息,方便排查
// 给用户提示错误信息
aiMessageDom.textContent += '\n【连接异常,请重试】';
eventSource.close(); // 关闭出错的连接
sendBtn.disabled = false; // 恢复发送按钮
abortBtn.disabled = true; // 禁用中断按钮
};
}
// ==================== 4. 中断 SSE 连接(停止 AI 输出)====================
function abortConnection() {
if (eventSource) {
eventSource.close(); // 关闭 SSE 连接
eventSource = null; // 重置连接对象
sendBtn.disabled = false; // 恢复发送按钮
abortBtn.disabled = true; // 禁用中断按钮
// 给 AI 消息追加“已中断”提示
const lastAiMessage = chatContainer.querySelector('.ai-message:last-child');
if (lastAiMessage) {
lastAiMessage.textContent += '\n【已中断回复】';
}
}
}
// ==================== 5. 绑定按钮事件(交互逻辑)====================
// 发送按钮:点击触发 sendMessage 函数
sendBtn.addEventListener('click', sendMessage);
// 输入框:按 Enter 键也能发送消息(提升用户体验)
userInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
sendMessage();
}
});
// 中断按钮:点击触发 abortConnection 函数
abortBtn.addEventListener('click', abortConnection);
</script>
</body>
</html>
前端代码关键说明
不用死记硬背代码,重点理解这 3 个核心点:
-
EventSource:前端原生的 SSE 实例,只要传入后端接口地址,就能建立连接,不用额外配置。 -
onmessage事件:服务器每推送一次数据,这个事件就会触发一次,我们在这里实现“逐字追加”,就能做出打字机效果。 -
中断功能:通过
eventSource.close()关闭 SSE 连接,就能停止接收服务器推送的数据,实现“中断回复”。
五、核心步骤 2:编写简易后端代码(模拟 AI 流式输出)
前端需要一个后端接口来建立 SSE 连接,这里我们用 Node.js 编写一个极简后端,功能是:接收用户的问题,模拟 AI 逐字生成回复,然后通过 SSE 推送给前端。
新建 server.js 文件,复制下面的代码,保存即可:
// 导入 Node.js 内置的 http 模块(用于创建服务器)
const http = require('http');
// 导入 url 模块(用于解析前端传递的参数)
const url = require('url');
// 1. 创建 HTTP 服务器
const server = http.createServer((req, res) => {
// 解析前端请求的 URL,获取查询参数(用户输入的问题)
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname; // 请求路径
const userMsg = parsedUrl.query.msg; // 用户输入的问题(从查询参数中获取)
// 2. 只处理 /chat 路径的请求(前端 SSE 连接的接口)
if (pathname === '/chat') {
// 关键:设置响应头,告诉前端这是 SSE 类型的响应
res.writeHead(200, {
'Content-Type': 'text/event-stream', // 必须设置为 text/event-stream,标识这是 SSE 响应
'Cache-Control': 'no-cache', // 禁止缓存,确保实时接收数据
'Connection': 'keep-alive', // 保持连接,让服务器能持续推送数据
'Access-Control-Allow-Origin': '*' // 允许跨域(前端和后端端口不同,必须设置,否则会报错)
});
// 模拟 AI 回复内容(新手可以修改这里的内容,自定义 AI 回复)
const aiReply = `你好!你刚才问的是:"${userMsg}"。我是用 SSE 实现的流式 AI 助手,正在逐字向你推送回复哦~ 这个效果就是 SSE 的核心能力,服务器可以持续、主动地向客户端推送数据,不用客户端反复请求。是不是很神奇?赶紧试试输入其他问题吧!`;
// 3. 模拟 AI 逐字输出(核心:每隔 50 毫秒推送一个字符,实现打字机效果)
let index = 0; // 字符索引,用于逐字截取回复内容
const interval = setInterval(() => {
// 每次截取一个字符(从 index 位置开始,截取 1 个字符)
const char = aiReply[index];
if (char) {
// 向前端推送数据(SSE 格式:data: 数据\n\n,必须严格遵循这个格式)
res.write(`data: ${char}\n\n`);
index++; // 索引加 1,下次截取下一个字符
} else {
// 所有字符推送完成,推送 [DONE] 标识,告诉前端结束
res.write(`data: [DONE]\n\n`);
clearInterval(interval); // 清除定时器,停止推送
res.end(); // 结束响应
}
}, 50); // 每隔 50 毫秒推送一个字符,速度可以调整(数值越小,打字越快)
// 处理客户端断开连接的情况(比如用户关闭页面、点击中断)
req.on('close', () => {
clearInterval(interval); // 清除定时器
res.end(); // 结束响应
console.log('客户端断开 SSE 连接');
});
} else {
// 处理其他路径的请求(返回 404)
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
}
});
// 3. 启动服务器,监听 3000 端口(端口可以修改,但要和前端接口地址对应)
const port = 3000;
server.listen(port, () => {
console.log(`SSE 服务器已启动,监听端口:${port}`);
console.log(`前端访问地址:http://localhost:${port}/index.html`);
});
后端代码关键说明
后端的核心是「设置 SSE 响应头 + 逐字推送数据」,重点记住 2 点:
-
响应头必须设置
Content-Type: text/event-stream,否则前端无法识别这是 SSE 连接。 -
推送数据的格式必须是
data: 数据\n\n(data: 后面跟推送的内容,然后两个换行),这是 SSE 的标准格式,少一个换行都会报错。
⚠️ 注意:后端的端口是 3000,前端接口地址写的是 http://localhost:3000/chat,两者必须一致,否则会出现跨域错误。
六、运行项目
代码写好后,我们来运行项目,看看实际效果,步骤非常简单:
步骤 1:启动后端服务器
-
打开 VS Code,打开我们创建的
sse-ai-agent文件夹。 -
打开 VS Code 的终端(顶部菜单:终端 → 新建终端)。
-
在终端中输入命令:
node server.js(注意:确保已经安装了 Node.js,否则会报错)。 -
如果终端显示
SSE 服务器已启动,监听端口:3000,说明后端启动成功。
步骤 2:打开前端页面
-
打开浏览器(推荐 Chrome、Edge)。
-
在地址栏输入:
http://localhost:3000/index.html,按下回车。 -
此时会看到聊天界面,输入问题(比如“什么是 SSE?”),点击发送,就能看到 AI 逐字输出的打字机效果啦!
常见问题排查(新手必看)
如果运行失败,大概率是以下 3 个问题,对照排查即可:
-
问题 1:终端输入
node server.js报错 → 检查 Node.js 是否安装成功(打开终端输入node -v,能显示版本号就是安装成功)。 -
问题 2:前端页面无法连接,控制台报错“跨域” → 检查后端代码中是否有
Access-Control-Allow-Origin: *(必须加上,否则跨域会被拦截)。 -
问题 3:AI 不输出内容 → 检查后端代码中
aiReply是否有内容,以及定时器的时间是否合理(50 毫秒是比较合适的速度)。
七、进阶优化(可选,新手可后续学习)
如果想让这个流式对话 Agent 更完善,可以尝试以下优化(本文不展开,但给出方向,新手可以逐步探索):
-
对接真实 AI 接口(如 OpenAI、文心一言):将后端的“模拟 AI 回复”替换成真实的 AI 接口请求,实现真正的 AI 流式对话。以下是对接 OpenAI 接口的简化代码(可直接复制替换,附详细注释):
// 1. 先安装依赖(终端输入命令):npm install openai // 2. 替换原有 server.js 中“模拟 AI 回复”相关代码,保留其他逻辑不变 const OpenAI = require('openai'); // 初始化 OpenAI 实例(替换成你的 API Key,从 OpenAI 官网获取) const openai = new OpenAI({ apiKey: '你的 OpenAI API Key', // 关键:替换成自己的 API Key }); // 替换原有“模拟 AI 回复”部分,改为调用 OpenAI 流式接口 // 注意:保留前面的响应头设置、路径判断等代码,只替换 aiReply 和定时器部分 async function getOpenAIStream(userMsg, res) { try { // 调用 OpenAI 流式接口(gpt-3.5-turbo 模型,适合新手,成本低) const stream = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ { role: "user", content: userMsg } // 传入用户的问题 ], stream: true, // 开启流式输出(核心,实现打字机效果) }); // 监听流数据,逐段推送给前端 for await (const chunk of stream) { // 提取 AI 回复的内容(处理空数据情况,避免报错) const content = chunk.choices[0]?.delta?.content || ''; if (content) { // 按照 SSE 标准格式,推送给前端 res.write(`data: ${content}\n\n`); } } // 所有内容推送完成,发送结束标识 res.write(`data: [DONE]\n\n`); res.end(); } catch (error) { // 错误处理:推送错误信息给前端 res.write(`data: 【AI 接口请求失败:${error.message}】\n\n`); res.write(`data: [DONE]\n\n`); res.end(); console.error('OpenAI 接口错误:', error); } } // 调用方式(替换原有定时器部分) // 注释掉原来的 aiReply 和 setInterval 代码,替换为: getOpenAIStream(userMsg, res);补充说明:需先在终端输入
npm install openai安装依赖(确保后端文件夹下执行该命令)。 -
API Key 需从 OpenAI 官网注册获取(免费额度足够测试,注意保护好自己的 Key,不要泄露)。
-
代码仅替换后端
server.js中“模拟 AI 回复”的核心逻辑,其他响应头、跨域设置等代码保持不变,确保和前端 SSE 连接兼容。 -
若无法访问 OpenAI 接口,可替换为国内 AI 接口(如文心一言、通义千问),逻辑类似,只需修改接口调用代码即可。。
-
增加错误重试:SSE 连接失败时,自动重试连接,提升用户体验。
八、总结
到这里,你已经学会了用 SSE 实现 AI 流式对话 Agent,总结一下核心知识点:
-
SSE 是前端原生的服务器单向推送技术,适合 AI 流式回复、通知等场景,比 WebSocket 更轻量、更容易上手。
-
前端用
EventSource建立 SSE 连接,通过onmessage接收服务器推送的数据,实现打字机效果。 -
后端需要设置 SSE 响应头,按照
data: 数据\n\n的格式逐字推送数据。 -
整个项目只有 2 个文件,代码全注释,复制就能运行,新手也能快速上手。
其实 SSE 没有想象中复杂,核心就是“服务器持续推数据,前端持续接数据”,只要掌握了这个逻辑,就能轻松实现各种流式效果。
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、转发~ 有任何问题,评论区留言,我会一一回复!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)