SpringBoot仿GPT数据流传输

news2024/12/28 8:32:54

目录

  • Java数据流传输响应
    • 前提
    • Springboot文字流响应
    • Web端接收流数据并显示
  • SpingBoot集成ChatGPT使用流响应结果

Java数据流传输响应

前提

在折腾ChatGpt集成在SpringBoot项目时,发现了ChatGpt api返回数据时有两种返回方式,一种是使用流传输,另一种是直接返回全部的数据。如果使用流传输,响应的速度很快,不需要获取全部答案的内容后再开始响应返回,可以达到服务端返回数据时像打字机一样的效果返回答案;而直接返回全部数据的话,需要在服务端接收完ChatGpt的全部结果后再一次性把全部的数据响应返回给客户端进行展示,这个缺点就是很慢,一个结果最快也需要10秒钟。所以本文尝试模仿ChatGpt使用流数据的方式返回数据给客户端。

Springboot文字流响应

首先再服务端测试使用流响应固定的文本字符串数据
主要方法是使用HttpServletResponse响应流,需要设置响应头如下:

res.setHeader("Content-Type", "text/event-stream");
res.setContentType("text/event-stream");
res.setCharacterEncoding("UTF-8");
res.setHeader("Pragma", "no-cache");

测试接口如下:

// 测试响应流
    @GetMapping("/api/test/sss")
    @AnonymousAccess
    public void test(String prompt, HttpServletResponse res) throws IOException, InterruptedException {
        log.info("【prompt内容】:{}", prompt);
        String str = "       什么是爱而不得? \n" +
                "东边日出西边雨,道是无晴却有晴。\n" +
                "他朝若是同淋雪,此生也算共白头。\n" +
                "我本将心向明月,奈何明月照沟渠。\n" +
                "此时相望不相闻,愿逐月华流照君。\n" +
                "衣带渐宽终不悔,为伊消得人憔悴。\n" +
                "此情可待成追忆,只是当时已惘然。\n" +
                "人生若只如初见,何事西风悲画扇。\n" +
                "曾经沧海难为水,除却巫山不是云。\n" +
                "何当共剪西窗烛,却话巴山夜雨时。\n" +
                "天长地久有时尽,此恨绵绵无绝期。\n" +
                "\n";
        // 响应流
        res.setHeader("Content-Type", "text/event-stream");
        res.setContentType("text/event-stream");
        res.setCharacterEncoding("UTF-8");
        res.setHeader("Pragma", "no-cache");
        ServletOutputStream out = null;
        try {
            out = res.getOutputStream();
            for (int i = 0; i < str.length(); i++) {
                out.write(String.valueOf(str.charAt(i)).getBytes());
                // 更新数据流
                out.flush();
                Thread.sleep(100);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

使用该接口,返回的数据就需要使用流来接受处理
如果直接再浏览器中请求该接口,效果如下:
在这里插入图片描述

Web端接收流数据并显示

在js中接收该文字流数据需要设置响应类型为:xhr.setRequestHeader("Content-Type", "text/event-stream");

具体实现数据接收代码如下:

        // 创建 XMLHttpRequest 对象
        const xhr = new XMLHttpRequest();
        // 设置请求的 URL
        xhr.open(
            "GET",
            `http://localhost:8080/api/test/sss`
        );
        // 设置响应类型为 text/event-stream
        xhr.setRequestHeader("Content-Type", "text/event-stream");
        // 监听 readyStateChange 事件
        xhr.onreadystatechange = () => {
            // 如果 readyState 是 3,表示正在接收数据
            if (xhr.readyState === 3) {
                // 将数据添加到文本框中
                console.log('xhr.responseText :>> ', xhr.responseText);
                reply("images/touxiang.png", xhr.responseText, randomStr)
                var height = $("#message").height();
                $("html").scrollTop(height)
            }
        };
        // 发送请求
        xhr.send();

效果如下:

在这里插入图片描述

这种效果就实现了ChatGpt的那种流传输的效果。

SpingBoot集成ChatGPT使用流响应结果

具体集成ChatGPT SDK的教程可以查看官方文档:https://gitcode.net/mirrors/grt1228/chatgpt-java

导入pom依赖:

<dependency>
    <groupId>com.unfbx</groupId>
    <artifactId>chatgpt-java</artifactId>
    <version>1.0.13</version>
</dependency>

使用ChatGpt流传输的demo示例可以查看:https://gitcode.net/mirrors/grt1228/chatgpt-java/blob/main/src/test/java/com/unfbx/chatgpt/OpenAiStreamClientTest.java

官方Demo SDK连接ChatGPT的教程很详细,具体教程看上面的demo文档就好了,这里主要讲一下拿到数据的细节和怎么把拿到的流数据响应给客户端。

下面是SDK调用的示例方法

 public static void ChatGptSendV1(String prompt, ChatSocketVo chatSocketVo) throws IOException {
        OpenAiConfig openAiConfig = new OpenAiConfig();
        OpenAiStreamClient openAiClient = OpenAiStreamClient.builder()
                .apiKey(Collections.singletonList(openAiConfig.getTkoen()))
                //自己做了代理就传代理地址,没有可不不传
                .apiHost(openAiConfig.getDomain())
                .build();
        //聊天模型:gpt-3.5
        ConsoleEventSourceListener eventSourceListener = new ConsoleEventSourceListener();
        Message message = Message.builder().role(Message.Role.USER).content(prompt).build();
        ChatCompletion chatCompletion = ChatCompletion
                .builder()
                .model(ChatCompletion.Model.GPT_3_5_TURBO.getName())
                .temperature(0.2)
                .maxTokens(2048)
                .messages(Arrays.asList(message))
                .stream(true)
                .build();

        openAiClient.streamChatCompletion(chatCompletion, eventSourceListener);
        CountDownLatch countDownLatch = new CountDownLatch(1);
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

在代码中消息的回调处理方法是:ConsoleEventSourceListener eventSourceListener = new ConsoleEventSourceListener();.
openAiClient.streamChatCompletion(chatCompletion, eventSourceListener);调用流传输的方法中,传入了一个SSE的EventSourceListener。其中的代码如下:

package com.unfbx.chatgpt.sse;

import java.util.Objects;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ConsoleEventSourceListener extends EventSourceListener {
    private static final Logger log = LoggerFactory.getLogger(ConsoleEventSourceListener.class);

    public ConsoleEventSourceListener() {
    }

    public void onOpen(EventSource eventSource, Response response) {
        log.info("OpenAI建立sse连接...");
    }

    public void onEvent(EventSource eventSource, String id, String type, String data) {
        log.info("OpenAI返回数据:{}", data);
        if (data.equals("[DONE]")) {
            log.info("OpenAI返回数据结束了");
        }
    }

    public void onClosed(EventSource eventSource) {
        log.info("OpenAI关闭sse连接...");
    }

    public void onFailure(EventSource eventSource, Throwable t, Response response) {
        try {
            if (Objects.isNull(response)) {
                log.error("OpenAI  sse连接异常:{}", t);
                eventSource.cancel();
            } else {
                ResponseBody body = response.body();
                if (Objects.nonNull(body)) {
                    log.error("OpenAI  sse连接异常data:{},异常:{}", body.string(), t);
                } else {
                    log.error("OpenAI  sse连接异常data:{},异常:{}", response, t);
                }

                eventSource.cancel();
            }
        } catch (Throwable var5) {
            throw var5;
        }
    }
}

很容易看出OpenAI回调流消息的处理方式是建立了sse连接,然而这个sse连接和WebSocket的使用很相似,在onEvent方法中data就是ai回答的消息内容。只是该默认的消息输出只做了日志的输出。
所以我们可以这样处理:

  1. 自己新建一个类集成EventSourceListener,模仿上面的ConsoleEventSourceListener,重写对应的结果建立连接,返回数据,关闭连接等方法。
  2. 在EventSourceListener类的构造函数中可以传入你需要的场景值等,比如websocket的session,然后每次接收到消息时,立马使用websoket将消息发送到客户端。
  3. 注意全局参数的多线程安全问题,由于建立的是长连接,构造参数传进的场景值必然需要当作全局变量进行定义,但是如果在多人同时使用改接口时,场景值就会错乱出现线程安全问题。解决方法可以在定义全局变量时加上@Autowired注解,原理可以参考其他教程。

自定义EventSourceListener代码示例如下(加入了一些消息记录的处理逻辑):

package com.team.modules.system.Utils;

import java.util.Date;
import java.util.Objects;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.team.modules.system.domain.ChatgptInfo;
import com.team.modules.system.domain.vo.ChatSocketVo;
import com.team.modules.system.domain.vo.IpDataVo;
import com.team.modules.websocket.WebSocketChatServe;
import com.team.utils.CDKeyUtil;
import com.unfbx.chatgpt.entity.chat.ChatCompletionResponse;
import lombok.SneakyThrows;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Created by tao.
 * Date: 2023/6/8 15:51
 * 描述:
 */
public class ChatEventSourceListener extends EventSourceListener {
    private static final Logger log = LoggerFactory.getLogger(com.unfbx.chatgpt.sse.ConsoleEventSourceListener.class);
    private static WebSocketChatServe webSocketChatServe = new WebSocketChatServe();
    @Autowired
    private String resContent;
    @Autowired
    private ChatSocketVo chatSocketVo;


    public ChatEventSourceListener(ChatSocketVo socketVo) {
        chatSocketVo = socketVo;
        resContent = CDKeyUtil.getSequence() + ":::";
    }

    public void onOpen(EventSource eventSource, Response response) {
        log.info("OpenAI建立sse连接...");
    }

    int i = 0;

    @SneakyThrows
    public void onEvent(EventSource eventSource, String id, String type, String data) {
        // OpenAI处理数据
//        log.info(i + "---------OpenAI返回数据:{}", data);
//        i++;
        if (!data.equals("[DONE]")) {
            ObjectMapper mapper = new ObjectMapper();
            ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class); // 读取Json
            String content = mapper.writeValueAsString(completionResponse.getChoices().get(0).getDelta());
            resContent = resContent + content;
            // 使用ws进行数据传输
            webSocketChatServe.sendMessageByKey(resContent, chatSocketVo.getKey());
        } else {
            log.info("OpenAI返回数据结束了");
            String[] split = resContent.split(":::");
            resContent = CDKeyUtil.getSequence() + ":::";
            // 记录内容和ip信息等
            String ip = chatSocketVo.getIpAddr();
            String ua = chatSocketVo.getUa();
            // 获取相关信息
            IpDataVo ipData = chatSocketVo.getIpDataVo();
            String address = ipData.getCountry() + " " + ipData.getProvince() + " " + ipData.getCity() + " " + ipData.getDistrict();
//            String address = "";
            ChatgptInfo chatgptInfo = new ChatgptInfo(chatSocketVo.getPrompt(), split[1], ip, address, ua, new Date(), ipData.getLocation());
            chatSocketVo.getChatgptService().save(chatgptInfo);
        }
    }

    public void onClosed(EventSource eventSource) {
        log.info("OpenAI关闭sse连接...");
    }

    @SneakyThrows
    public void onFailure(EventSource eventSource, Throwable t, Response response) {
        try {
            if (Objects.isNull(response)) {
                log.error("OpenAI  sse连接异常:{}", t);
                eventSource.cancel();
            } else {
                ResponseBody body = response.body();
                if (Objects.nonNull(body)) {
                    log.error("OpenAI  sse连接异常data:{},异常:{}", body.string(), t);
                } else {
                    log.error("OpenAI  sse连接异常data:{},异常:{}", response, t);
                }
                eventSource.cancel();
            }
        } catch (Throwable var5) {
            throw var5;
        }
    }
}

效果如下:
在这里插入图片描述

当然响应返回数据的方式也可以使用文章开头介绍的使用响应流进行,缺点是还是得去规避线程安全问题,可以加一个@Async注解;然后尝试过在本地用该方法没发现有什么问题,但是部署在服务器发现该方法就行不通了,会等所有数据全部返回才将数据返回。

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

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

相关文章

MyCat总结

目录 什么是mycat 核心概念 逻辑库 逻辑表 分片节点 数据库主机 用户 mycat原理 目录结构 配置文件 读写分离 搭建读写分离 搭建主从复制&#xff1a; 搭建读写分离&#xff1a; 分片技术 垂直拆分 实现分库&#xff1a; 水平拆分 实现分库&#xff1a; ER表 全局表 分…

简要介绍 | 基于深度学习的姿态估计技术

注1&#xff1a;本文系“简要介绍”系列之一&#xff0c;仅从概念上对基于深度学习的姿态估计技术进行非常简要的介绍&#xff0c;不适合用于深入和详细的了解。 注2&#xff1a;"简要介绍"系列的所有创作均使用了AIGC工具辅助 基于深度学习的姿态估计技术 背景介绍 …

【备战秋招】每日一题:2023.05-B卷-华为OD机试 - 告警抑制

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&a…

基于Dlib的疲劳检测系统

需要源码的朋友可以私信我 基于Dlib的疲劳检测系统 1、设计背景及要求2、系统分析3、系统设计3.1功能结构图3.2基于EAR、MAR和HPE算法的疲劳检测3.2.1基于EAR算法的眨眼检测3.2.2基于MAR算法的哈欠检测3.3.3基于HPE算法的点头检测 4、系统实现与调试4.1初步实现4.2具体实现过程…

使用MATLAB画SCI论文图

从gcf和gca说起 不论是 Python 绘图还是Matlab绘图&#xff0c;想要获得更好看的图&#xff0c;都会用到这两个单词。 gcf&#xff1a;get current figure&#xff0c;是目标图像的图形句柄对象 gca&#xff1a;get current axes&#xff0c;是目标图像的坐标轴句柄对象 Mat…

pandas---数学函数、离散化处理、分组聚合

1. 数学函数 方差&#xff1a;当数据分布比较分散&#xff08;即数据在平均数附近波动较大&#xff09;时&#xff0c;各个数据与平均数的差的平方和 较大&#xff0c;方差就较大&#xff1b;当数据分布比较集中时&#xff0c;各个数据与平均数的差的平方和较小。因此方差越 …

【计算机网络】如何学好计网-第一章概论

相关术语 URI&#xff1a;Uniform Resource Identifier 统一资源标识符&#xff0c;指的是一个资源 URL&#xff1a;Uniform Resource Location 统一资源定位符&#xff0c;URI的子集&#xff0c;用地址定为的方式指定一个资源 URN&#xff1a;Uniform Resource Name 统一资…

北京论道|光环云李卓然:以“云”为帆 赋能文化出海行业创新增长

2023年6月15~16日&#xff0c;由流媒体网主办&#xff0c;北京新媒体&#xff08;集团&#xff09;有限公司、北京联通、北京电信、北京移动共同协办的「北京论道暨第25届中国智能视听与科技创新高峰论坛」在北京隆重举行。 本届论道以“电视的未来与未来的电视”为主题&#…

java语言中方法的重载

文章目录 前言一、重载是什么&#xff1f;二、使用步骤 1.实操展示2.注意事项总结 前言 小时候&#xff0c;我们学会了十以内的加法&#xff0c;却对十以外的加法感到害怕&#xff0c;但从现在来看&#xff0c;它们都属于加法&#xff0c;只是计算方法略有不同。方法的重载也一…

【备战秋招】每日一题:2023.05-B卷-华为OD机试 - 阿里巴巴找黄金宝箱(IV)

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&a…

Computer Graphics From Scratch - Chapter 9

系列文章目录 简介&#xff1a;Computer Graphics From Scratch-《从零开始的计算机图形学》简介 第一章: Computer Graphics From Scratch - Chapter 1 介绍性概念 第二章&#xff1a;Computer Graphics From Scratch - Chapter 2 基本光线追踪 第三章&#xff1a;Computer Gr…

IEEE 最佳论文提名 | TraND:用步态算法优化身份识别

来源&#xff1a;投稿 作者&#xff1a;小灰灰 编辑&#xff1a;学姐 论文标题&#xff1a;TraND: Transferable Neighborhood Discovery for Unsupervised Cross-domain Gait Recognition 论文链接: https://arxiv.org/pdf/2102.04621v1.pdf 步态识别开始应用在公共场域身份…

centos mysql安装配置远程访问

包含以下三个部分&#xff1a;mysql安装&#xff0c;mysql密码重置、mysql配置远程访问。 一 centos安装mysql 1、wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm 2、yum -y install mysql57-community-release-el7-10.noarch.rpm 3、yum…

to be delete

一、grafana版本升级 1.1 还是先跟着官网简单走一波 建议经常升级Grafana&#xff0c;以获取最新的修补程序和增强功能。 为了实现这一点&#xff0c;Grafana升级向后兼容&#xff0c;并且升级过程简单快捷。升级通常是安全的&#xff08;在许多次要版本和一个主要版本之间&a…

Python习题进阶

1.十六进制数字的大小 描述 计算的世界&#xff0c;除了二进制与十进制&#xff0c;使用最多的就是十六进制了&#xff0c;现在使用input读入一个十六进制的数字&#xff0c;输出它的十进制数字是多少&#xff1f; 输入描述&#xff1a; input读入一个十六进制数字&#xf…

微信支付服务商接入指引(企业)

目录 一、官方指引二、申请流程1.进入服务商平台2.填写资料3.账户验证&资料审核 三、登录 一、官方指引 https://kf.qq.com/faq/161220iqeAfA1612202yeURB.html 二、申请流程 1.进入服务商平台 https://pay.weixin.qq.com/static/partner_guide/service_provider.shtml…

【JVM快速入门篇】

本笔记内容为狂神说JVM快速入门篇部分 目录 一、JVM的位置 二、JVM体系结构 三、类加载器&#xff08;ClassLoader&#xff09; 四、双亲委派机制 双亲委派机制的作用 五、沙箱安全机制 六、Native ​编辑Native Method Stack 本机方法栈 Native Ilnterface本地接口…

低代码靠谱吗?实操一遍就知道了

一、前言 最近一段时间&#xff0c;“低代码”概念特别流行&#xff0c;有些人特别推崇它&#xff0c;也有些人对此不屑一顾。 推崇它的人认为&#xff0c;它有很多优点&#xff0c;比如说能够降低开发周期&#xff0c;提高系统开发效率&#xff0c;降低开发成本&#xff0c;学…

使用HBuilder将h5网站打包成app 自行打包

1、点击manifest.json&#xff0c;基础配置&#xff0c;APP名字&#xff0c;是否全面屏 2、然后点击图标配置&#xff0c;选好后点自动生成所有图标并替换 点击模块配置&#xff0c;选VideoPlayer和X5内核 然后顶部菜单选发行&#xff0c;选择原生App-云打包 选公测证书然后…

WPF教程-XAML介绍

XAML介绍 1.定义 为构建应用程序用户界面而创建的一种新的“可扩展应用程序标记语言”&#xff0c;提供一种便于扩展和定位的语法来定义和程序业务逻辑分离的用户界面。 XAML 是一种 XML&#xff08;Extensible Markup Language&#xff09;的扩展&#xff0c;XAML 是 WPF 中…