Java語(yǔ)言實(shí)現(xiàn)大語(yǔ)言模型輸出的打字機(jī)效果(Stream流式)

現(xiàn)在市面上Java實(shí)現(xiàn)的流式輸出代碼很少泄朴,只能自己動(dòng)手豐衣足食。

一、為什么大語(yǔ)言模型使用流式輸出內(nèi)容

大語(yǔ)言模型采用流式輸出內(nèi)容的原因主要有以下幾點(diǎn):

  1. 提高用戶體驗(yàn):流式輸出使得模型的回復(fù)不是一次性生成整個(gè)回答,而是逐字逐句地生成。這種方式避免了用戶長(zhǎng)時(shí)間等待整個(gè)回復(fù)生成完畢的情況毙沾,從而提升了用戶體驗(yàn)。

  2. 提升交互響應(yīng)速度:通過(guò)逐字蹦出回復(fù)宠页,可以實(shí)現(xiàn)更快的交互響應(yīng)左胞。這意味著在用戶輸入消息后寇仓,模型可以快速開(kāi)始生成回答的開(kāi)頭,并根據(jù)上下文逐漸細(xì)化回答烤宙。

  3. 增強(qiáng)對(duì)話透明度:流式輸出可以讓用戶看到模型逐步構(gòu)建回答的過(guò)程遍烦,這有助于用戶理解模型是如何形成回答的,提高了對(duì)話的透明度和可解釋性躺枕。

  4. 優(yōu)化性能表現(xiàn):對(duì)于大型語(yǔ)言模型來(lái)說(shuō)服猪,生成完整的內(nèi)容可能需要較長(zhǎng)的計(jì)算時(shí)間。流式輸出允許模型邊計(jì)算邊輸出拐云,這樣即使模型推理效率不是很高罢猪,也能保證用戶體驗(yàn)不會(huì)受到太大影響。

  5. 實(shí)現(xiàn)動(dòng)畫(huà)效果:流式輸出還可以模仿打字機(jī)的動(dòng)畫(huà)效果叉瘩,即一個(gè)字或一個(gè)詞的輸出膳帕,給用戶一種答案逐漸出現(xiàn)的視覺(jué)效果。

綜上所述薇缅,流式輸出是大語(yǔ)言模型在交互過(guò)程中的一種有效策略危彩,它兼顧了效率和用戶體驗(yàn),同時(shí)也增強(qiáng)了模型的互動(dòng)性和透明度泳桦。

二汤徽、關(guān)于SSE技術(shù)

當(dāng)然WebSocket也可以達(dá)到效果,本文使用更輕量的SSE來(lái)實(shí)現(xiàn)蓬痒。
SSE泻骤,全稱為Server-Sent Events(服務(wù)器發(fā)送事件)漆羔,是一種允許服務(wù)器向?yàn)g覽器客戶端推送實(shí)時(shí)信息的Web技術(shù)梧奢。這種機(jī)制基于HTTP協(xié)議,利用長(zhǎng)輪詢的方式演痒,讓服務(wù)器可以主動(dòng)向客戶端發(fā)送更新的數(shù)據(jù)亲轨,而無(wú)需客戶端不斷地發(fā)起請(qǐng)求去詢問(wèn)是否有新的數(shù)據(jù)。這個(gè)特點(diǎn)正好符合我都需求鸟顺,看了這么多大語(yǔ)言模型的應(yīng)用惦蚊,一直在琢磨底層實(shí)現(xiàn)。

SSE的工作原理是建立在傳統(tǒng)的HTTP請(qǐng)求之上讯嫂,但與傳統(tǒng)HTTP請(qǐng)求不同的是蹦锋,一旦建立連接,服務(wù)器就可以持續(xù)地向客戶端發(fā)送消息欧芽,直到連接被關(guān)閉莉掂。客戶端接收到的消息通常以JSON或其他格式編碼千扔,并且每條消息都包含一個(gè)事件類型和數(shù)據(jù)負(fù)載憎妙。

