第八期 Stream 請求 ChatGPT/WebSocket 推送響應

本期內容
?使用 Websocket Stomp 協(xié)議配合@MessageMapping和@Payload?開放消息接口,和接收 JSON 請求體。
實現(xiàn)私有訂閱,服務器將請求的結果響應給對應的用戶,而不是廣播給所有用戶。
使用 Proxy 將請求轉發(fā)給 OpenAI。
對代碼進行邏輯分層,讓代碼更清晰。
代碼實現(xiàn)
1. WebsocketController 接收消息
與 MVC 中的 Controller 不同,這邊不能使用@RestController需要用@Controller。@AllArgsConstructor
是 lombok 提供的一個為類中的屬性生成構成器的注解。這樣我們可以方便的使用 Spring 推薦的構造器依賴注入。
@MessageMapping類似于@RequestMapping用來標識消息路由。它不僅可以加在方法上,也可以加載類上。加在類上那就表名類中的所有消息路由都會拼接上這個路徑。
在被@MessageMapping標識的方法中(如下的 chat 方法),可以使用@Payload解析 JSON 格式的消息體和@RequestBody一樣。同時也可以配合@Valid或者@Validated做參數校驗。
chat()方法的第二個參數接收了Principle。它代表著在 websocket 的 handshake 階段獲取到的用戶信息??梢詤⒖?span id="s0sssss00s" class="color-blue-01">io.qifan.chatgpt.assistant.infrastructure.websocket.UserHandshakeHandler#determineUser
這個方法。
2. 發(fā)送消息
發(fā)送消息的邏輯包含下面四個步驟。
1. GPT 配置校驗
2. 創(chuàng)建 OpenAIService 用于調用 OpenAI 接口。
3. 構造請求參數,將用戶發(fā)送的內容以及用戶的 GPT 配置填充到請求中。
4. 發(fā)送請求并將響應的結果通過私有訂閱地址推送給響應的用戶。
2.1 GPT 配置校驗
在正式調用 OpenAI 的 GPT 接口之前,需要做一些基礎配置的校驗。只有這些基礎數據校驗通過后才能保障后面的代碼正常運行。如果不存在 API Key 則無法調用 OpenAI 的 GPT 接口。
2.2 創(chuàng)建 OpenAIService
配置 proxy,通過 proxy 轉發(fā)給 OpenAI。先定義 Property 配置類,spring boot 會自動讀取 application.yml 中的配置信息到配置類中。在代碼中注入該配置類就可以獲取到 yml 中的配置信息了。
引入封裝好的OpenAI API。
下面開始創(chuàng)建 OpenAIService,用于發(fā)送請求。在創(chuàng)建 OpenAIService 時我們配置了它底層的代理,API Key 以及 Jackson 序列化和反序列化。
2.3 構造 ChatGPT 請求
構造的 ChatGPT 請求參數需要包含用戶的歷史發(fā)送消息和 GPT 的歷史回復消息,這樣它才能記住你們之前的對話內容。所以可以看見我開始的時候根據聊天會話查詢該會話內的聊天記錄,然后將最新的消息插入到歷史消息的尾部。還需要填寫要使用的 GPT 模型,默認是 3.5。還有隨機性,話題新鮮度,最大回復數。最后我們選擇了請求方式是 stream,這樣可以一個個字的得到 ChatGPT 的響應,而不是長時間的等待最后得到一個結果。
還需要在 ChatMessageMapper 中添加我們的 ChatMessage 實體類和第三方包中的 ChatMessage 映射,這樣我們可以方便的構造請求參數。
2.4 發(fā)送請求和推送消息
在?io.qifan.chatgpt.assistant.gpt.session.ChatSession.Statistic?新增 plusChat 和 plusToken 方法。方便統(tǒng)計用戶調用 GPT 接口時的消耗情況。
先獲取已有的統(tǒng)計數量,在上面累加本次用戶發(fā)送消息的長度。新建一個 ChatGPT 回答消息對象`responseMessage`用于記錄回答的消息。由于本次的請求是 stream 類型,所以每次響應是一個 Token(一個單詞或者一個中文字)的,這邊就需要阻塞一個按順序調用`convertAndSendToUser`推送給前端?;卮鹜戤吅髮⒂脩舭l(fā)送的消息和 GPT 回答的消息都插入到數據庫,并且更新會話消耗 Token 的統(tǒng)計數量。
3. 組合各個步驟發(fā)送消息
依次按照配置校驗,創(chuàng)建 OpenAIService,ChatGPT 請求參數,發(fā)送請求的順序調用實現(xiàn)消息發(fā)送邏輯。
代碼測試
1. 創(chuàng)建會話
調用創(chuàng)建聊天會話接口,得到會話 id。
復制你調用后得到的 result。
2. 發(fā)送消息
安裝 stompjs 和 websocket。stompjs 是在 websocket 建立的連接上用特定的協(xié)議去通信。也就是說單單安裝 stompjs 無法使用,需要有 websocket 的連接才能使用。
HomeView.vue中編寫如下的測試代碼,先是向后端發(fā)起 websocket 連接,如果握手成功則訂閱/user/queue/chatMessage/receive。
要注意,后端推送的訂閱地址是?/queue/chatMessage/receive,而用戶的訂閱地址是/user/queue/chatMessage/receive。但是為什么依然可以推送給對應的用戶呢?
可以這么理解,當用戶發(fā)送訂閱消息/user/queue/chatMessage/receive時,其中的/user被替換成了用戶 id 如:/queue/chatMessage/receive-1234。然后在服務端推送消息時,使用的是convertAndSendToUser推送給這個訂閱地址/queue/chatMessage/receive,實際上會推送給/queue/chatMessage/receive-1234。這樣推送和訂閱的最終地址都達到了一致,并且這個地址是用戶私有的。
那為什么/user可以被替換成用戶 id 呢?因為我們之前在 io.qifan.chatgpt.assistant.infrastructure.websocket.WebSocketConfig#configureMessageBroker里面配置了setUserDestinationPrefix("/user")。這行配置就是告訴 SpringWebSocket 遇到 /user開頭的訂閱地址要替換成用戶 id,變成改用戶的私有訂閱地址。