項目實戰:基于Spring WebFlux與LangChain4j實現大語言模型流式輸出

一、背景

????????在大語言模型(LLM)應用場景中,GPT-4等模型的響應生成往往需要數秒至數十秒的等待時間。傳統同步請求會導致用戶面對空白頁面等待,體驗較差。本文通過Spring WebFlux響應式編程SSE服務器推送技術,實現類似打印機的逐字流式輸出效果,同時結合LangChain4j框架進行AI能力集成,有效提升用戶體驗。

二、技術選型

  1. Spring WebFlux:基于 Reactor 的異步非阻塞 Web 框架
  2. SSE(Server-Sent Events):輕量級服務器推送技術
  3. LLM框架:LangChain4j?
  4. 大模型 API:以 OpenAI 的 GPT-4 (實際大模型是deepseek)
  5. 開發工具:IntelliJ IDEA + JDK 17

三、Spring WebFlux介紹

Spring Webflux 教程 - spring 中文網

這里就不多介紹了,網上教程很多

四、整體方案

五、實現步驟

1、pom依賴

        <dependency><groupId>io.milvus</groupId><artifactId>milvus-sdk-java</artifactId><version>2.5.1</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-milvus</artifactId><version>0.36.2</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId><version>0.36.2</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai</artifactId><version>0.36.2</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai-spring-boot-starter</artifactId><version>0.36.2</version></dependency><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-reactor</artifactId><version>0.36.2</version></dependency>

2、controller層

content-type= text/event-stream

@ApiOperation(value = "流式對話")
@PostMapping(value = "", produces = TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestBody @Validated ChatReq chatReq) {log.info("--流式對話 chat request: {}--", chatReq);return chatService.chat(chatReq);
}
@ApiModel(value = "對話請求")
public class ChatReq {@ApiModelProperty(value = "對話id")private Long chatId;@ApiModelProperty(value = "對話類型")private Integer type;@ApiModelProperty(value = "提問")private String question;@ApiModelProperty(value = "外部id")private List<Long> externalIds;@ApiModelProperty(value = "向量檢索閾值", example = "0.5")@Min(value = 0)@Max(value = 1)private Double retrievalThreshold;@ApiModelProperty(value = "向量匹配結果數", example = "5")@Min(value = 1)private Integer topK;....}

3、service層

1)主體請求

public Flux<String> chat(ChatReq chatReq) {// Create a Sink that will emit items to FluxSinks.Many<ApiResponse<String>> sink = Sinks.many().multicast().onBackpressureBuffer();// 用于控制數據生成邏輯的標志AtomicBoolean isCancelled = new AtomicBoolean(false);ChatStreamingResponseHandler chatStreamingResponseHandler = new ChatStreamingResponseHandler();// 判斷新舊對話if (isNewChat(chatReq.getChatId())) { // 新對話,涉及業務略過chatReq.setHasHistory(false);chatModelHandle(chatReq);} else { // 舊對話// 根據chatId查詢對話類型和對話歷史chatReq.setHasHistory(true);chatModelHandle(chatReq);}return sink.asFlux().doOnCancel(() -> {log.info("停止流處理");isCancelled.set(true); // 設置取消標志sink.tryEmitComplete(); // 停止流});}

2)構建請求參數

有會話歷史,獲取會話歷史(請求回答和回答)

封裝成ChatMessages(question存UserMessage、answer存AiMessage)

?
private void chatModelHandle(ChatReq chatReq){List<ChatMessage> history = new ArrayList<>();if (chatReq.getHasHistory()) {// 組裝對話歷史,獲取question和answer分別存UserMessage和AiMessagehistory = getHistory(chatReq.getChatId());}Integer chatType = chatReq.getType();//依賴文本List<Long> externalIds = chatReq.getExternalIds();// 判斷對話類型if (ChatType.NORMAL.getCode().equals(chatType)) { // 普通對話if (chatReq.getHasHistory()) {history.add(UserMessage.from(chatReq.getQuestion()));}chatStreamingResponseHandler = new ChatStreamingResponseHandler(sink, chatReq, isCancelled);ChatModelClient.getStreamingChatLanguageModel(chatReq.getTemperature()).generate(chatReq.getHasHistory() ? history : chatReq.getQuestion(), chatStreamingResponseHandler);} else if (ChatType.DOCUMENT_DB.getCode().equals(chatType)) { // 文本對話Prompt prompt = geneRagPrompt(chatReq);if (chatReq.getHasHistory()) {history.add(UserMessage.from(prompt.text()));}chatStreamingResponseHandler = new ChatStreamingResponseHandler(sink, chatReq, isCancelled);ChatModelClient.getStreamingChatLanguageModel(chatReq.getTemperature()).generate(chatReq.getHasHistory() ? history : prompt.text(), chatStreamingResponseHandler);} else {throw new BizException("功能待開發");}}?

3)如果有參考文本,獲取參考文本

在向量庫中,根據參考文本id和向量檢索閾值,查看參考文本topN

    private List<PPid> search(ChatReq chatReq, MilvusClientV2 client, MilvusConfig config, EmbeddingModel model) {//使用文本id進行查詢TextSegment segment = TextSegment.from(chatReq.getQuestion());Embedding queryEmbedding = model.embed(segment).content();SearchResp searchResp = client.search(SearchReq.builder().collectionName(config.getCollectionName()).data(Collections.singletonList(new FloatVec(queryEmbedding.vector()))).filter(String.format("ARRAY_CONTAINS(documentIdList, %s)", chatReq.getExternalIds())).topK(chatReq.getTopK() == null ? config.getTopK() : chatReq.getTopK()).outputFields(Arrays.asList("pid", "documentId")).build());// 過濾掉分數低于閾值的結果List<SearchResp.SearchResult> searchResults = searchResp.getSearchResults().get(0);Double minScore = chatReq.getRetrievalThreshold() == null ? config.getMinScore() : chatReq.getRetrievalThreshold();return searchResults.stream().filter(item -> item.getScore() >= minScore).sorted((item1, item2) -> Double.compare(item2.getScore(), item1.getScore())).map(item -> new PPid((Long) item.getEntity().get("documentId"),(Long) item.getEntity().get("pid"))).toList();}

獲取參考文本id后,獲取文本,再封裝請求模版

?
private Prompt genePrompt(String context) {...
}?

4)連接大模型客戶端

public static StreamingChatLanguageModel getStreamingChatLanguageModel() {ChatModelConfig config = ChatConfig.getInstance().getChatModelConfig();return OpenAiStreamingChatModel.builder().baseUrl(config.getBaseUrl()).modelName(config.getModelName()).apiKey(config.getApiKey()).maxTokens(config.getMaxTokens()).timeout(Duration.ofSeconds(config.getTimeout())).build();
}

5)大模型輸出處理


