智谱清言 AI 通用大语言模型 ChatGLM Java SDK – Github

此项目是由 JavaJDK11 的长期版本开发,设备环境需要 JDK >= 11


当前 ChatGLM Java SDK 最新为 0.1.1 Beta 版本。

Java Maven Dependency (BlueChatGLM)调用

top.pulselinkbluechatglm0.1.1-Beta

Java Gradle (BlueChatGLM)调用

implementation group: 'top.pulselink', name: 'bluechatglm', version: '0.1.1-Beta'

Java sbt (BlueChatGLM)调用

libraryDependencies += "top.pulselink" % "bluechatglm" % "0.1.1-Beta"

1. Utils 工具

1.1 NTP 网络时间服务器

它通过互联网或局域网上的时间服务器来提供高精度,高安全的时间信息,确保所有设备都使用相同的时间是关键的。这里的应用是对于 JWT 验证使用

//获取网络时间协议服务器(NTP Server)private long getNTPTime() throws IOException {int port = 123;InetAddress address = InetAddress.getByName("ntp.aliyun.com");try (DatagramSocket socket = new DatagramSocket()) {byte[] buf = new byte[48];buf[0] = 0x1B;DatagramPacket requestPacket = new DatagramPacket(buf, buf.length, address, port);socket.send(requestPacket);DatagramPacket responsePacket = new DatagramPacket(new byte[48], 48);socket.receive(responsePacket);byte[] rawData = responsePacket.getData();long secondsSince1900 = 0;for (int i = 40; i <= 43; i++) {secondsSince1900 = (secondsSince1900 << 8) | (rawData[i] & 0xff);}return (secondsSince1900 - 2208988800L) * 1000;}}

1.2 保存 API 密钥

保存 API 密钥并将其存储在调用 chatglm_api_key.txt 文件的本地文件中:

private static String loadApiKey() {//加载 API 密钥try (BufferedReader reader = new BufferedReader(new FileReader(API_KEY_FILE))) {return reader.readLine();} catch (IOException e) {return null; // If the file doesn't exist or an error occurs, return null}}private static void saveApiKey(String apiKey) { //保存 API 密钥try (BufferedWriter writer = new BufferedWriter(new FileWriter(API_KEY_FILE))) {writer.write(apiKey);} catch (IOException e) {e.printStackTrace(); // Handle the exception according to your needs}}

1.3 保存聊天内容文件

用户聊天和 ChatGLM 回复将保存在chatglm_history.txt 中,聊天内容 txt 文件将在每个会话结束时删除。

private void createHistoryFileIfNotExists() {//检查是否存在文件Path filePath = Paths.get(historyFilePath);if (Files.exists(filePath)) {try {Files.delete(filePath);} catch (IOException e) {e.printStackTrace();}}try {Files.createFile(filePath);} catch (IOException e) {e.printStackTrace();}}private void registerShutdownHook() {//关闭程序的时候删除历史聊天记录Runtime.getRuntime().addShutdownHook(new Thread(() -> {try {Files.deleteIfExists(Paths.get(historyFilePath));} catch (IOException e) {e.printStackTrace();}}));}

2. 易于使用的 SDK

2.1 易于调用且使用的 Java Maven 库

相对于很多人来说,使用这个 SDK 的难度较低。以下的三个示例是使用 Scanner 输入你的问题,控制台将输出 ChatGLM 回答:

调用SSE请求,示例代码如下 (目前已解决无法输入中文等问题,可以正常使用):

public static void main(String[] args) {Scanner scanner = new Scanner(System.in);String apiKeyss = loadApiKey();//加载 API 密钥if (apiKeyss == null) {//如果不存在文件或者密钥为空,则需要输入密钥System.out.println("Enter your API key:");apiKeyss = scanner.nextLine();saveApiKey(apiKeyss);}while (scanner.hasNext()) {String userInput = scanner.nextLine(); ChatClient chats = new ChatClient(apiKeyss);//初始 ChatClient (实例化) chats.registerShutdownHook(); //删除聊天的历史文件 chats.SSEInvoke(userInput);//将你输入的问题赋值给流式请求的 System.out.print(chats.getResponseMessage()); //打印出 ChatGLM 的回答内容System.out.println();}}

调用异步请求,示例代码如下:

public static void main(String[] args) {Scanner scanner = new Scanner(System.in);String apiKeyss = loadApiKey();//加载 API 密钥if (apiKeyss == null) {//如果不存在文件或者密钥为空,则需要输入密钥System.out.println("Enter your API key:");apiKeyss = scanner.nextLine();saveApiKey(apiKeyss);}while (scanner.hasNext()) {String userInput = scanner.nextLine(); ChatClient chats = new ChatClient(apiKeyss);//初始 ChatClient (实例化) chats.registerShutdownHook(); //删除聊天的历史文件 chats.AsyncInvoke(userInput);//将你输入的问题赋值给异步请求的 System.out.print(chats.getResponseMessage()); //打印出 ChatGLM 的回答内容System.out.println();}}

调用同步请求,示例代码如下:

public static void main(String[] args) {Scanner scanner = new Scanner(System.in);String apiKeyss = loadApiKey();//加载 API 密钥if (apiKeyss == null) {//如果不存在文件或者密钥为空,则需要输入密钥System.out.println("Enter your API key:");apiKeyss = scanner.nextLine();saveApiKey(apiKeyss);}while (scanner.hasNext()) {String userInput = scanner.nextLine(); ChatClient chats = new ChatClient(apiKeyss);//初始 ChatClient (实例化) chats.registerShutdownHook(); //删除聊天的历史文件 chats.SyncInvoke(userInput);//将你输入的问题赋值给同步请求的 System.out.print(chats.getResponseMessage()); //打印出 ChatGLM 的回答内容System.out.println();}}

2.2 资深开发者‍

对于资深开发者,我们会后续跟进开发工作,目前的版本是 ChatGLM 4 的语言模型版本,并且已经解决了SSE中文输入看不懂的问题,当然我们也希望其他的开发商为本项目提供技术支持! 先感谢您!


3.项目介绍

CustomJWT 是对于这个项目的自定制而写的,后期会继续开发更新,拓展这个项目

根据 JWT.io 这个网站进行了解以及原理的学习,对于这个项目的JWT 验证,Java实现起来还是较容易实现的,其中使用的部分是 Base64Url 而不是常规的 Base64

编码 Base64Url 使用的编辑如下:

private String encodeBase64Url(byte[] data) {String base64url = Base64.getUrlEncoder().withoutPadding().encodeToString(data);//将输入的内容转换成 Base64Urlreturn base64url; //返回 base64url}

创建 JWT,实现 Header 验证:

protected String createJWT() {String encodedHeader = encodeBase64Url(header.getBytes());String encodedPayload = encodeBase64Url(payload.getBytes());String toSign = encodedHeader + "." + encodedPayload;byte[] signatureBytes = generateSignature(toSign, secret, algorithm);String calculatedSignature = encodeBase64Url(signatureBytes);return toSign + "." + calculatedSignature;}

验证 JWT 签名部分是否与输出的结果一致:

protected boolean verifyJWT(String jwt) {jwt = jwt.trim();String[] parts = jwt.split("\\.");if (parts.length != 3) {return false;}String encodedHeader = parts[0];String encodedPayload = parts[1];String signature = parts[2];String toVerify = encodedHeader + "." + encodedPayload;byte[] calculatedSignatureBytes = generateSignature(toVerify, secret, algorithm);String calculatedSignature = encodeBase64Url(calculatedSignatureBytes);return calculatedSignature.equals(signature);} 

请求调用

同步请求SSE请求中使用的请求方式如下:

HttpRequest request = HttpRequest.newBuilder().uri(URI.create(apiUrl)).header("Accept", "application/json").header("Content-Type", "application/json;charset=UTF-8").header("Authorization", "Bearer " + token).POST(HttpRequest.BodyPublishers.ofString(jsonRequestBody)).build();

使用 POST 方法制作 jsonRequestBody ,如下文所示(同步方法的 Stream 为 false):

String jsonRequestBody = String.format("{\"model\":\"%s\", \"messages\":[{\"role\":\"%s\",\"content\":\"%s\"},{\"role\":\"%s\",\"content\":\"%s\"}], \"stream\":false,\"temperture\":%f,\"top_p\":%f}", Language_Model, system_role, system_content, user_role, message, temp_float, top_p_float);
SSE 流式传输模型(可以正常使用!完美支持)

这里我们将使用 concurrent.Flow 方法来解决SSE流处理的问题:

public class SSESubscriber implements Flow.Subscriber {private Flow.Subscription subscription;@Overridepublic void onSubscribe(Flow.Subscription subscription) {this.subscription = subscription;subscription.request(1);}@Overridepublic void onNext(String item) {if (item.startsWith("data: ")) {String jsonData = item.substring("data: ".length());//System.out.println("Received SSE item: " + jsonData); //Debugif (!jsonData.equals("[DONE]")) {responseDataBody(jsonData.replaceAll("Invalid JSON format: \\[\"DONE\"\\]", ""));}}subscription.request(1);}@Overridepublic void onError(Throwable throwable) {System.out.println("Error in SSESubscriber: " + throwable.getMessage());}@Overridepublic void onComplete() {//System.out.println("SSESubscriber completed");}}

并在此处调用并接收 chatglm-4 消息:

try (JsonReader jsonReader = new JsonReader(new StringReader(responseData))) {jsonReader.setLenient(true);JsonElement jsonElement = JsonParser.parseReader(jsonReader);if (jsonElement.isJsonObject()) {JsonObject jsonResponse = jsonElement.getAsJsonObject();if (jsonResponse.has("choices")) {JsonArray choices = jsonResponse.getAsJsonArray("choices");if (!choices.isEmpty()) {JsonObject choice = choices.get(0).getAsJsonObject();if (choice.has("delta")) {JsonObject delta = choice.getAsJsonObject("delta");if (delta.has("content")) {String content = delta.get("content").getAsString();getMessage = convertUnicodeEmojis(content);getMessage = getMessage.replaceAll("\"", "").replaceAll("\\\\n\\\\n", "\n").replaceAll("\\\\nn", "\n").replaceAll("\\n", "\n").replaceAll("\\\\", "").replaceAll("\\\\", "");for (char c : getMessage.toCharArray()) {charQueue.offer(c);}while (!charQueue.isEmpty()) {queueResult.append(charQueue.poll());}}}}}} else {System.out.println("Invalid JSON format: " + jsonElement);}} catch (IOException e) {System.out.println("Error reading JSON: " + e.getMessage());}
异步请求传输模型(AsyncInvokeModel:推荐使用,速度快)

这里采用的是HTTPRequest方法,来接收消息:

String jsonRequestBody = String.format("{\"model\":\"%s\", \"messages\":[{\"role\":\"%s\",\"content\":\"%s\"},{\"role\":\"%s\",\"content\":\"%s\"}],\"temperture\":%f,\"top_p\":%f}",Language_Model, system_role, system_content, user_role, message, temp_float, top_p_float);HttpRequest request = HttpRequest.newBuilder().uri(URI.create(apiUrl)).header("Accept", "application/json").header("Content-Type", "application/json;charset=UTF-8").header("Authorization", "Bearer " + token).POST(HttpRequest.BodyPublishers.ofString(jsonRequestBody)).build();

整体使用的是异步发送信息,这样的好处是可以减少线程阻塞,这里的codestatus是获取错误消息。当你得到一个request_id 的时候,再进行查询

if (response.statusCode() == 200) {//When the response value is 200, output the corresponding parameters of the interface for an asynchronous request.processResponseData(response.body());return CompletableFuture.completedFuture(response.body());} else {JsonObject errorResponse = JsonParser.parseString(response.body()).getAsJsonObject();if (errorResponse.has("id") && errorResponse.has("task_status")) {int code = errorResponse.get("id").getAsInt();String status = errorResponse.get("task_status").getAsString();throw new RuntimeException("HTTP request failure, Your request id is: " + code + ", Status: " + status);} else {return CompletableFuture.failedFuture(new RuntimeException("HTTP request failure, Code: " + response.statusCode()));}}});

当你得到需要的Task_id的时候,进行GET请求查询(部分代码):

.....略 .sendAsync(HttpRequest.newBuilder().uri(URI.create(checkUrl + ID)).header("Accept", "application/json").header("Content-Type", "application/json;charset=UTF-8").header("Authorization", "Bearer " + token).GET().build(), HttpResponse.BodyHandlers.ofString()).thenCompose(response -> {if (response.statusCode() == 200) {return CompletableFuture.completedFuture(response.body());} else {return CompletableFuture.failedFuture(new RuntimeException("HTTP request failure, Code: " + response.statusCode()));}});

最后通过JSON的提取,提取代码示例为:

try {JsonObject jsonResponse = JsonParser.parseString(responseData).getAsJsonObject();if (jsonResponse.has("choices")) {JsonArray choices = jsonResponse.getAsJsonArray("choices");if (!choices.isEmpty()) {JsonObject choice = choices.get(0).getAsJsonObject();if (choice.has("message")) {JsonObject message = choice.getAsJsonObject("message");if (message.has("content")) {String content = message.get("content").getAsString();getMessage = convertUnicodeEmojis(content);getMessage = getMessage.replaceAll("\"", "").replaceAll("\\\\n\\\\n", "\n").replaceAll("\\\\nn", "\n").replaceAll("\\n", "\n").replaceAll("\\\\", "").replaceAll("\\\\", "");}}}}} catch (JsonSyntaxException e) {System.out.println("Error processing task status: " + e.getMessage());}
同步请求传输模型(InvokeModel:推荐使用,速度较快)

同步请求还算不错,运行的时候一般情况下都还算快,当然同步的缺点就是请求量过大可能会阻塞线程(单线程

这里直接说明关于处理信息这一块,这一块就是解析 JSON 也没有其他的东西了,示例代码:

try {JsonObject jsonResponse = JsonParser.parseString(responseData).getAsJsonObject();if (jsonResponse.has("choices")) {JsonArray choices = jsonResponse.getAsJsonArray("choices");if (!choices.isEmpty()) {JsonObject choice = choices.get(0).getAsJsonObject();if (choice.has("message")) {JsonObject message = choice.getAsJsonObject("message");if (message.has("content")) {String content = message.get("content").getAsString();getMessage = convertUnicodeEmojis(content);getMessage = getMessage.replaceAll("\"", "").replaceAll("\\\\n\\\\n", "\n").replaceAll("\\\\nn", "\n").replaceAll("\\n", "\n").replaceAll("\\\\", "").replaceAll("\\\\", "");}}}}} catch (JsonSyntaxException e) {System.out.println("Error processing task status: " + e.getMessage());}

总体下来,介绍本项目三种请求方式应该还是相对简单,如果有任何问题,可以在 Issue 处发起讨论,也希望各路大神的对这个项目的支援!再次感谢!


4.结语

感谢您打开我的项目,这是一个自主开发 ChatGLM Java SDK 的开发项目,为了解决官方的 SDK 存在的问题我也在努力开发和更新这个项目,当然我个人也会继续开发这个项目,我也比较坚持开源的原则,毕竟白嫖不香嘛(doge)。最后也希望越来越多的人一起参与开发 ,最后如果喜欢的朋友,可以打开我的这个 ChatGLM Java SDK 项目 帮我点个 ⭐️ ,谢谢大家看到最后!


最后的最后感恩 gson 的 jar 包开发人员‍‍