基于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来实现实时聊天,可以分为几个主要步骤:
- 搭建WebSocket服务端
- 与文心一言大模型API对接
- 前端WebSocket连接
一、 搭建WebSocket服务端(使用Java)
我们可以使用Spring Boot
来搭建一个WebSocket服务端。
(1)添加依赖: 在pom.xml
中添加Spring Boot WebSocket的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
(2)配置WebSocket连接: 创建一个WebSocket配置类来启用WebSocket功能:
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 类中处理连接、消息接收和发送。
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.添加依赖
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
8.我们可以参考官方提供的示例代码,编写自己的程序
代码实例
下面这段代码是一个 Java 示例程序,使用 OkHttp
库向文心一言 API 发送请求,并通过用户提供的输入获取模型的响应。
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_KEY
和SECRET_KEY
获取用于访问文心一言 API 的 Access Token。 - 发送请求到文心一言 API:使用获取到的 Access Token,向文心一言 API 发送一个包含用户输入的消息,请求模型生成响应。
具体步骤:
1. 获取 Access Token
getAccessToken()
方法的作用是通过 API_KEY
和 SECRET_KEY
获取一个 Access Token。这个令牌是调用文心一言 API 所必需的。步骤如下:
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_id
和client_secret
是用来鉴权的。 - 响应解析:在获取到响应后,程序解析出其中的
access_token
,这是后续访问文心一言 API 的凭证。
2. 发送请求到文心一言 API
接下来的部分是构建并发送请求到文心一言的聊天 API。
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 功能相关的配置项,用于启用或禁用某些功能。
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。
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. 整体流程
- 程序启动后,调用
getAccessToken()
方法获取 Access Token。 - 使用获取到的 Access Token,构建一个带有用户消息的请求,发送到文心一言 API。
- 接收文心一言的响应(比如对用户问题的回答),并打印在控制台。
4. 需要的外部依赖
- OkHttp:用于发送 HTTP 请求,处理网络连接。
- JSON库:用于解析 JSON 数据,这里使用的是
org.json
库(JSONObject
类)。
我们也可以使用RestTemplate
或WebClient
来调用文心一言API,具体怎么使用大家可以自行探索。
三、 前端WebSocket连接
在前端,你可以使用JavaScript的WebSocket API来连接后端WebSocket服务并进行通信。
前端代码示例:
<!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>
参考文章: