DeepSeek+SpringAI实现流式对话

news2025/6/7 2:38:45

大模型的响应速度通常是很慢的,为了避免用户用户能够耐心等待输出的结果,我们通常会使用流式输出一点点将结果输出给用户。

那么问题来了,想要实现流式结果输出,后端和前端要如何配合?后端要使用什么技术实现流式输出呢?接下来本文给出具体的实现代码,先看最终实现效果:
在这里插入图片描述

解决方案
在 Spring Boot 中实现流式输出可以使用 Sse(Server-Sent Events,服务器发送事件)技术来实现,它是一种服务器推送技术,适合单向实时数据流,我们使用 Spring MVC(基于 Servlet)中的 SseEmitter 对象来实现流式输出。

具体实现如下。
1.后端代码
Spring Boot 程序使用 SseEmitter 对象提供的 send 方法发送数据,具体实现代码如下:

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@RestController
public class StreamController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter streamData() {
        // 创建 SSE 发射器,设置超时时间(例如 1 分钟)
        SseEmitter emitter = new SseEmitter(60_000L);
        // 创建新线程,防止主程序阻塞
        new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    Thread.sleep(1000); // 模拟延迟
                    // 发送数据
                    emitter.send("time=" + System.currentTimeMillis());
                }
                // 发送完毕
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        }).start();
        return emitter;
    }
}

2.前端代码
前端接受数据流也比较简单,不需要在使用传统 Ajax 技术了,只需要创建一个 EventSource 对象,监听后端 SSE 接口,然后将接收到的数据流展示出来即可,如下代码所示:

<!DOCTYPE html>
<html>
  <head>
    <title>流式输出示例</title>
  </head>
  <body>
    <h2>流式数据接收演示</h2>
    <button onclick="startStream()">开始接收数据</button>
    <div id="output" style="margin-top: 20px; border: 1px solid #ccc; padding: 10px;"></div>

    <script>
      function startStream() {
        const output = document.getElementById('output');
        output.innerHTML = ''; // 清空之前的内容

        const eventSource = new EventSource('/stream');

        eventSource.onmessage = function(e) {
          const newElement = document.createElement('div');
          newElement.textContent = "print -> " + e.data;
          output.appendChild(newElement);
        };

        eventSource.onerror = function(e) {
          console.error('EventSource 错误:', e);
          eventSource.close();
          const newElement = document.createElement('div');
          newElement.textContent = "连接关闭";
          output.appendChild(newElement);
        };
      }
    </script>
  </body>
</html>

3.运行项目
运行项目测试结果:

  • 启动 Spring Boot 项目。
  • 在浏览器中访问地址 http://localhost:8080/index.html,即可看到流式输出的内容逐渐显示在页面上。

4.最终版:流式输出

import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.Map;

@RestController
public class ChatController {
    private final OpenAiChatModel chatModel;

    @Autowired
    public ChatController(OpenAiChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping("/ai/generate")
    public Map generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
        return Map.of("generation", this.chatModel.call(message));
    }

