Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)

06-01 1603阅读

一、引言

        在现代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 事件来处理这些数据。

    优点

    1. 简单易用: SSE 基于 HTTP 协议,使用简单,易于实现。

    2. 自动重连: 如果连接断开,EventSource 会自动尝试重新连接。

    3. 文本格式: 数据以纯文本格式发送,易于解析和处理。

    4. 单向通信: 适合服务器向客户端推送数据的场景。

    缺点

    1. 单向通信: SSE 只支持服务器向客户端发送数据,不支持客户端向服务器发送数据。

    2. 文本格式限制: 数据必须以文本格式发送,不支持二进制数据。

    3. 连接限制: 浏览器对每个源的 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

    Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)

    进入程序目录,运行以下命令

    cd ai-chat-robot
    npm install
    npm run dev

    可以看到程序已运行在本地的5173端口,点击打开看到以下画面则创建成功Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)

    Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)

    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 }}
    
            
          
          
          
            
            发送
          
        
      
    
    
    import hljs from 'highlight.js';
    import MarkdownIt from 'markdown-it';
    import {nextTick, ref} from 'vue';
    // 初始化MarkdownIt实例
    const md = new MarkdownIt({
      html: true,
      linkify: true,
      typographer: true,
      highlight: (str, lang) => {
        if (lang && hljs.getLanguage(lang)) {
          try {
            return '
    ' +
              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; } } }

    运行程序后页面效果如下:

    Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)

    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
            });
          }
        });
      });
    };

    Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)

    Spring AI应用:聊天机器人之前端Vue3+SSE实现流式输出(打字机)(三)

    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中文网

                      

                      

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码