Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)
一、引言
在现代Web应用中,聊天机器人已经成为一个非常常见的功能。为了实现流畅的用户体验,流式输出(Streaming Output)是一个关键技术。通过流式输出,聊天机器人可以在生成响应的过程中逐步将内容推送到前端,而不是等待整个响应完成后再一次性发送。这种方式可以显著减少用户等待时间,提升用户体验。
本文将介绍如何结合Spring AI和Vue3,利用Server-Sent Events (SSE) 实现聊天机器人的流式输出。
二、技术栈
-
后端: Spring Boot + Spring AI
-
前端: Vue3 + Vite
-
通信协议: Server-Sent Events (SSE)
三、Server-Sent Events (SSE)简介
Server-Sent Events (SSE) 是一种允许服务器向客户端推送实时更新的技术。与 WebSocket 不同,SSE 是单向的,即服务器可以向客户端发送数据,但客户端不能向服务器发送数据。SSE 基于 HTTP 协议,使用简单且易于实现,非常适合需要服务器向客户端推送实时数据的场景,如新闻更新、股票价格、聊天机器人等。
SSE 的工作原理非常简单。客户端通过创建一个 EventSource 对象来连接到服务器的一个端点。服务器通过保持 HTTP 连接打开,并持续向客户端发送事件流。每个事件都是一个文本块,通常以 data: 开头,后面跟着实际的数据。客户端通过监听 onmessage 事件来处理这些数据。
优点
-
简单易用: SSE 基于 HTTP 协议,使用简单,易于实现。
-
自动重连: 如果连接断开,EventSource 会自动尝试重新连接。
-
文本格式: 数据以纯文本格式发送,易于解析和处理。
-
单向通信: 适合服务器向客户端推送数据的场景。
缺点
-
单向通信: SSE 只支持服务器向客户端发送数据,不支持客户端向服务器发送数据。
-
文本格式限制: 数据必须以文本格式发送,不支持二进制数据。
-
连接限制: 浏览器对每个源的 SSE 连接数有限制(通常是 6 个)。
四、后端实现
后端实现参考前文:
Spring AI进阶:使用DeepSeek本地模型搭建AI聊天机器人(一)-CSDN博客
Spring AI进阶:AI聊天机器人之ChatMemory持久化(二)-CSDN博客
五、前端实现
1、使用 Vite 创建 Vue 3 项目
Vite 是一个现代化的前端构建工具,具有极快的启动速度和热更新能力。如果你还没有安装 Vite,可以通过以下命令全局安装:
npm install -g create-vite
运行以下命令来创建一个新的 Vue 3 项目:
npm create vite@latest ai-chat-robot -- --template vue
进入程序目录,运行以下命令
cd ai-chat-robot npm install npm run dev
可以看到程序已运行在本地的5173端口,点击打开看到以下画面则创建成功
2、安装必要组件库
安装Axios
npm install axios --save
安装sass 与 sass-loader
npm install sass sass-loader --save
安装Element-plus
npm install element-plus --save
安装完成后,你需要在项目中引入 Element Plus,打开main.js
import {createApp} from 'vue' import './style.css' import App from './App.vue' import ElementPlus from 'element-plus' // 引入 Element Plus import 'element-plus/dist/index.css' // 引入 Element Plus 的样式 createApp(App) .use(ElementPlus) // 使用 Element Plus .mount('#app')
安装MarkdownIt插件,大模型返回的内容都是Markdown格式的文本,所以需要安装此插件实现markdown文本的回显
npm install markdown-it --save
安装highlight.js,Highlight.js 是一个用 JavaScript 编写的语法高亮库。它可以在浏览器和服务器上工作,几乎可以处理任何标记,并且具有自动语言检测功能。
npm install highlight.js --save
3、新建Chat.vue
引入MarkdownIt插件与highlight.js插件,编写一个简单的聊天对话框页面,并添加一些点击事件,代码如下:
AI聊天机器人 开启新会话
- {{ session.sessionName }}
' + hljs.highlight(lang, str, true).value + '
'; } catch (__) { } } return '' + md.utils.escapeHtml(str) + '
'; } }); // 引用聊天消息容器 const chatMessages = ref(null); // 聊天会话列表 const sessions = ref([]); // 当前选中的会话 const currentSession = ref({}); // 新消息输入框内容 const newMessage = ref(''); // 聊天记录 const messages = ref([]); // 定义事件源的引用,用于实时通信 const eventSource = ref(null); // 选择会话 const selectSession = (session) => { currentSession.value = session; }; // 创建新会话 const openNewSession = () => { newMessage.value = ''; messages.value = []; currentSession.value = {}; }; // 发送消息 const sendMessage = () => { }; .chat-container { display: flex; height: 100vh; padding: 0; } .chat-sessions { width: 25%; background-color: #f4f4f4; padding: 10px; .chat-header { text-align: center; line-height: 30px; width: 100%; } ul { list-style-type: none; padding: 0; li { padding: 10px; cursor: pointer; &:hover { background-color: #ddd; } } } } .chat-window { width: 75%; display: flex; flex-direction: column; } .chat-messages { flex: 1; overflow-y: auto; padding: 10px; background-color: #fff; .message { margin-bottom: 10px; pre { background-color: #f9f9f9; padding: 10px; border-radius: 5px; white-space: pre-wrap; } :deep(.think){ display: inline-block; padding: 0 10px; color: #999999; font-size: 13px; background-color: #efecec; border-radius: 5px; } &.sent { text-align: right; pre { background-color: #e1ffc7; } } } } .chat-input { display: flex; padding: 10px; textarea { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; resize: none; } button { margin-left: 10px; padding: 10px 20px; border: none; background-color: #007bff; color: #fff; border-radius: 5px; cursor: pointer; &:hover { background-color: #0056b3; } } }运行程序后页面效果如下:
4、添加接口定义的JS代码
修改vite.config.js,添加后台服务器地址
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path'; // https://vite.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, 'src') } }, server: { host: true, port: 5173, proxy: { "/api/": { target: "http://127.0.0.1:8080", // 后台接口 changeOrigin: true, secure: false, // 如果是https接口,需要配置这个参数 // ws: true, //websocket支持 rewrite: (path) => path.replace(/^\/api/, "/api"), }, } } })
增加axios.js工具
import axios from "axios"; // createAxios function createAxios() { // instance const instance = axios.create({ baseURL: 'http://127.0.0.1:8080', timeout: 50000, }); // defaults instance.defaults.headers.post["Content-Type"] = "application/json;charset=utf-8"; instance.defaults.headers.get["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8"; return instance; } // export default createAxios;
新建request.js定义接口
import createAxios from "@/utils/axios.js"; // instance const instance = createAxios(); // API instance.API = { // 查询聊天记录 getMessages: (params = {}) => { return instance.get(`/api/chat/ai/messages?sessionId=` + params); }, // 查询聊天会话 getSession: (params = {}) => { return instance.get(`/api/chat/ai/sessions?userId=` + params); }, }; // export default instance;
5、在Chat.vue中新增查询聊天会话的方法
/** * 初始化会话列表 * @param init 是否初次加载 */ const init = (init) => { let userId = localStorage.getItem('WANGANUI_USER'); // 设置一个默认的用户ID,并存储到缓存 if (!userId) { userId = String(new Date().getTime()); localStorage.setItem('WANGANUI_USER', userId); } ChatApi.API.getSession(userId).then(res => { sessions.value = res.data; if (sessions.value.length > 0 && init) { currentSession.value = sessions?.value[0]; } }); }; // 初始化会话列表 init(true)
6、在Chat.vue中新增查询聊天记录的方法
在初始化session的方法和selectSession方法中调用,这里我将DeepSeek-R1返回的逻辑推理的内容做了自定义处理。
// 查询聊天记录 const loadMessages = () => { ChatApi.API.getMessages(currentSession.value.sessionId).then(res => { res.data.forEach(item => { if (item.messageType === 'USER') { messages.value.push({ msg: item.text, type: 1 }); } else { const text = item.text.replaceAll("", "").replaceAll("", ""); messages.value.push({ msg: md.render(text), type: 2 }); } }); }); };
7、在Chat.vue中新增发送消息的方法
-
构建 SSE 请求:
-
使用 EventSource 创建一个 SSE 连接,向后端发送用户输入的消息、会话 ID 和用户 ID。
-
请求 URL 示例:http://127.0.0.1:8080/api/chat/ai/generateStream?message=用户输入&sessionId=会话ID&userId=用户ID。
-
处理 SSE 响应:
-
监听 onmessage 事件,逐步接收后端返回的数据。
-
将接收到的数据拼接成完整的消息(messageOrigin),并使用 md.render 将 Markdown 格式的文本渲染为 HTML。
-
将渲染后的内容更新到消息列表的最后一条消息中(messages.value[messages.value.length - 1].msg)。
-
结束处理:
-
当后端返回 finishReason: "stop" 时,表示消息传输完成,对消息内容进行格式化(替换 标签为 )。
-
调用 scrollToBottom 方法,将聊天窗口滚动到底部。
-
错误处理:
-
监听 onerror 事件,如果发生错误,关闭 SSE 连接。
-
监听 onclose 事件,在连接关闭时执行相关操作。
// 发送消息 const sendMessage = () => { const value = newMessage.value; if (!value) { return ElMessage.warning('请输入问题'); } if (!currentSession.value) { // 添加一个模拟的新Session const sessionId = ''; const sessionName = value.length >= 15 ? String(value).substring(0, 15) + '...' : value; sessions.value = [{ sessionName, sessionId }].concat(sessions.value); } if (eventSource.value != null) { eventSource.value.close(); } // 将用户输入的消息添加到消息列表中,并设置消息类型为用户发送 messages.value.push({ msg: newMessage.value, type: 1 }); messages.value.push({ msg: '', type: 2 }); newMessage.value = ''; let messageOrigin = ''; const apiBaseUrl = "http://127.0.0.1:8080/api/chat/ai/generateStream"; const encodedValue = encodeURIComponent(value); const encodedSessionId = currentSession.value?.sessionId ? encodeURIComponent(currentSession.value.sessionId) : ''; const userId = localStorage.getItem('WANGANUI_USER') || ''; eventSource.value = new EventSource(`${apiBaseUrl}?message=${encodedValue}&sessionId=${encodedSessionId}&userId=${userId}`); eventSource.value.onmessage = function (event) { try { let substring = event.data.replaceAll("data:", ""); let parse = JSON.parse(substring); messageOrigin += parse.result?.output?.text; if (parse.result?.metadata?.finishReason === "stop") { messageOrigin = messageOrigin.replace("", "").replace("", ""); eventSource.value.close(); } messages.value[messages.value.length - 1].msg = md.render(messageOrigin); // 调用滚动方法 scrollToBottom(); if (!currentSession.value.sessionId) { init(false); } } catch (error) { console.error("消息异常:", error); } }; eventSource.value.onerror = function (event) { eventSource.value.close(); }; eventSource.value.onclose = function (event) { console.log("事件关闭:", event); }; };
在大模型输出时,我们可以根据输出的内容动态滚动聊天窗口
/** * 滚动到聊天框底部 */ const scrollToBottom = async () => { await nextTick(); if (chatMessages.value) { const lastMessage = chatMessages.value?.children[chatMessages.value.children.length - 1]; if (lastMessage) { lastMessage.scrollIntoView({behavior: 'smooth', block: 'end'}); } } else { console.error('聊天框不可用'); } };
六、总结
通过以上步骤,你已经成功实现了一个基于 Spring AI 和 Vue 3 的聊天机器人应用,利用 SSE 实现了流式输出,提升了用户体验。你可以根据需要进一步扩展和优化功能。
希望本文对你有所帮助!如果有任何问题或建议,请随时联系。
参考资料:
前端源码(main分支):wangxt_base/ai-chat-web - Gitee.com
后端源码:ai-chat: Spring AI 相关技术介绍
markdown-it文档:Core | markdown-it 中文文档
highlight.js文档:开始 | highlight.js中文网
-
-
-
-
-