    @GetMapping("/ai/generateStream")
    public SseEmitter streamChat(@RequestParam String message) {
        // 创建 SSE 发射器,设置超时时间(例如 1 分钟)
        SseEmitter emitter = new SseEmitter(60_000L);
        // 创建 Prompt 对象
        Prompt prompt = new Prompt(new UserMessage(message));
        // 订阅流式响应
        chatModel.stream(prompt).subscribe(response -> {
            try {
                String content = response.getResult().getOutput().getContent();
                System.out.print(content);
                // 发送 SSE 事件
                emitter.send(SseEmitter.event()
                             .data(content)
                             .id(String.valueOf(System.currentTimeMillis()))
                             .build());
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        },
                                           error -> { // 异常处理
                                               emitter.completeWithError(error);
                                           },
                                           () -> { // 完成处理
                                               emitter.complete();
                                           }
                                          );
        // 处理客户端断开连接
        emitter.onCompletion(() -> {
            // 可在此处释放资源
            System.out.println("SSE connection completed");
        });
        emitter.onTimeout(() -> {
            emitter.complete();
            System.out.println("SSE connection timed out");
        });
        return emitter;
    }
}

前端核心 JS 代码如下:

$('#send-button').click(function () {
  const message = $('#chat-input').val();
  const eventSource = new EventSource(`/ai/generateStream?message=` + message);
  // 构建动态结果
  var chatMessages = $('#chat-messages');
  var newMessage = $('<div class="message user"></div>');
  newMessage.append('<img class="avatar" src="/imgs/user.png" alt="用户头像">');
  newMessage.append(`<span class="nickname">${message}</span>`);
  chatMessages.prepend(newMessage);
  var botMessage = $('<div class="message bot"></div>');
  botMessage.append('<img class="avatar" src="/imgs/robot.png" alt="助手头像">');
  // 流式输出
  eventSource.onmessage = function (event) {
    botMessage.append(`${event.data}`);
  };
  chatMessages.prepend(botMessage);
  $('#chat-input').val('');
  eventSource.onerror = function (err) {
    console.error("EventSource failed:", err);
    eventSource.close();
  };
});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2400820.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Spark征服之路-2.1-安装部署Spark(一)】

实验目标&#xff1a; 本节课实验将完成Spark 4种部署模式的其中2种&#xff0c;分别是Local、Standalone模式。 实验准备工作&#xff1a; 三台linux虚拟机spark的压缩包 实验步骤&#xff1a; Spark-local Spark的Local模式仅需要单个虚拟机节点即可&#xff0c;无需启…

VS代码生成工具ReSharper v2025.1——支持.NET 10和C# 14预览功能

实质上&#xff0c;ReSharper特征可用于C#&#xff0c;VB.net&#xff0c;XML&#xff0c;Asp.net&#xff0c;XAML和构建脚本。 使用ReSharper&#xff0c;你可以进行深度代码分析&#xff0c;智能代码协助&#xff0c;实时错误代码高亮显示&#xff0c;解决方案范围内代码分析…

【Godot】如何导出 Release 版本的安卓项目

在使用 Godot 引擎开发安卓游戏或应用时&#xff0c;发布到应用市场&#xff08;如 Google Play、华为应用市场等&#xff09;通常需要生成一个 Release 版本的 .apk 包&#xff0c;而非 Debug 版本。本文将详细介绍如何将 Godot 项目导出为 Release 版本的安卓项目&#xff0c…

VSCode 工作区配置文件通用模板(CMake + Ninja + MinGW/GCC 编译器 的 C++ 或 Qt 项目)

下面是一个通用模板&#xff0c;适用于大多数使用 VSCode CMake Ninja MinGW/GCC 编译器 的 C 或 Qt 项目。你可以将这个 .vscode 文件夹复制到你的项目根目录下&#xff0c;稍作路径调整即可使用。 &#x1f4c1; .vscode/ 目录结构&#xff08;通用模板&#xff09; .vs…

Java八股文——Redis篇

目录 1. 缓存穿透解决方案1. 缓存空值2. 布隆过滤器&#xff08;Bloom Filter&#xff09;3. 参数校验4. 接口限流与验证码 2. 缓存击穿解决方案1. 设置热点数据永不过期&#xff08;或很长过期时间&#xff09;2. 使用互斥锁&#xff08;如分布式锁&#xff09;3. 利用异步更新…

爬虫接口类型判断与表单需求识别全解析

爬虫接口类型判断与表单需求识别全解析 在爬虫开发中&#xff0c;准确判断目标接口的类型以及是否需要表单提交&#xff0c;是实现高效、稳定爬取的关键一步。本文将通过实际案例&#xff0c;详细介绍如何通过浏览器开发者工具和代码验证来判断接口类型及表单需求。 一、接口…

Chainlink:连接 Web2 与 Web3 的去中心化桥梁

区块链技术通过智能合约实现了去中心化的自动执行&#xff0c;但智能合约无法直接访问链下数据&#xff0c;限制了其在现实世界的应用。Chainlink 作为去中心化预言机网络&#xff0c;以信任最小化的方式解决了这一问题&#xff0c;成为连接传统互联网&#xff08;Web2&#xf…

编译一个Mac M系列可以用的yuview

做音视频的有一个神器工具YUView&#xff0c;具体使用和它的功能可以看&#xff1a;https://zhuanlan.zhihu.com/p/558580168&#xff0c; 这个作者讲得很清楚&#xff0c;但是官方只提供了intel的版本&#xff0c;arm版本要自己编&#xff0c;且依赖低版本的ffmpeg。 操作过程…

rabbitmq Topic交换机简介

1. Topic交换机 说明 尽管使用 direct 交换机改进了我们的系统&#xff0c;但是它仍然存在局限性——比方说我们的交换机绑定了多个不同的routingKey&#xff0c;在direct模式中虽然能做到有选择性地接收日志&#xff0c;但是它的选择性是单一的&#xff0c;就是说我的一条消息…

网络交换机:构建高效、安全、灵活局域网的基石

在数字化时代&#xff0c;网络交换机作为局域网(LAN)的核心设备&#xff0c;承担着数据转发、通信优化和安全防护的关键任务。其通过独特的MAC地址学习、冲突域隔离、VLAN划分等技术&#xff0c;显著提升了网络性能&#xff0c;成为企业、学校、医院等场景不可或缺的基础设施。…

【Oracle】存储过程

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 存储过程基础概述1.1 存储过程的概念与特点1.2 存储过程的组成结构1.3 存储过程的优势 2. 基础存储过程2.1 简单存储过程2.1.1 创建第一个存储过程2.1.2 带变量的存储过程 2.2 带参数的存储过程2.2.1 输入参…

单元测试-断言常见注解

目录 1.断言 2.常见注解 3.依赖范围 1.断言 断言练习 package com.gdcp;import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;//测试类 public class UserServiceTest {Testpublic void testGetGender(){UserService userService new UserService…

TDengine 在电力行业如何使用 AI ?

在你面前摆着一堆机器运行日志、传感器读数、电表数据&#xff0c;几十万、几百万条每秒增长的数据流&#xff0c;你会怎么处理&#xff1f;是加人、加脚本&#xff0c;还是干脆放弃实时分析&#xff1f; 过去&#xff0c;时序数据是工业的“副产品”&#xff1a;只是存着、查…

OpenCV C/C++ 视频播放器 (支持调速和进度控制)

OpenCV C/C 视频播放器 (支持调速和进度控制) 本文将引导你使用 C 和 OpenCV 库创建一个功能稍复杂的视频播放器。该播放器不仅能播放视频&#xff0c;还允许用户通过滑动条来调整播放速度&#xff08;加速/减速&#xff09;以及控制视频的播放进度。 使用opencv打开不会压缩画…

【Linux庖丁解牛】—自定义shell的编写!

1. 打印命令行提示符 在我们使用系统提供的shell时&#xff0c;每次都会打印出一行字符串&#xff0c;这其实就是命令行提示符&#xff0c;那我们自定义的shell当然也需要这一行字符串。 这一行字符串包含用户名&#xff0c;主机名&#xff0c;当前工作路径&#xff0c;所以&a…

Linux运维笔记:1010实验室电脑资源规范使用指南

文章目录 一. 检查资源使用情况&#xff0c;避免冲突1. 检查在线用户2. 检查 CPU 使用情况3. 检查 GPU 使用情况4. 协作建议 二. 备份重要文件和数据三. 定期清理硬盘空间四. 退出 ThinLinc 时注销&#xff0c;释放内存五. 校外使用时配置 VPN注意事项 总结 实验室的电脑配备了…

【Docker 从入门到实战全攻略(二):核心概念 + 命令详解 + 部署案例】

5. Docker Compose Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。通过一个 YAML 文件来配置应用服务&#xff0c;然后使用一个命令即可创建并启动所有服务。 基本命令 docker-compose up # 创建并启动所有服务 docker-compose down # 停止并移除容器、网络等…

【conda配置深度学习环境】

好的&#xff01;我们从头开始配置一个基于Conda的虚拟环境&#xff0c;覆盖深度学习&#xff08;如PyTorch&#xff09;和传统机器学习&#xff08;如XGBoost&#xff09;&#xff0c;并适配你的显卡&#xff08;假设为NVIDIA&#xff0c;若为AMD请告知&#xff09;。以下是完…