基于SpringBoot+WebSocket的前后端连接,并接入文心一言大模型API

前言:

本片博客只讲述了操作的大致流程,具体实现步骤并不标准,请以参考为准。

本文前提:熟悉使用webSocket

如果大家还不了解什么是WebSocket,可以参考我的这篇博客:

rWebSocket 详解:全双工通信的实现与应用-CSDN博客文章浏览阅读214次。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务器主动向客户端推送数据。通过 WebSocket API,浏览器和服务器只需完成一次握手,即可建立持久性连接,开始双向数据传输。https://blog.csdn.net/Future_yzx/article/details/145359356?spm=1001.2014.3001.5502文章浏览阅读214次。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务器主动向客户端推送数据。通过 WebSocket API,浏览器和服务器只需完成一次握手,即可建立持久性连接,开始双向数据传输。https://blog.csdn.net/Future_yzx/article/details/145359356?spm=1001.2014.3001.5502https://blog.csdn.net/Future_yzx/article/details/145359356?spm=1001.2014.3001.5502

要使用Java的Springboot+WebSocket连接前后端并接入文心一言大模型API来实现实时聊天,可以分为几个主要步骤:

  1. 搭建WebSocket服务端
  2. 与文心一言大模型API对接
  3. 前端WebSocket连接

一、 搭建WebSocket服务端(使用Java)

我们可以使用Spring Boot来搭建一个WebSocket服务端。

(1)添加依赖: 在pom.xml中添加Spring Boot WebSocket的依赖:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

(2)配置WebSocket连接: 创建一个WebSocket配置类来启用WebSocket功能:

java 复制代码
package com.qcby.byspringbootdemo.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
 
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

作用: 这个类负责配置Spring WebSocket。通过使用 ServerEndpointExporter 来实现 WebSocket 的启用

(3)创建WebSocketServer : 在 WebSocketServer 类中处理连接、消息接收和发送。

java 复制代码
package com.qcby.byspringbootdemo.server;
 
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qcby.byspringbootdemo.util.WenXinYiYanUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
 
/**
 * WebSocket 服务端
 */
@Component
@Slf4j
@Service
@ServerEndpoint("/api/websocket/{sid}")
public class WebSocketServer {
    //当前在线连接数
    private static int onlineCount = 0;
 
    //存放每个客户端对应的 WebSocketServer 对象
    private static final CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
 
    //用户信息
    private Session session;
 
    //当前用户的 sid
    private String sid = "";
 
    //JSON解析工具
    private static final ObjectMapper objectMapper = new ObjectMapper();
 
    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        this.sid = sid;
        webSocketSet.add(this); //加入集合
        addOnlineCount(); //在线数加1
        try {
            // 发送 JSON 格式的消息
            String successMessage = "{\"message\": \"conn_success\"}";
            sendMessage(successMessage);
            log.info("有新窗口开始监听: " + sid + ", 当前在线人数为: " + getOnlineCount());
        } catch (IOException e) {
            log.error("WebSocket IO Exception", e);
        }
    }
 
 
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this); //从集合中删除
        subOnlineCount(); //在线数减1
        log.info("释放的 sid 为:" + sid);
        log.info("有一连接关闭!当前在线人数为 " + getOnlineCount());
    }
 
    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自窗口 " + sid + " 的信息: " + message);
 
        String targetSid;
        String msgContent;
        try {
            //解析接收到的 JSON 消息
            Map<String, String> messageMap = objectMapper.readValue(message, Map.class);
            targetSid = messageMap.get("targetSid");
            msgContent = messageMap.get("message");
        } catch (IOException e) {
            log.error("消息解析失败", e);
            return;
        }
 
        //判断目标用户是否为管理员且管理员不在线
        boolean isTargetAdmin = isAdmin(targetSid);
        if (isTargetAdmin && !isAdminOnline()) {
            log.info("管理员不在线,调用文心一言进行自动回复");
            String wenxinResponse = getWenxinResponse(msgContent);
            if (wenxinResponse != null) {
                log.info("文心一言返回的回复: " + wenxinResponse);
                Map<String, String> responseMap = new HashMap<>();
                responseMap.put("sourceSid", sid);
                responseMap.put("message", wenxinResponse);
 
                String jsonResponse;
                try {
                    //将回复消息转换为 JSON 格式
                    jsonResponse = objectMapper.writeValueAsString(responseMap);
                } catch (IOException e) {
                    log.error("JSON 序列化失败", e);
                    return;
                }
 
                //发送自动回复消息给发送方
                try {
                    sendMessage(jsonResponse);
                    log.info("发送自动回复消息: " + jsonResponse);
                } catch (IOException e) {
                    log.error("消息发送失败", e);
                }
                return;
            }
        }
 
 
        //如果管理员在线或者不是管理员,按照正常逻辑发送消息
        Map<String, String> responseMap = new HashMap<>();
        responseMap.put("sourceSid", sid);
        responseMap.put("message", msgContent);
 
        String jsonResponse;
        try {
            //将消息转换为 JSON 格式
            jsonResponse = objectMapper.writeValueAsString(responseMap);
        } catch (IOException e) {
            log.error("JSON 序列化失败", e);
            return;
        }
 
        //将消息发送给目标 sid
        for (WebSocketServer item : webSocketSet) {
            try {
                if (targetSid.equals(item.sid)) {
                    item.sendMessage(jsonResponse);
                    break;
                }
            } catch (IOException e) {
                log.error("消息发送失败", e);
            }
        }
    }
 
 
 
    /**
     * 判断是否是管理员
     */
    private boolean isAdmin(String sid) {
        return "admin".equals(sid);
    }
 
    /**
     * 发生错误时调用的方法
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误", error);
    }
 
    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }
 
    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
        log.info("推送消息到窗口 " + sid + ",推送内容: " + message);
 
        for (WebSocketServer item : webSocketSet) {
            try {
                if (sid == null) {
                    item.sendMessage(message); //推送给所有人
                } else if (item.sid.equals(sid)) {
                    item.sendMessage(message); //推送给指定 sid
                }
            } catch (IOException e) {
                log.error("推送消息失败", e);
            }
        }
    }
 
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
 
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }
 
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
 
    public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
        return webSocketSet;
    }
 
    public String getSid() {
        return this.sid;
    }
 
    private boolean isAdminOnline() {
        for (WebSocketServer item : webSocketSet) {
            if (isAdmin(item.sid)) {
                log.info("管理员已在线: " + item.sid);
                return true;
            }
        }
        log.info("管理员不在线");
        return false;
    }
 
 
 
 
    private String getWenxinResponse(String query) {
        try {
            //调用WenXinYiYanUtil类的静态方法获取回复
            String wenxinReplyJson = WenXinYiYanUtil.getWenxinReply(query);
 
            //将文心一言回复的JSON字符串解析为JSONObject
            JSONObject wenxinReplyObj = JSONObject.parseObject(wenxinReplyJson);
 
            //提取出要展示给用户的回复内容
            String result = wenxinReplyObj.getString("result");
 
            return result;
        } catch (IOException | JSONException e) {
            log.error("调用文心一言失败", e);
            return null;
        }
    }
 
}

**作用:**使用 @ServerEndpoint 注解声明了 WebSocket 端点,指定了路径 /api/websocket/{sid},其中 {sid} 是一个动态路径参数,代表每个连接的唯一标识。

WebSocket 事件处理:

@OnOpen: 连接建立时调用的方法。每当一个新的 WebSocket 连接建立时,执行此方法,并将当前连接的 sid 和 session 保存到 webSocketSet 中,同时增加在线人数。

发送一个 JSON 格式的 "conn_success" 消息给客户端,表示连接成功。

@OnClose: 连接关闭时调用的方法。每当一个 WebSocket 连接关闭时,执行此方法,并从 webSocketSet 中移除该连接,同时减少在线人数。

@OnMessage: 收到客户端消息时调用的方法。该方法解析接收到的消息并根据目标用户的 sid 将消息发送给目标客户端。如果目标用户是管理员且管理员不在线,系统会通过 WenXinYiYanUtil 获取自动回复,进行自动响应。

@OnError: 发生错误时调用的方法,日志记录错误信息。

消息发送:

sendMessage: 该方法用于向客户端发送消息,利用 session.getBasicRemote().sendText() 实现。

sendInfo: 该方法用于群发消息,向所有连接的客户端发送自定义的消息。可以根据传入的 sid 进行定向推送。

二、 调用文心一言大模型API

要与文心一言大模型API进行集成,你需要访问其官方文档并获取API密钥,通常通过HTTP请求与API交互。

步骤:
1.在百度智能云开放平台中注册成为开发者

官网:全面解读 百度智能云 API 服务商 -- 服务商门户 -- 幂简集成

2.进入百度智能云官网进行登录,点击立即体验

3.进入千帆ModelBuilder,点击左侧的应用接入并且点击创建应用

4.在页面上的应用名称输入自己想要的应用名称和应用描述

5.获取对应的API Key 和 Secret Key

6.配置文心一言ERNIE4.0 API并调用,选择一个想要使用的模型

7.添加依赖

XML 复制代码
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.3</version>
</dependency>

8.我们可以参考官方提供的示例代码,编写自己的程序

代码实例

下面这段代码是一个 Java 示例程序,使用 OkHttp 库向文心一言 API 发送请求,并通过用户提供的输入获取模型的响应。

java 复制代码
import okhttp3.*;
import org.json.JSONException;
import org.json.JSONObject;
 
import java.io.*;
import java.util.concurrent.TimeUnit;
 
class Sample {
 
    // 密钥
    public static final String API_KEY = "";
    public static final String SECRET_KEY = "";
 
    // OkHttpClient 配置,设置连接超时和读取超时
    static final OkHttpClient HTTP_CLIENT = new OkHttpClient().newBuilder()
            .connectTimeout(60, TimeUnit.SECONDS)  // 设置连接超时为60秒
            .readTimeout(60, TimeUnit.SECONDS)     // 设置读取超时为60秒
            .build();
 
    public static void main(String[] args) throws IOException, JSONException {
        // 定义请求的媒体类型
        MediaType mediaType = MediaType.parse("application/json");
 
        // 构建请求体,消息内容包含了用户请求
        RequestBody body = RequestBody.create(mediaType, "{\"messages\":["
                + "{\"role\":\"user\",\"content\":\"北京的天气是什么\"}" // 用户输入的消息内容
                + "],"
                + "\"temperature\":0.95,"   // 设置温度参数,控制模型的输出多样性
                + "\"top_p\":0.8,"          // 设置 top_p 参数,控制模型输出的多样性
                + "\"penalty_score\":1,"    // 设置惩罚得分参数,影响模型对重复内容的惩罚
                + "\"enable_system_memory\":false," // 禁用系统内存
                + "\"disable_search\":false,"        // 禁用搜索功能
                + "\"enable_citation\":false}");    // 禁用引用功能
 
        // 构建 HTTP 请求
        Request request = new Request.Builder()
                .url("https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token=" + getAccessToken())
                .method("POST", body)  // 设置请求方法为 POST
                .addHeader("Content-Type", "application/json")  // 设置请求头,表示发送的内容是 JSON 格式
                .build();
 
        // 发送请求并获取响应
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            // 输出响应内容,打印接口返回的数据
            System.out.println(response.body().string());
        } catch (IOException e) {
            // 捕获 IO 异常(如网络错误、超时等),并打印异常信息
            e.printStackTrace();
        }
    }
 
    /**
     * 从用户的 API 密钥(AK、SK)生成鉴权签名(Access Token)
     *
     * @return 鉴权签名(Access Token)
     * @throws IOException 如果发生 I/O 异常
     * @throws JSONException 如果发生 JSON 解析异常
     */
    static String getAccessToken() throws IOException, JSONException {
        // 设置请求体的媒体类型为 x-www-form-urlencoded
        MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
 
        // 创建请求体,包含 API 的 client_id 和 client_secret
        RequestBody body = RequestBody.create(mediaType, "grant_type=client_credentials&client_id=" + API_KEY
                + "&client_secret=" + SECRET_KEY);
 
        // 构建请求,使用 POST 方法获取 Access Token
        Request request = new Request.Builder()
                .url("https://aip.baidubce.com/oauth/2.0/token") // 请求 URL,获取 Access Token
                .method("POST", body)  // 使用 POST 方法发送请求
                .addHeader("Content-Type", "application/x-www-form-urlencoded") // 请求头
                .build();
 
        // 发送请求并获取响应
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            // 从响应中解析出 Access Token
            return new JSONObject(response.body().string()).getString("access_token");
        }
    }
}

