簡介
本文分享一個我前幾個月實現(xiàn)的一個智能聊天系統(tǒng)小項目,包含了java后端,微信小程序端,web頁面端三個子工程。
代碼已經(jīng)全部開源,地址放在了文末。
最近一年,chatGPT的火爆程度,已經(jīng)不需要我再多說了,但是依舊有很多人想用卻用不上,原因大家也都很清楚,因為需要科學(xué)上網(wǎng)才可以訪問,并且注冊也需要綁定海外的銀行卡。那么這就給了很多人賺錢的機會,于是很多套殼類網(wǎng)站層出不窮,只需要簡單寫一下代碼,部署到海外的服務(wù)器上,就可以進行訪問了,并且可以實現(xiàn)和chatgpt官網(wǎng)一樣的效果。然后再通過充值會員,或者購買次數(shù),來賺錢。
當(dāng)然,我這個項目也是很久之前就已經(jīng)實現(xiàn)了,但是并不是為了賺錢,當(dāng)時的想法是,第一是為了練習(xí)編程技術(shù),作為程序員,遇到新鮮的事物,總是會想著嘗試一番。第二是為了方便自己,因為自己平時學(xué)習(xí)或者辦公中,也會經(jīng)常使用chatgpt,但是公司網(wǎng)絡(luò)又不允許我使用官網(wǎng),那么不如自己來套殼一個,然后再提供前端頁面進行交互,不就可以了嗎。
后來,因為網(wǎng)頁版對于手機使用非常不方便,于是就又開發(fā)出一個微信小程序,可以隨時隨地使用了,對我的工作和學(xué)習(xí)幫助還是挺大的,遇到問題就可以直接詢問chatGPT了,而且我預(yù)設(shè)置了多種角色,精心調(diào)試了prompt,來實現(xiàn)特定場景或特定領(lǐng)域的問答機器人。
現(xiàn)在,我決定將這些所有的東西全部開源,毫無保留,大家可以使用代碼進行學(xué)習(xí)或者部署使用,微信小程序端就不建議大家發(fā)布了,現(xiàn)在微信是不允許對接chatGPT的,大概率會審核不通過。我是因為發(fā)布的早,并且沒有做過宣傳,只是自己和身邊人使用,訪問量非常小。
效果圖展示
先來看下效果圖吧,這樣才能更加直觀的展示。vue實現(xiàn)的網(wǎng)頁端和微信小程序端,整體功能是一樣的,只是布局有一點小的差異。
網(wǎng)頁端
可以根據(jù)類別進行劃分,每個類別下有多種角色
聊天頁面,類似微信聊天一樣,左側(cè)是聯(lián)系人列表,右邊是對話框,下方是輸入框。對話框中,自己的輸入在右邊顯示,chatGPT的輸出在左邊顯示。支持markdown格式轉(zhuǎn)換。
微信小程序端
類似的布局,同樣的功能,支持積分扣減
后端項目介紹
后端是使用spring boot進行搭建的, 同時會使用到mysql, mybaits,spring data jpa, redis等組件。
為什么使用spring boot,因為自己是一名java程序員,當(dāng)然對python和go也略知一二,只是對性能要求沒有那么高,并且自己根據(jù)熟悉,所以選擇了java語言,而Spring Boot是一個開源的Java開發(fā)框架,它簡化了Java應(yīng)用程序的開發(fā)和部署過程。相比于傳統(tǒng)的Java開發(fā),Spring Boot具有以下優(yōu)點:
1.簡化配置:Spring Boot提供了自動配置的功能,可以根據(jù)項目的依賴自動配置各種組件,無需手動編寫大量的XML配置文件,大大提高了開發(fā)效率。
2.內(nèi)嵌服務(wù)器:Spring Boot內(nèi)置了常用的服務(wù)器,如Tomcat、Jetty等,可以直接將應(yīng)用程序打包為可執(zhí)行的Jar包或War包,簡化了部署和運行的過程。
3.良好的擴展性:Spring Boot基于Spring框架,可以與其他Spring生態(tài)系統(tǒng)的組件無縫集成,如Spring Data、Spring Security等,拓展了開發(fā)的能力。
搭建Spring Boot項目環(huán)境非常簡單,只需要幾個步驟:
1.在IDE中創(chuàng)建一個新的Spring Boot項目,可以選擇使用Spring Initializr或者直接創(chuàng)建一個空的Maven或Gradle項目。
2.在項目的依賴管理文件(如pom.xml)中,添加Spring Boot的啟動器依賴,例如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.編寫Spring Boot應(yīng)用程序的入口類,例如:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4.在入口類中,可以添加一些配置和組件,例如定義數(shù)據(jù)源、配置日志等。
至此,一個簡單的Spring Boot項目就搭建完成了。接下來,我們將介紹如何集成OpenAI接口實現(xiàn)ChatGPT功能。
為了實現(xiàn)ChatGPT功能,我們需要先了解OpenAI接口的相關(guān)信息。OpenAI是一個人工智能技術(shù)公司,提供了各種自然語言處理的API,其中包括了ChatGPT功能。ChatGPT是一個強大的對話生成模型,可以根據(jù)輸入的對話歷史生成下一句話。
要使用OpenAI接口,我們需要進行以下步驟:
1.注冊O(shè)penAI賬號并獲取API密鑰:在OpenAI官網(wǎng)注冊一個賬號,并獲取API密鑰,用于進行API請求。
2.集成OpenAI接口到Spring Boot項目:添加OpenAI的API依賴到項目中,我這里使用的是另外一個開源項目chatgpt-java,例如:
<dependency>
<groupId>com.unfbx</groupId>
<artifactId>chatgpt-java</artifactId>
<version>1.0.12</version>
</dependency>
3.使用webSocket。
WebSocket是一種在客戶端和服務(wù)器之間實現(xiàn)雙向通信的協(xié)議,通過該協(xié)議可以在服務(wù)端主動向客戶端推送數(shù)據(jù),實現(xiàn)實時的雙向通信。與傳統(tǒng)的HTTP請求-響應(yīng)模式不同,WebSocket的連接一旦建立,客戶端和服務(wù)器可以持久性地保持連接,雙方可以隨時發(fā)送和接收消息,達到實時通信的效果。這種實時通信對于聊天應(yīng)用、實時數(shù)據(jù)展示和協(xié)同編輯等場景非常有用。
(1)添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
(2)建立連接,這里需要參數(shù)用戶id
@OnOpen
public void onOpen(Session session, @PathParam("uid") String uid) {
log.info("websocket open,uid:{}", uid);
this.session = session;
this.uid = uid;
webSocketSet.add(this);
SESSIONS.add(session);
if (chatWebSocketMap.containsKey(uid)) {
chatWebSocketMap.remove(uid);
chatWebSocketMap.put(uid, this);
} else {
chatWebSocketMap.put(uid, this);
addOnlineCount();
}
log.info("websocket onOpen, userId:{}, online count:{}", this.uid, getOnlineCount());
}
(3)接收前端消息,并調(diào)用openAI。
因為websocket只能有定義一個字符串進行前后端交互,所以如果我們需要傳遞多個參數(shù)的話,需要將其轉(zhuǎn)換為json字符串傳遞進來,并在接受后進行解析。如下所示,我們需要將roleId傳遞進來,roleId代表是和哪一個角色進行對話,這個在后面會用到,messages列表,里面存放了歷史對話記錄,這里傳遞進來是為了保持上下文進行通信,創(chuàng)建一個EventSourceListener,并把session傳入進去,最后ChatCompletion 就是與openAI交互的結(jié)構(gòu)體。
@OnMessage
public void onMessage(String message) {
log.info("onMessage, userId:{} ", this.uid);
JSONObject jsonObject = JSONUtil.parseObj(message);
Integer roleId = jsonObject.getInt("roleId");
String messageString = jsonObject.getStr("message");
List<Message> messages = new ArrayList<>();
messages = JSONUtil.toList(messageString, Message.class);
//接受參數(shù)
OpenAIWebSocketEventSourceListener eventSourceListener = new OpenAIWebSocketEventSourceListener(this.session);
ChatCompletion chatCompletion = buildChatCompletion(roleId, messages);
openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener);
}
(4)組裝報文。
我們可以將每一種角色的prompt,使用到的模型,最大token等參數(shù),保存到數(shù)據(jù)庫中,根據(jù)roleId進行獲取,然后組裝成ChatCompletion。其中,message結(jié)構(gòu)體中,包含了role和content,role分為三種,分別是system,user,assistant。其中system是系統(tǒng)指定的,里面可以存放你的prompt,用來告訴chatGPT,它現(xiàn)在要扮演什么角色,要怎么輸出內(nèi)容,都可以在system指令中進行指定。user為用戶輸入的文本。assistant為chatGPT的回答的文本。model中可以指定自己使用的模型,可以使用gpt-3.5-turbo,gpt-4-32k等,根據(jù)自己實際情況進行選擇。
private ChatCompletion buildChatCompletion(int id, List<Message> messages) {
ChatCompletion chatCompletion;
Role role = roleService.getRoleById(id);
if (role != null) {
Message roleMessage = Message.builder().content(role.getRoleMessage())
.role(Message.Role.SYSTEM).build();
messages.add(0, roleMessage);
chatCompletion = ChatCompletion.builder()
.temperature(role.getTemperature())
.model(role.getModel())
.maxTokens(role.getMaxTokens())
.topP(role.getTopP())
.presencePenalty(role.getPresencePenalty())
.frequencyPenalty(role.getFrequencyPenalty())
.messages(messages)
.stream(true)
.user(this.uid)
.build();
} else {
chatCompletion = ChatCompletion.builder()
.messages(messages)
.stream(true)
.user(this.uid)
.build();
}
return chatCompletion;
}
(5)發(fā)送請求到openAI
public void streamChatCompletion(ChatCompletion chatCompletion, EventSourceListener eventSourceListener) {
if (Objects.isNull(eventSourceListener)) {
log.error("參數(shù)異常:EventSourceListener不能為空,可以參考:com.unfbx.chatgpt.sse.ConsoleEventSourceListener");
throw new BaseException(CommonError.PARAM_ERROR);
} else {
if (!chatCompletion.isStream()) {
chatCompletion.setStream(true);
}
try {
EventSource.Factory factory = EventSources.createFactory(this.okHttpClient);
ObjectMapper mapper = new ObjectMapper();
String requestBody = mapper.writeValueAsString(chatCompletion);
Request request = (new Request.Builder()).url(this.apiHost + "v1/chat/completions").post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody)).build();
factory.newEventSource(request, eventSourceListener);
} catch (JsonProcessingException var8) {
log.error("請求參數(shù)解析異常:{}", var8);
var8.printStackTrace();
} catch (Exception var9) {
log.error("請求參數(shù)解析異常:{}", var9);
var9.printStackTrace();
}
}
}
上面代碼中,通過okHttpClient創(chuàng)建了一個EventSource.Factory對象,然后創(chuàng)建了一個ObjectMapper對象,用于將數(shù)據(jù)轉(zhuǎn)換為JSON格式。使用ObjectMapper將completion對象轉(zhuǎn)換為JSON字符串,存儲在requestBody變量中,使用ObjectMapper將completion對象轉(zhuǎn)換為JSON字符串,存儲在requestBody變量中,最后通過已創(chuàng)建的EventSource.Factory對象調(diào)用newEventSource方法,傳入創(chuàng)建的Request對象和eventSourceListener參數(shù)。這樣就創(chuàng)建了一個EventSource對象,并開始監(jiān)聽事件。簡單來說,這段代碼的作用是創(chuàng)建一個EventSource對象,并發(fā)送一個POST請求。
現(xiàn)在有兩項技術(shù)需要解釋一下,分別是okHttpClient和EventSource。
okHttpClient
okHttpClient是一個開源的Java HTTP客戶端庫,由Square公司開發(fā)。它提供了一個簡單易用的接口,用于發(fā)送HTTP請求并處理響應(yīng)。
特點:
-
支持同步和異步請求:okHttpClient可以發(fā)送同步和異步的HTTP請求。同步請求會阻塞當(dāng)前線程,直到接收到服務(wù)器的響應(yīng);異步請求則使用回調(diào)函數(shù)來處理響應(yīng)。
-
連接池管理:okHttpClient使用連接池來管理和復(fù)用HTTP連接,從而降低網(wǎng)絡(luò)請求的延遲。它支持并發(fā)請求和多線程環(huán)境下的高效連接復(fù)用。
-
支持HTTP/2和SPDY協(xié)議:okHttpClient支持最新的HTTP/2和SPDY協(xié)議,可以提供更快的傳輸速度和更低的網(wǎng)絡(luò)延遲。
-
支持攔截器:okHttpClient提供了攔截器機制,可以在請求和響應(yīng)的不同階段進行自定義操作,例如添加請求頭、日志記錄等。
-
支持重試和重定向:okHttpClient可以自動處理請求的失敗、重試和重定向,減少開發(fā)者處理這些場景的工作量。
-
可擴展性和靈活性:okHttpClient通過插件系統(tǒng)提供了很高的可擴展性,并且可以根據(jù)實際需求進行各種配置,如超時設(shè)置、緩存策略等。
總的來說,okHttpClient是一個功能強大、易于使用和高效的Java HTTP客戶端庫,用于發(fā)送HTTP請求并處理響應(yīng),適用于各種網(wǎng)絡(luò)場景和需求。
EventSource
EventSource是HTML5中定義的一種客戶端API,也被稱為服務(wù)器發(fā)送事件(Server-Sent Events)。它提供了一種在客戶端與服務(wù)器之間實現(xiàn)單向通信的機制。以下是EventSource的作用和詳細解釋:
作用:
-
實時數(shù)據(jù)推送:EventSource可以與服務(wù)器建立長連接,通過服務(wù)器推送數(shù)據(jù),實現(xiàn)實時更新,而無需客戶端主動請求。
-
可靠性:EventSource會自動恢復(fù)與服務(wù)器連接,即使網(wǎng)絡(luò)連接中斷或重新連接。
-
簡化開發(fā):相比于WebSocket,EventSource更為簡單易用,不需要進行握手等繁瑣的操作。
工作原理:
-
客戶端通過創(chuàng)建EventSource對象,并指定服務(wù)器端的URL來與服務(wù)器進行連接。
-
服務(wù)器向客戶端發(fā)送事件流(event stream),可以包含不同類型的事件(event),每個事件都包含一個事件類型(event type)和事件數(shù)據(jù)(event data)。
-
客戶端通過監(jiān)聽EventSource對象的相關(guān)事件來獲取服務(wù)器發(fā)來的數(shù)據(jù),如onmessage事件用于接收消息,onerror事件用于處理連接錯誤,等等。
優(yōu)點:
-
對于服務(wù)器推送數(shù)據(jù)的場景非常適用,特別是在實時性要求較高且數(shù)據(jù)量較小的情況下。
-
內(nèi)部自動處理了連接狀態(tài)、網(wǎng)絡(luò)中斷等問題,減輕了開發(fā)者的負擔(dān)。
-
不需要考慮跨域問題,因為EventSource默認支持跨域訪問。
需要注意的是,EventSource與WebSocket的區(qū)別在于數(shù)據(jù)傳輸方式。WebSocket提供了雙向通信的能力,并支持客戶端和服務(wù)器端之間的全雙工通信,而EventSource只支持服務(wù)器向客戶端的單向數(shù)據(jù)推送。
我們這里后端代碼調(diào)用openAI使用的是EventSource,前端和后端通信用的是WebSocket,其實都可以使用EventSource。
(6)監(jiān)聽openAI響應(yīng)
我們通過EventSourceListener監(jiān)聽
public void onEvent(EventSource eventSource, String id, String type, String data) {
if (data.equals("[DONE]")) {
log.info("OpenAI返回數(shù)據(jù)結(jié)束了");
session.getBasicRemote().sendText("[DONE]");
return;
}
ObjectMapper mapper = new ObjectMapper();
ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class);
String delta = mapper.writeValueAsString(completionResponse.getChoices().get(0).getDelta());
session.getBasicRemote().sendText(delta);
}
session對象代表了客戶端與服務(wù)器之間的一個會話連接。它是WebSocket中的一個核心對象,用于進行數(shù)據(jù)交互。session.getBasicRemote()方法返回RemoteEndpoint.Basic對象,通過該對象可以向客戶端發(fā)送消息。
sendText()方法是RemoteEndpoint.Basic對象的一個方法,用于發(fā)送文本消息到客戶端。delta是要發(fā)送的文本消息內(nèi)容。這句代碼的作用是將delta文本消息發(fā)送給客戶端,實現(xiàn)服務(wù)器向客戶端的實時消息推送功能,這樣,之前和后端建立好WebSocket連接的前端,就可以接收到來自chatGPT的響應(yīng)了。
數(shù)據(jù)庫結(jié)構(gòu)
主要涉及到3張表,分別為role_type,role_desc和role。其中,role_type表是存放角色分類的,如娛樂、生活、工作、學(xué)習(xí)、健康等。
建表語句如下:
CREATE TABLE `role_type` (
`id` varchar(64) NOT NULL COMMENT '主鍵',
`role_type_name` varchar(256) NOT NULL COMMENT '類別名稱',
`sort_num` decimal(10,2) NOT NULL DEFAULT '1.00' COMMENT '排序序號',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色類別表';
role_desc表是角色描述信息表,其中包含了角色名稱,簡介,圖片等內(nèi)容,主要是用于角色列表展示的。
建表語句如下:
CREATE TABLE `role_desc` (
`id` varchar(10) NOT NULL COMMENT '主鍵',
`title` varchar(512) NOT NULL COMMENT '標題',
`description` text COMMENT '內(nèi)容',
`chat` text COMMENT '聊天內(nèi)容',
`image` varchar(128) NOT NULL COMMENT '圖片',
`role_id` int DEFAULT NULL COMMENT 'roleId',
`sort_num` decimal(10,2) NOT NULL DEFAULT '1.00' COMMENT '排序序號',
`post_status` smallint DEFAULT '0' COMMENT '上線狀態(tài)',
`create_date` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',
`update_date` datetime DEFAULT NULL COMMENT '更新時間',
`role_type_id` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `type_index` (`role_type_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色描述表';
role表,是角色參數(shù)表,用于保存角色的prompt和調(diào)用openAI的一些核心參數(shù),如模型,最大token等
建表語句如下:
CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '唯一標識,自動遞增',
`role_name` varchar(255) DEFAULT NULL,
`role_message` text,
`model` varchar(255) NOT NULL DEFAULT 'gpt-3.5-turbo',
`create_time` datetime NOT NULL,
`temperature` double NOT NULL DEFAULT '0.2',
`max_tokens` int NOT NULL DEFAULT '2048',
`top_p` double NOT NULL DEFAULT '1',
`presence_penalty` double NOT NULL DEFAULT '0',
`frequency_penalty` double NOT NULL DEFAULT '0',
`n` int NOT NULL DEFAULT '1',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb3;
總結(jié)
本次分享,只分享了后端代碼,前端和小程序端,下次繼續(xù)分享。如果你想繼續(xù)了解的話,可以關(guān)注我的公眾號【程序員修煉】。
最后,代碼獲取方式,關(guān)注公眾號,發(fā)送"ChatGPTService"獲取后端代碼,發(fā)送"ChatGPTWeChat"獲取小程序代碼,發(fā)送"ChatGPTWeb"獲取前端代碼。文章來源:http://www.zghlxwxcb.cn/news/detail-706960.html
另外,有任何問題,可加我微信【cxyxl66】進行咨詢,來者不拒。文章來源地址http://www.zghlxwxcb.cn/news/detail-706960.html
到了這里,關(guān)于福利!打造自己的ChatGPT聊天小程序,前后端代碼全開源的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!