@Slf4j
@Data
@NoArgsConstructor
public class ChatStreamingResponseHandler implements StreamingResponseHandler<AiMessage> {private Sinks.Many<ApiResponse<String>> sink;private ChatReq chatReq;private AtomicBoolean isCancelled;public ChatStreamingResponseHandler(Sinks.Many<ApiResponse<String>> sink, ChatReq chatReq, AtomicBoolean isCancelled) {this.sink = sink;this.chatReq = chatReq;this.isCancelled = isCancelled;}@Overridepublic void onNext(String answer) {//取消不輸出if (isCancelled.get()) {return;}sink.tryEmitNext(BaseController.success(answer));}@Overridepublic void onComplete(Response<AiMessage> response) {if (!isCancelled.get()) {sink.tryEmitNext("結束標識");sink.tryEmitComplete();}// 業務處理}@Overridepublic void onError(Throwable error) {if (!isCancelled.get()) {sink.tryEmitError(error);}// 業務處理}}

六、效果呈現

七、結尾

上面簡要列一下實現步驟,可以留言深入討論。

有許多體驗還需要完善,以參考豆包舉例

1、實現手動停止響應

2、刷新或者頁面關閉自動停止流式輸出,重連后流式輸出繼續

3、將多個Token打包發送,減少SSE幀數量

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/74332.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/74332.shtml
英文地址,請注明出處:http://en.pswp.cn/web/74332.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Go語言入門經典:數組與切片詳解

Go語言入門經典&#xff1a;數組與切片詳解 數組和切片是Go語言中兩種重要的數據結構。數組是一種固定長度的集合&#xff0c;而切片則是一種靈活的動態集合。本章將詳細講解數組和切片的定義、初始化、訪問元素、動態操作等內容&#xff0c;幫助讀者全面掌握這兩種數據結構。…

uniapp中如何用iconfont來管理圖標集成到我們開發的項目中

我們在開發不管小程序還是APP的過程中都會用到圖標這個東西,那么iconfont提供了對應的功能,怎么才能方便的集成到我們的小程序或者APP項目中,目標是方便調用并且方便管理。 首先注冊ICONFONT賬號 www.iconfont.cn中去注冊即可選擇我們需要的圖標如下 我們搜索我們需要的圖…

從實用的角度聊聊Linux下文本編輯器VIM

本文從實用的角度聊聊Vim的常用命令。何為實用&#xff1f;我舉個不實用的例子大家就明白了&#xff0c;用vim寫代碼。;) “vim是從 vi 發展出來的一個文本編輯器。代碼補全、編譯及錯誤跳轉等方便編程的功能特別豐富&#xff0c;在程序員中被廣泛使用&#xff0c;和Emacs并列成…

優化程序命名:提升專業感與用戶體驗

在軟件開發的廣闊天地中&#xff0c;程序命名這一環節常常被開發者們忽視。不少程序沿用著簡單直白、缺乏雕琢的名字&#xff0c;如同素面朝天的璞玉&#xff0c;雖不影響其核心功能的發揮&#xff0c;但卻在無形之中錯失了許多提升用戶印象與拓展應用場景的機會。今天&#xf…

LeetCode BFS解決最短路問題

廣度優先搜索(BFS, Breadth-First Search)是一種用于圖和樹結構的遍歷算法&#xff0c;特別適合解決無權圖的最短路徑問題。 算法思想&#xff1a; BFS從起始節點開始&#xff0c;按照"廣度優先"的原則&#xff0c;逐層向外擴展搜索&#xff1a; 先訪問起始節點的…

[物聯網iot]對比WIFI、MQTT、TCP、UDP通信協議

第一步&#xff1a;先理解最基礎的關系&#xff08;類比快遞&#xff09; 假設你要給朋友寄快遞&#xff1a; Wi-Fi&#xff1a;相當于“公路和卡車”&#xff0c;負責把包裹從你家運到快遞站。 TCP/UDP&#xff1a;相當于“快遞公司的運輸規則”。 TCP&#xff1a;順豐快遞&…

基于python的電影數據分析及可視化系統

一、項目背景 隨著電影行業的快速發展&#xff0c;電影數據日益豐富&#xff0c;如何有效地分析和可視化這些數據成為行業內的一個重要課題。本系統旨在利用Python編程語言&#xff0c;結合數據分析與可視化技術&#xff0c;為電影行業從業者、研究者及愛好者提供一個便捷的電…

Java8 到 Java21 系列之 Lambda 表達式:函數式編程的開端(Java 8)

Java8 到 Java21 系列之 Lambda 表達式&#xff1a;函數式編程的開端&#xff08;Java 8&#xff09; 系列目錄 Java8 到 Java21 系列之 Lambda 表達式&#xff1a;函數式編程的開端&#xff08;Java 8&#xff09;Java 8 到 Java 21 系列之 Stream API&#xff1a;數據處理的…

②EtherCAT/Ethernet/IP/Profinet/ModbusTCP協議互轉工業串口網關

型號 協議轉換通信網關 EtherCAT 轉 Modbus TCP 配置說明 網線連接電腦到模塊上的 WEB 網頁設置網口&#xff0c;電腦所連網口的網段設置成 192.168.1.X&#xff08;X 是除 8 外的任一數值&#xff09;后&#xff0c;打開瀏覽器&#xff0c;地址欄輸入 192.168.1.8 &#xff…

機器視覺--python基礎語法

Python基礎語法 1. Python標識符 在 Python 里&#xff0c;標識符由字母、數字、下劃線組成。 在 Python 中&#xff0c;所有標識符可以包括英文、數字以及下劃線(_)&#xff0c;但不能以數字開頭。 Python 中的標識符是區分大小寫的。 以下劃線開頭的標識符是有特殊意義的…

算法日常記錄

1. 鏈表 1.1 刪除鏈表的倒數第 N 個結點 問題描述&#xff1a;給你一個鏈表&#xff0c;刪除鏈表的倒數第 n 個結點&#xff0c;并且返回鏈表的頭結點。 輸入&#xff1a;head [1,2,3,4,5], n 2 輸出&#xff1a;[1,2,3,5] 思路&#xff1a;先讓fast跑n步&#xff0c;然后…

14使用按鈕實現helloworld(1)

目錄 還可以通過按鈕的方式來創建 hello world 涉及Qt 中的信號槽機制本質就是給按鈕的點擊操作,關聯上一個處理函數當用戶點擊的時候 就會執行這個處理函數 connect&#xff08;誰發的信號&#xff0c; 信號類型&#xff0c; 誰來處理這個信息&#xff0c; 怎么處理的&…

【Golang】泛型與類型約束

文章目錄 一、環境二、沒有泛型的Go三、泛型的優點四、理解泛型&#xff08;一&#xff09;泛型函數&#xff08;Generic function&#xff09;1&#xff09;定義2&#xff09;調用 &#xff08;二&#xff09;類型約束&#xff08;Type constraint&#xff09;1&#xff09;接…

k8s常用總結

1. Kubernetes 架構概覽 主節點&#xff08;Master&#xff09;&#xff1a; 負責集群管理&#xff0c;包括 API Server、Controller Manager、Scheduler 和 etcd 存儲。 工作節點&#xff08;Node&#xff09;&#xff1a; 運行 Pod 和容器&#xff0c;包含 kubelet、kube-pr…

Android 單例模式全解析:從基礎實現到最佳實踐

單例模式&#xff08;Singleton Pattern&#xff09;是軟件開發中常用的設計模式&#xff0c;其核心是確保一個類在全局范圍內只有一個實例&#xff0c;并提供全局訪問點。在 Android 開發中&#xff0c;單例模式常用于管理全局資源&#xff08;如網絡管理器、數據庫助手、配置…

ffmpeg濾鏡使用

ffmpeg實現畫中畫效果 FFmpeg中&#xff0c;可以通過overlay將多個視頻流、多個多媒體采集設備、多個視頻文件合并到一個界面中&#xff0c;生成畫中畫的效果 FFmpeg 濾鏡 overlay 基本參數 x和y x坐標和Y坐標 eof action 遇到 eof表示時的處理方式&#xff0c;默認為重復。…

OpenAI即將開源!DeepSeek“逼宮”下,AI爭奪戰將走向何方?

OpenAI 終于要 Open 了。 北京時間 4 月 1 日凌晨&#xff0c;OpenAI 正式宣布&#xff1a;將在未來幾個月內開源一款具備推理能力的語言模型&#xff0c;并開放訓練權重參數。這是自 2019 年 GPT-2 部分開源以來&#xff0c;OpenAI 首次向公眾開放核心模型技術。 【圖片來源于…

貪心算法,其優缺點是什么?

什么是貪心算法&#xff1f; 貪心算法(Greedy Algorithm)是一種在每一步選擇中都采取在當前狀態下最優(局部最優)的選擇&#xff0c;從而希望導致全局最優解的算法策略。 它不像動態規劃那樣考慮所有可能的子問題&#xff0c;而是做出局部最優選擇&#xff0c;依賴這些選擇來…

python string 類型字符拼接 +=的缺點,以及取代方法

在Python中&#xff0c;使用進行字符串拼接雖然語法簡單&#xff0c;但在性能和代碼維護方面存在明顯缺陷。以下是詳細分析及替代方案&#xff1a; 一、的缺點 性能低下 內存分配問題&#xff1a;字符串在Python中不可變&#xff0c;每次操作會創建新字符串對象&#xff0c;導…

web前端開發-JS

web前端開發-JS 什么是JavaScript Web標準也稱網頁標準,由一系列的標準組成,大部分由W3C(World Wide Web Consortium,萬維網聯盟)負責制定。三個組成部分: HTML:負責網頁的結構(頁面元素和內容)。CSS:負責網頁的表現(頁面元素的外觀、位置等頁面樣式,如:顏色、大小等)。JavaS…