SSE具有以下特點(diǎn):

  1. 單向通信:SSE主要用于服務(wù)器向客戶端發(fā)送數(shù)據(jù)库正,而不是雙向通信。如果需要客戶端向服務(wù)器發(fā)送數(shù)據(jù)厘唾,通常需要另外的機(jī)制褥符,如WebSocket。

  2. 簡(jiǎn)單高效:SSE使用標(biāo)準(zhǔn)的HTTP協(xié)議抚垃,不需要額外的庫(kù)或插件喷楣,且相比于WebSocket,它在某些情況下可能更加高效讯柔,因?yàn)樗恍枰?wù)器發(fā)送數(shù)據(jù)抡蛙,而不需要保持全雙工的連接。

  3. 自動(dòng)重連:如果連接中斷魂迄,SSE會(huì)自動(dòng)嘗試重新連接粗截,這對(duì)于需要高可靠性的實(shí)時(shí)數(shù)據(jù)推送非常有用。

  4. 跨瀏覽器支持:大多數(shù)現(xiàn)代瀏覽器都支持SSE捣炬,包括Chrome熊昌、Firefox、Safari和Edge湿酸。

SSE常用于需要實(shí)時(shí)數(shù)據(jù)更新的應(yīng)用場(chǎng)景婿屹,如股票價(jià)格更新、新聞推送推溃、社交媒體通知等昂利。通過(guò)SSE,開(kāi)發(fā)者可以輕松地構(gòu)建實(shí)時(shí)交互式的Web應(yīng)用程序铁坎,為用戶提供更加豐富和動(dòng)態(tài)的體驗(yàn)蜂奸。

三、SSE代碼實(shí)現(xiàn)

在Java中硬萍,SseEmitter 是 Spring 框架提供的一個(gè)用于服務(wù)器發(fā)送事件(Server-Sent Events, SSE)的工具扩所。SSE 允許服務(wù)器向客戶端推送實(shí)時(shí)信息,客戶端通過(guò)一個(gè)持久的HTTP連接接收這些信息朴乖。
SpringBoot的pom依賴如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--
           <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-webflux</artifactId>
       </dependency>
       -->

在使用SSE之前也測(cè)試了webflux框架實(shí)現(xiàn)打字機(jī)祖屏,效果不好放棄。
為了使用 SseEmitter 實(shí)現(xiàn)類似打字機(jī)效果的流式輸出买羞,你需要?jiǎng)?chuàng)建一個(gè) SseEmitter 實(shí)例袁勺,然后逐步發(fā)送數(shù)據(jù)給客戶端。下面是一個(gè)簡(jiǎn)單的例子:

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TypewriterController {

    @GetMapping("/typewriter")
    public SseEmitter typewriter() {
        SseEmitter emitter = new SseEmitter();
        
        // 模擬從大語(yǔ)言模型獲取數(shù)據(jù)的過(guò)程
        String[] data = {"Hello", "World", "from", "the", "large", "language", "model"};
        
        for (String word : data) {
            try {
                // 模擬打字機(jī)效果畜普,每個(gè)單詞之間暫停100毫秒
                Thread.sleep(100);
                emitter.send(SseEmitter.event().data(word));
            } catch (InterruptedException e) {
                emitter.completeWithError(e);
                return emitter;
            }
        }
        
        emitter.complete();
        return emitter;
    }
}

在這個(gè)例子中期丰,我們定義了一個(gè) TypewriterController 類,它有一個(gè) /typewriter 端點(diǎn)。當(dāng)這個(gè)端點(diǎn)被訪問(wèn)時(shí)咐汞,它會(huì)創(chuàng)建一個(gè)新的 SseEmitter 對(duì)象盖呼,并逐個(gè)發(fā)送字符串?dāng)?shù)組中的單詞。每發(fā)送一個(gè)單詞后化撕,線程會(huì)暫停100毫秒來(lái)模擬打字機(jī)的效果几晤。

客戶端可以通過(guò)建立一個(gè)到 /typewriter 端點(diǎn)的持久連接來(lái)接收這些事件。例如植阴,如果你使用JavaScript作為客戶端蟹瘾,你可以這樣寫:

<!DOCTYPE html>
<html>
<head>
    <title>Typewriter Effect</title>
</head>
<body>
    <div id="output"></div>
    <script>
        var source = new EventSource('/typewriter');
        
        source.onmessage = function(event) {
            var outputDiv = document.getElementById('output');
            outputDiv.innerHTML += event.data + ' '; // 將接收到的數(shù)據(jù)添加到頁(yè)面中
        };
    </script>
</body>
</html>

這段HTML和JavaScript代碼會(huì)打開(kāi)一個(gè)到服務(wù)器的SSE連接,并在接收到新數(shù)據(jù)時(shí)更新頁(yè)面的內(nèi)容掠手。每次收到數(shù)據(jù)時(shí)憾朴,都會(huì)將其追加到 <div id="output"> 元素中,從而實(shí)現(xiàn)類似于打字機(jī)逐字顯示文本的效果喷鸽。

