現(xiàn)在市面上Java實(shí)現(xiàn)的流式輸出代碼很少泄朴,只能自己動(dòng)手豐衣足食。
一、為什么大語(yǔ)言模型使用流式輸出內(nèi)容
大語(yǔ)言模型采用流式輸出內(nèi)容的原因主要有以下幾點(diǎn):
提高用戶體驗(yàn):流式輸出使得模型的回復(fù)不是一次性生成整個(gè)回答,而是逐字逐句地生成。這種方式避免了用戶長(zhǎng)時(shí)間等待整個(gè)回復(fù)生成完畢的情況毙沾,從而提升了用戶體驗(yàn)。
提升交互響應(yīng)速度:通過(guò)逐字蹦出回復(fù)宠页,可以實(shí)現(xiàn)更快的交互響應(yīng)左胞。這意味著在用戶輸入消息后寇仓,模型可以快速開(kāi)始生成回答的開(kāi)頭,并根據(jù)上下文逐漸細(xì)化回答烤宙。
增強(qiáng)對(duì)話透明度:流式輸出可以讓用戶看到模型逐步構(gòu)建回答的過(guò)程遍烦,這有助于用戶理解模型是如何形成回答的,提高了對(duì)話的透明度和可解釋性躺枕。
優(yōu)化性能表現(xiàn):對(duì)于大型語(yǔ)言模型來(lái)說(shuō)服猪,生成完整的內(nèi)容可能需要較長(zhǎng)的計(jì)算時(shí)間。流式輸出允許模型邊計(jì)算邊輸出拐云,這樣即使模型推理效率不是很高罢猪,也能保證用戶體驗(yàn)不會(huì)受到太大影響。
實(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):
單向通信:SSE主要用于服務(wù)器向客戶端發(fā)送數(shù)據(jù)库正,而不是雙向通信。如果需要客戶端向服務(wù)器發(fā)送數(shù)據(jù)厘唾,通常需要另外的機(jī)制褥符,如WebSocket。
簡(jiǎn)單高效:SSE使用標(biāo)準(zhǔn)的HTTP協(xié)議抚垃,不需要額外的庫(kù)或插件喷楣,且相比于WebSocket,它在某些情況下可能更加高效讯柔,因?yàn)樗恍枰?wù)器發(fā)送數(shù)據(jù)抡蛙,而不需要保持全雙工的連接。
自動(dòng)重連:如果連接中斷魂迄,SSE會(huì)自動(dòng)嘗試重新連接粗截,這對(duì)于需要高可靠性的實(shí)時(shí)數(shù)據(jù)推送非常有用。
跨瀏覽器支持:大多數(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();
}
}
}