解释如下:

这段代码是一个 Java 示例程序,使用 OkHttp 库向文心一言 API 发送请求,并通过用户提供的输入获取模型的响应。具体来说,它包括了两个主要部分:

  • 获取访问令牌(Access Token) :通过 API_KEYSECRET_KEY 获取用于访问文心一言 API 的 Access Token。
  • 发送请求到文心一言 API:使用获取到的 Access Token,向文心一言 API 发送一个包含用户输入的消息,请求模型生成响应。
具体步骤:
  1. 获取 Access Token

getAccessToken() 方法的作用是通过 API_KEYSECRET_KEY 获取一个 Access Token。这个令牌是调用文心一言 API 所必需的。步骤如下:

java 复制代码
static String getAccessToken() throws IOException, JSONException {
    // 设置请求体的媒体类型为 x-www-form-urlencoded
    MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");

    // 创建请求体,包含 API 的 client_id 和 client_secret
    RequestBody body = RequestBody.create(mediaType, "grant_type=client_credentials&client_id=" + API_KEY
            + "&client_secret=" + SECRET_KEY);

    // 构建请求,使用 POST 方法获取 Access Token
    Request request = new Request.Builder()
            .url("https://aip.baidubce.com/oauth/2.0/token") // 请求 URL,获取 Access Token
            .method("POST", body)  // 使用 POST 方法发送请求
            .addHeader("Content-Type", "application/x-www-form-urlencoded") // 请求头
            .build();

    // 发送请求并获取响应
    try (Response response = HTTP_CLIENT.newCall(request).execute()) {
        // 从响应中解析出 Access Token
        return new JSONObject(response.body().string()).getString("access_token");
    }
}
  • 请求体grant_type=client_credentials&client_id=API_KEY&client_secret=SECRET_KEY,请求体中的 client_idclient_secret 是用来鉴权的。
  • 响应解析 :在获取到响应后,程序解析出其中的 access_token,这是后续访问文心一言 API 的凭证。
  1. 发送请求到文心一言 API