請(qǐng)注意众雷,在實(shí)際應(yīng)用中,你可能需要處理更多的細(xì)節(jié)做祝,比如錯(cuò)誤處理叙淌、連接關(guān)閉時(shí)的清理工作等犀勒。此外,如果你的大語(yǔ)言模型是通過(guò)異步方式生成數(shù)據(jù)的淤刃,你可能還需要考慮如何與 SseEmitter 進(jìn)行集成渠羞,以確保數(shù)據(jù)能夠正確地流式傳輸?shù)娇蛻舳恕?/p>

四崔赌、API方式對(duì)接SSE服務(wù)端

上面基于JavaScript作為客戶端獲取返回的流式數(shù)據(jù)涵紊,通過(guò)后端的Java代碼也可以對(duì)接SSE服務(wù)端哈街,增加其他邏輯,比如:鑒權(quán)悯嗓、計(jì)費(fèi)件舵、敏感詞過(guò)濾等。
pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.guo.test</groupId>
    <artifactId>streamClient</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>streamClient</name>
    <description>streamClient</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.10.0</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp-sse</artifactId>
            <version>4.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

要注意下面代碼設(shè)置接收媒體類型為:
text/event-stream绅作,建議使用`MediaType.TEXT_EVENT_STREAM_VALUE`代替字符串編碼芦圾。

package com.guo.test.streamclient.client;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.http.MediaType;

import java.io.IOException;
import java.util.concurrent.TimeUnit;


public class streamClient {
    private static final String SSE_URL = "http://localhost:8080/llm/stream/query?query=地鐵安全門的規(guī)范"; // 替換為你的SSE端點(diǎn)地址

    public static void main(String[] args) {
        OkHttpClient client = new OkHttpClient.Builder()
                .readTimeout(0, TimeUnit.MILLISECONDS) // 設(shè)置無(wú)限讀取超時(shí)蛾派,因?yàn)镾SE是長(zhǎng)連接
                .build();

        Request request = new Request.Builder()
                .url(SSE_URL)
                .header("Accept", "text/event-stream") // 設(shè)置接收SSE媒體類型
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected code " + response);
            }

            // 獲取響應(yīng)體并讀取流
            ResponseBody responseBody = response.body();
            if (responseBody == null) {
                return;
            }

            try (java.io.Reader reader = responseBody.charStream()) {
                char[] buffer = new char[1024];
                int bytesRead;
                while ((bytesRead = reader.read(buffer)) != -1) {
                    String data = new String(buffer, 0, bytesRead);
                    // 處理接收到的SSE數(shù)據(jù)
                    System.out.println("Received data: " + data);

                    // 查找SSE事件邊界(通常是"\n\n")
                    /*int eventBoundary = data.indexOf("\n\n");
                    if (eventBoundary != -1) {
                        String event = data.substring(0, eventBoundary);
                        String eventData = data.substring(eventBoundary + 2);
                        // 處理事件頭部和事件數(shù)據(jù)
                        System.out.println("Event: " + event);
                        System.out.println("Event Data: " + eventData);
                    }*/
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末俄认,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子洪乍,更是在濱河造成了極大的恐慌眯杏,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壳澳,死亡現(xiàn)場(chǎng)離奇詭異岂贩,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)巷波,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門萎津,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)卸伞,“玉大人,你說(shuō)我怎么就攤上這事锉屈』绨粒” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵颈渊,是天一觀的道長(zhǎng)遂黍。 經(jīng)常有香客問(wèn)我,道長(zhǎng)俊嗽,這世上最難降的妖魔是什么雾家? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮绍豁,結(jié)果婚禮上芯咧,老公的妹妹穿的比我還像新娘。我一直安慰自己竹揍,他們只是感情好唬党,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著鬼佣,像睡著了一般驶拱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晶衷,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天蓝纲,我揣著相機(jī)與錄音,去河邊找鬼晌纫。 笑死税迷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锹漱。 我是一名探鬼主播箭养,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哥牍!你這毒婦竟也來(lái)了毕泌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嗅辣,失蹤者是張志新(化名)和其女友劉穎撼泛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體澡谭,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡愿题,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片潘酗。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杆兵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仔夺,到底是詐尸還是另有隱情拧咳,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布囚灼,位于F島的核電站骆膝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏灶体。R本人自食惡果不足惜阅签,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蝎抽。 院中可真熱鬧政钟,春花似錦、人聲如沸樟结。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瓢宦。三九已至碎连,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驮履,已是汗流浹背鱼辙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留玫镐,地道東北人倒戏。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像恐似,于是被迫代替她去往敵國(guó)和親杜跷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容