前言
我在前一段時間突發(fā)奇想,就使用java來調用chatgpt的接口,然后寫了一個簡單小程序,也上了熱榜第一,java調用chatgpt接口,實現專屬于自己的人工智能助手,事實上,這個程序毛病挺多的,最不能讓人接受的一點就是返回速度非常緩慢(即使使用非常好的外網服務器)。
現在,我改進了一下程序,使用異步請求的方式,基本可以實現秒回復。并且還基于webSocket編寫了一個微信小程序來進行交互,可以直接使用微信小程序來進行體驗。
現在我將所有代碼都上傳了github(鏈接在文章結尾),大家可以clone下來,部署到服務器上,真正實現自己的聊天機器人?。?!
效果展示
部分截圖如下
原理說明
在 java調用chatgpt接口,實現專屬于自己的人工智能助手 我說明了java調用chatgpt的基本原理,這里的代碼就是對這個代碼的改進,使用異步請求的方式來進行。
注意看官方文檔,我們在請求時可以提供一個參數stream,然后就可以實現按照流的形式進行返回,這種方式基本可以做到沒有延遲就給出答案。
由于這次改進的思路主要就是將請求改為了異步,其他的基本一樣,所以就不做解釋,直接給出代碼了,代碼上面都有注釋
/**
* 這個方法用于測試的,可以在控制臺打印輸出結果
*
* @param chatGptRequestParameter 請求的參數
* @param question 問題
*/
public void printAnswer(ChatRequestParameter chatGptRequestParameter, String question) {
asyncClient.start();
// 創(chuàng)建一個post請求
AsyncRequestBuilder asyncRequest = AsyncRequestBuilder.post(url);
// 設置請求參數
chatGptRequestParameter.addMessages(new ChatMessage("user", question));
// 請求的參數轉換為字符串
String valueAsString = null;
try {
valueAsString = objectMapper.writeValueAsString(chatGptRequestParameter);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 設置編碼和請求參數
ContentType contentType = ContentType.create("text/plain", charset);
asyncRequest.setEntity(valueAsString, contentType);
asyncRequest.setCharset(charset);
// 設置請求頭
asyncRequest.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
// 設置登錄憑證
asyncRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey);
// 下面就是生產者消費者模型
CountDownLatch latch = new CountDownLatch(1);
// 用于記錄返回的答案
StringBuilder sb = new StringBuilder();
// 消費者
AbstractCharResponseConsumer<HttpResponse> consumer = new AbstractCharResponseConsumer<HttpResponse>() {
HttpResponse response;
@Override
protected void start(HttpResponse response, ContentType contentType) throws HttpException, IOException {
setCharset(charset);
this.response = response;
}
@Override
protected int capacityIncrement() {
return Integer.MAX_VALUE;
}
@Override
protected void data(CharBuffer src, boolean endOfStream) throws IOException {
// 收到一個請求就進行處理
String ss = src.toString();
// 通過data:進行分割,如果不進行此步,可能返回的答案會少一些內容
for (String s : ss.split("data:")) {
// 去除掉data:
if (s.startsWith("data:")) {
s = s.substring(5);
}
// 返回的數據可能是(DONE)
if (s.length() > 8) {
// 轉換為對象
ChatResponseParameter responseParameter = objectMapper.readValue(s, ChatResponseParameter.class);
// 處理結果
for (Choice choice : responseParameter.getChoices()) {
String content = choice.getDelta().getContent();
if (content != null && !"".equals(content)) {
// 保存結果
sb.append(content);
// 將結果使用webSocket傳送過去
System.out.print(content);
}
}
}
}
}
@Override
protected HttpResponse buildResult() throws IOException {
return response;
}
@Override
public void releaseResources() {
}
};
// 執(zhí)行請求
asyncClient.execute(asyncRequest.build(), consumer, new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse response) {
latch.countDown();
chatGptRequestParameter.addMessages(new ChatMessage("assistant", sb.toString()));
System.out.println("回答結束?。?!");
}
@Override
public void failed(Exception ex) {
latch.countDown();
System.out.println("failed");
ex.printStackTrace();
}
@Override
public void cancelled() {
latch.countDown();
System.out.println("cancelled");
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
大家代碼可以直接不看,反正最終的效果就是可以實現問了問題就返回結果。運行效果如下
可以發(fā)現,輸出就類似于官方的那種效果,一個字一個字的輸出
服務器端代碼說明
我使用java搭建了一個簡單的服務器端程序,提供最基礎的用戶登錄校驗功能,以及提供了WebSocket通信。
用戶校驗的代碼
package com.ttpfx.controller;
import com.ttpfx.entity.User;
import com.ttpfx.service.UserService;
import com.ttpfx.utils.R;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author ttpfx
* @date 2023/3/29
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
public static ConcurrentHashMap<String, User> loginUser = new ConcurrentHashMap<>();
public static ConcurrentHashMap<String, Long> loginUserKey = new ConcurrentHashMap<>();
@RequestMapping("/login")
public R login(String username, String password) {
if (username == null) return R.fail("必須填寫用戶名");
User user = userService.queryByName(username);
if (user == null) return R.fail("用戶名不存在");
String targetPassword = user.getPassword();
if (targetPassword == null) return R.fail("用戶密碼異常");
if (!targetPassword.equals(password)) return R.fail("密碼錯誤");
loginUser.put(username, user);
loginUserKey.put(username, System.currentTimeMillis());
return R.ok(String.valueOf(loginUserKey.get(username)));
}
@RequestMapping("/logout")
public R logout(String username) {
loginUser.remove(username);
loginUserKey.remove(username);
return R.ok();
}
@RequestMapping("/checkUserKey")
public R checkUserKey(String username, Long key){
if (username==null || key == null)return R.fail("用戶校驗異常");
if (!Objects.equals(loginUserKey.get(username), key)){
return R.fail("用戶在其他地方登錄?。?!");
}
return R.ok();
}
@RequestMapping("/loginUser")
public R loginUser(){
return R.ok("success",loginUser.keySet());
}
}
基于webSocket通信的代碼
package com.ttpfx.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ttpfx.entity.UserLog;
import com.ttpfx.model.ChatModel;
import com.ttpfx.service.UserLogService;
import com.ttpfx.service.UserService;
import com.ttpfx.vo.chat.ChatRequestParameter;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author ttpfx
* @date 2023/3/28
*/
@Component
@ServerEndpoint("/chatWebSocket/{username}")
public class ChatWebSocketServer {
/**
* 靜態(tài)變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的線程安全Map,用來存放每個客戶端對應的MyWebSocket對象。
*/
private static ConcurrentHashMap<String, ChatWebSocketServer> chatWebSocketMap = new ConcurrentHashMap<>();
/**
* 與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數據
*/
private Session session;
/**
* 接收的username
*/
private String username = "";
private UserLog userLog;
private static UserService userService;
private static UserLogService userLogService;
@Resource
public void setUserService(UserService userService) {
ChatWebSocketServer.userService = userService;
}
@Resource
public void setUserLogService(UserLogService userLogService) {
ChatWebSocketServer.userLogService = userLogService;
}
private ObjectMapper objectMapper = new ObjectMapper();
private static ChatModel chatModel;
@Resource
public void setChatModel(ChatModel chatModel) {
ChatWebSocketServer.chatModel = chatModel;
}
ChatRequestParameter chatRequestParameter = new ChatRequestParameter();
/**
* 建立連接
* @param session 會話
* @param username 連接用戶名稱
*/
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
this.session = session;
this.username = username;
this.userLog = new UserLog();
// 這里的用戶id不可能為null,出現null,那么就是非法請求
try {
this.userLog.setUserId(userService.queryByName(username).getId());
} catch (Exception e) {
e.printStackTrace();
try {
session.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
this.userLog.setUsername(username);
chatWebSocketMap.put(username, this);
onlineCount++;
System.out.println(username + "--open");
}
@OnClose
public void onClose() {
chatWebSocketMap.remove(username);
System.out.println(username + "--close");
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println(username + "--" + message);
// 記錄日志
this.userLog.setDateTime(LocalDateTime.now());
this.userLog.setPreLogId(this.userLog.getLogId() == null ? -1 : this.userLog.getLogId());
this.userLog.setLogId(null);
this.userLog.setQuestion(message);
long start = System.currentTimeMillis();
// 這里就會返回結果
String answer = chatModel.getAnswer(session, chatRequestParameter, message);
long end = System.currentTimeMillis();
this.userLog.setConsumeTime(end - start);
this.userLog.setAnswer(answer);
userLogService.save(userLog);
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static void sendInfo(String message, String toUserId) throws IOException {
chatWebSocketMap.get(toUserId).sendMessage(message);
}
}
我們只需要編寫簡單的前端代碼,就可以實現和后端的socket通信。對于后端,我們只需要改一下apiKey和數據庫配置就可以直接運行了。
微信小程序代碼說明
我寫了一個簡單微信小程序來和后端進行通信,界面如下
大家只需要下載源代碼,然將程序中的ip改為自己服務器的ip即可
代碼鏈接
github的地址為 https://github.com/c-ttpfx/chatgpt-java-wx
可以直接使用 git clone https://github.com/c-ttpfx/chatgpt-java-wx.git 下載代碼到本地
我在github里面說明了安裝使用的基本步驟,大家按照步驟使用即可
總結
上面聊天小程序就是我花2天寫出來的,可能會有一些bug,我自己測試的時候倒是沒有怎么遇到bug,聊天和登錄功能都能正常使用。
對于微信小程序,由于我不是專業(yè)搞前端的,就只東拼西湊實現了最基本的功能(登錄、聊天),大家可以自己寫一個,反正后端接口都提供好了嘛,也不是很難,不想寫也可以將就使用我的。
最后,也是最重要的,大家?guī)臀业拇astar一下!??! 感謝大家了(≥▽≤)/(≥▽≤)/文章來源:http://www.zghlxwxcb.cn/news/detail-778962.html
更新日志
2023/5/13 14:42更新
對代碼進行了重構,最新的代碼已經支持代理,通過在application.yaml里面進行簡單配置即可使用文章來源地址http://www.zghlxwxcb.cn/news/detail-778962.html
gpt:
proxy:
host: 127.0.0.1
port: 7890
到了這里,關于使用chatgpt實現微信聊天小程序(秒回復),github開源(附帶鏈接)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!