接下来的部分是构建并发送请求到文心一言的聊天 API。

java 复制代码
RequestBody body = RequestBody.create(mediaType, "{\"messages\":[" 
        + "{\"role\":\"user\",\"content\":\"北京的天气是什么\"}" // 用户输入的消息内容
        + "],"
        + "\"temperature\":0.95,"   // 设置温度参数,控制模型的输出多样性
        + "\"top_p\":0.8,"          // 设置 top_p 参数,控制模型输出的多样性
        + "\"penalty_score\":1,"    // 设置惩罚得分参数,影响模型对重复内容的惩罚
        + "\"enable_system_memory\":false," // 禁用系统内存
        + "\"disable_search\":false,"        // 禁用搜索功能
        + "\"enable_citation\":false}");    // 禁用引用功能
  • 请求体 :请求体是一个 JSON 字符串,其中包含了用户的消息(例如:"北京的天气是什么"),以及一些参数,如 temperature, top_p 等,控制生成的回答的多样性和随机性。
    • "messages": 包含一个消息对象,其中 "role""user",表示消息来源是用户,"content" 是用户的实际问题。
    • "temperature": 控制模型生成内容的多样性,值越高,生成的内容越随机,通常在 0 到 1 之间。0.95 是一个比较高的值,表示会有更多的随机性。
    • "top_p": 也是控制生成内容多样性的参数,决定了模型从哪些概率分布中选择内容。0.8 表示在一个给定的概率质量上选取词汇。
    • "penalty_score": 惩罚得分,影响模型对重复内容的惩罚,值越高,模型更倾向于避免重复。
    • "enable_system_memory", "disable_search", "enable_citation": 这些选项是与文心一言 API 功能相关的配置项,用于启用或禁用某些功能。
java 复制代码
Request request = new Request.Builder()
        .url("https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token=" + getAccessToken())
        .method("POST", body)  // 设置请求方法为 POST
        .addHeader("Content-Type", "application/json")  // 设置请求头,表示发送的内容是 JSON 格式
        .build();
  • 请求 URL :请求的 URL 中包含了获取到的 access_token,它会用来验证请求的合法性。
  • 请求方法POST,表示发送的数据(如用户消息)需要上传到服务器。
  • 请求头Content-Type 设置为 application/json,表示请求体的格式是 JSON。
java 复制代码
try (Response response = HTTP_CLIENT.newCall(request).execute()) {
    // 输出响应内容,打印接口返回的数据
    System.out.println(response.body().string());
} catch (IOException e) {
    // 捕获 IO 异常(如网络错误、超时等),并打印异常信息
    e.printStackTrace();
}
  • 发送请求 :调用 HTTP_CLIENT.newCall(request).execute() 发送请求,并获取服务器的响应。
  • 打印响应:如果请求成功,打印返回的响应内容,通常是文心一言的回答。
3. 整体流程
  1. 程序启动后,调用 getAccessToken() 方法获取 Access Token。
  2. 使用获取到的 Access Token,构建一个带有用户消息的请求,发送到文心一言 API。
  3. 接收文心一言的响应(比如对用户问题的回答),并打印在控制台。
4. 需要的外部依赖
  • OkHttp:用于发送 HTTP 请求,处理网络连接。
  • JSON库 :用于解析 JSON 数据,这里使用的是 org.json 库(JSONObject 类)。

我们也可以使用RestTemplateWebClient来调用文心一言API,具体怎么使用大家可以自行探索。

三、 前端WebSocket连接

在前端,你可以使用JavaScript的WebSocket API来连接后端WebSocket服务并进行通信。

前端代码示例:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实时聊天</title>
</head>
<body>
    <div>
        <input type="text" id="inputMessage" placeholder="请输入消息">
        <button onclick="sendMessage()">发送</button>
    </div>
    <div id="chatBox"></div>

    <script>
        // 创建WebSocket连接
        const socket = new WebSocket("ws://localhost:8080/api/websocket/+sid");

        // 监听消息
        socket.onmessage = function(event) {
            const chatBox = document.getElementById("chatBox");
            const message = event.data;
            chatBox.innerHTML += `<p>机器人: ${message}</p>`;
        };

        // 发送消息到WebSocket
        function sendMessage() {
            const inputMessage = document.getElementById("inputMessage").value;
            socket.send(inputMessage);

            const chatBox = document.getElementById("chatBox");
            chatBox.innerHTML += `<p>你: ${inputMessage}</p>`;
        }
    </script>
</body>
</html>

参考文章:

实现 WebSocket 接入文心一言_apipost websocket-CSDN博客

文心一言ERNIE-Bot 4.0模型流式和非流式API调用(SpringBoot+OkHttp3+SSE+WebSocket) - autunomy - 博客园

相关推荐
JH30739 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
qq_124987075312 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_12 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_8187320613 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
杨了个杨898213 小时前
memcached部署
qt·websocket·memcached
汤姆yu16 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶16 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip17 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide18 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf18 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端