Размышляя об интеграции ChatGpt в проект SpringBoot, я обнаружил, что API ChatGpt имеет два метода возврата при возврате данных: один — использовать потоковую передачу, а другой — напрямую возвращать все данные. Если используется потоковая передача, скорость ответа очень высокая. Нет необходимости получать все содержимое ответа перед началом возврата ответа. Однако можно добиться эффекта возврата ответа, как на пишущей машинке, когда сервер возвращает данные. , если все данные возвращаются напрямую, их необходимо вернуть на стороне сервера. После получения всех результатов ChatGpt все ответы с данными сразу возвращаются клиенту для отображения. Недостаток заключается в том, что это очень медленно и очень медленно. результат может занять до 10 секунд максимум. Поэтому в этой статье делается попытка имитировать использование ChatGpt потоковой передачи данных для возврата данных клиенту.
Сначала протестируйте серверную часть, используя поток для ответа на данные фиксированной текстовой строки. Основной метод — использовать поток ответа HttpServletResponse. Вам необходимо установить заголовок ответа следующим образом:
res.setHeader("Content-Type", "text/event-stream");
res.setContentType("text/event-stream");
res.setCharacterEncoding("UTF-8");
res.setHeader("Pragma", "no-cache");
Тестовый интерфейс выглядит следующим образом:
// Последовательность тестового ответа
@GetMapping("/api/test/sss")
@AnonymousAccess
public void test(String prompt, HttpServletResponse res) throws IOException, InterruptedException {
log.info("[содержимое запроса]: {}", prompt);
String str = " Что такое любовь, которую невозможно получить? \n" +
«Солнце встает на востоке, а дождь идет на западе. Солнца нет, но есть солнце.\n» +
"Если бы они вместе оказались под снегом, они бы были вместе до конца своей жизни.\n" +
"Изначально я стремился к яркой луне, но яркая луна светит над канавой.\n" +
«В это время мы смотрим друг на друга, но не слышим друг друга. Надеюсь, лунный свет осветит тебя.\n» +
«Пояс становится все шире и шире, но я больше об этом не жалею. Я чувствую себя изможденным из-за Йи.\n» +
"Это чувство можно будет вспомнить позже, но оно уже было в растерянности.\n" +
«Если жизнь похожа на первую встречу, то что случилось с Грустным поклонником рисования Западного Ветра.\n» +
«Давным-давно с морем было трудно справиться, за исключением Ушаня, оно не было облаком.\n» +
«Почему мы должны вместе вырезать свечи из западного окна и говорить о дождливой ночи в Васане?\n» +
"Вечность неба и земли рано или поздно закончится, и эта ненависть будет длиться вечно.\n" +
"\n";
// поток ответов
res.setHeader("Content-Type", "text/event-stream");
res.setContentType("text/event-stream");
res.setCharacterEncoding("UTF-8");
res.setHeader("Pragma", "no-cache");
ServletOutputStream out = null;
try {
out = res.getOutputStream();
for (int i = 0; i < str.length(); i++) {
out.write(String.valueOf(str.charAt(i)).getBytes());
// возобновлятьданныепоток out.flush();
Thread.sleep(100);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Используя этот интерфейс, возвращаемые данные необходимо обрабатывать с помощью потока. Если вы напрямую запросите этот интерфейс в браузере, эффект будет следующим:
Чтобы получать данные текстового потока в js, вам необходимо установить тип ответа: xhr.setRequestHeader("Content-Type", "text/event-stream");
Конкретная реализация кода приема данных выглядит следующим образом:
// создавать XMLHttpRequest объект
const xhr = new XMLHttpRequest();
// Установите запрошенное URL
xhr.open(
"GET",
`http://localhost:8080/api/test/sss`
);
// Установите тип ответа на text/event-stream
xhr.setRequestHeader("Content-Type", "text/event-stream");
// монитор readyStateChange событие
xhr.onreadystatechange = () => {
// если readyState да 3. Указывает, что данные получены.
if (xhr.readyState === 3) {
// Добавьте данные в текстовое поле
console.log('xhr.responseText :>> ', xhr.responseText);
reply("images/touxiang.png", xhr.responseText, randomStr)
var height = $("#message").height();
$("html").scrollTop(height)
}
};
// Отправить запрос
xhr.send();
Эффект следующий:
Этот эффект обеспечивает эффект потоковой передачи ChatGpt.
Специальная интеграция ChatGPT SDKУчебные пособия можно просмотреть в официальной документации.:https://gitcode.net/mirrors/grt1228/chatgpt-java
Импортировать зависимости pom:
<dependency>
<groupId>com.unfbx</groupId>
<artifactId>chatgpt-java</artifactId>
<version>1.0.13</version>
</dependency>
использоватьChatGptпотокпереданныйdemoПримеры можно посмотреть:https://gitcode.net/mirrors/grt1228/chatgpt-java/blob/main/src/test/java/com/unfbx/chatgpt/OpenAiStreamClientTest.java
Учебное пособие по подключению официального Demo SDK к ChatGPT очень подробное. Для получения конкретных руководств просто прочитайте демонстрационный документ выше. Здесь мы в основном говорим о деталях получения данных и о том, как ответить клиенту с полученными потоковыми данными.
Ниже приведен пример метода, вызываемого SDK.
public static void ChatGptSendV1(String prompt, ChatSocketVo chatSocketVo) throws IOException {
OpenAiConfig openAiConfig = new OpenAiConfig();
OpenAiStreamClient openAiClient = OpenAiStreamClient.builder()
.apiKey(Collections.singletonList(openAiConfig.getTkoen()))
//Если вы сами являетесь прокси, просто передайте адрес прокси. Если у вас его нет, не передавайте его.
.apiHost(openAiConfig.getDomain())
.build();
//Модель чата: gpt-3.5
ConsoleEventSourceListener eventSourceListener = new ConsoleEventSourceListener();
Message message = Message.builder().role(Message.Role.USER).content(prompt).build();
ChatCompletion chatCompletion = ChatCompletion
.builder()
.model(ChatCompletion.Model.GPT_3_5_TURBO.getName())
.temperature(0.2)
.maxTokens(2048)
.messages(Arrays.asList(message))
.stream(true)
.build();
openAiClient.streamChatCompletion(chatCompletion, eventSourceListener);
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Метод обработки обратного вызова сообщения в коде: ConsoleEventSourceListener eventSourceListener = new ConsoleEventSourceListener();. В методе вызова передачи потока в openAiClient.streamChatCompletion(chatCompletion, eventSourceListener); передается SSE EventSourceListener. Код выглядит следующим образом:
package com.unfbx.chatgpt.sse;
import java.util.Objects;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConsoleEventSourceListener extends EventSourceListener {
private static final Logger log = LoggerFactory.getLogger(ConsoleEventSourceListener.class);
public ConsoleEventSourceListener() {
}
public void onOpen(EventSource eventSource, Response response) {
log.info("OpenAI устанавливает соединение...");
}
public void onEvent(EventSource eventSource, String id, String type,String data) {
log.info("OpenAIвозвращатьсяданные:{}", data);
if (data.equals("[DONE]")) {
log.info("Возврат данных OpenAI завершен");
}
}
public void onClosed(EventSource eventSource) {
log.info("OpenAI закрыт ссесоединять...");
}
public void onFailure(EventSource eventSource, Throwable t, Response response) {
try {
if (Objects.isNull(response)) {
log.error("OpenAI sseсоединятьаномальный:{}", t);
eventSource.cancel();
} else {
ResponseBody body = response.body();
if (Objects.nonNull(body)) {
log.error("OpenAI sseсоединятьаномальныйdata:{},аномальный:{}", body.string(), t);
} else {
log.error("OpenAI sseсоединятьаномальныйdata:{},аномальный:{}", response, t);
}
eventSource.cancel();
}
} catch (Throwable var5) {
throw var5;
}
}
}
Легко видеть, что способ обработки сообщений потока обратного вызова OpenAI заключается в установлении sse-соединения. Однако использование этого sse-соединения очень похоже на WebSocket. В методе onEvent данные — это содержимое сообщения, на которое отвечает AI. Просто вывод сообщений по умолчанию выводит только логи. Итак, мы можем справиться с этим следующим образом:
Пример пользовательского кода EventSourceListener выглядит следующим образом (добавлена некоторая логика обработки записей сообщений):
package com.team.modules.system.Utils;
import java.util.Date;
import java.util.Objects;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.team.modules.system.domain.ChatgptInfo;
import com.team.modules.system.domain.vo.ChatSocketVo;
import com.team.modules.system.domain.vo.IpDataVo;
import com.team.modules.websocket.WebSocketChatServe;
import com.team.utils.CDKeyUtil;
import com.unfbx.chatgpt.entity.chat.ChatCompletionResponse;
import lombok.SneakyThrows;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Created by tao.
* Date: 2023/6/8 15:51
* описывать:
*/
public class ChatEventSourceListener extends EventSourceListener {
private static final Logger log = LoggerFactory.getLogger(com.unfbx.chatgpt.sse.ConsoleEventSourceListener.class);
private static WebSocketChatServe webSocketChatServe = new WebSocketChatServe();
@Autowired
private String resContent;
@Autowired
private ChatSocketVo chatSocketVo;
public ChatEventSourceListener(ChatSocketVo socketVo) {
chatSocketVo = socketVo;
resContent = CDKeyUtil.getSequence() + ":::";
}
public void onOpen(EventSource eventSource, Response response) {
log.info("OpenAI устанавливает соединение...");
}
int i = 0;
@SneakyThrows
public void onEvent(EventSource eventSource, String id, String type,String data) {
// OpenAIиметь дело сданные
// log.info(i + "---------OpenAIвозвращатьсяданные:{}", data);
// i++;
if (!data.equals("[DONE]")) {
ObjectMapper mapper = new ObjectMapper();
ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class); // Чтение Json
String content = mapper.writeValueAsString(completionResponse.getChoices().get(0).getDelta());
resContent = resContent + content;
// передача данных с помощью ws
webSocketChatServe.sendMessageByKey(resContent, chatSocketVo.getKey());
} else {
log.info("Возврат данных OpenAI завершен");
String[] split = resContent.split(":::");
resContent = CDKeyUtil.getSequence() + ":::";
// Записывайте контент, информацию об IP и т. д.
String ip = chatSocketVo.getIpAddr();
String ua = chatSocketVo.getUa();
// Получите актуальную информацию
IpDataVo ipData = chatSocketVo.getIpDataVo();
String address = ipData.getCountry() + " " + ipData.getProvince() + " " + ipData.getCity() + " " + ipData.getDistrict();
// String address = "";
ChatgptInfo chatgptInfo = new ChatgptInfo(chatSocketVo.getPrompt(), split[1], ip, address, ua, new Date(), ipData.getLocation());
chatSocketVo.getChatgptService().save(chatgptInfo);
}
}
public void onClosed(EventSource eventSource) {
log.info("OpenAI закрыт ссесоединять...");
}
@SneakyThrows
public void onFailure(EventSource eventSource, Throwable t, Response response) {
try {
if (Objects.isNull(response)) {
log.error("OpenAI sseсоединятьаномальный:{}", t);
eventSource.cancel();
} else {
ResponseBody body = response.body();
if (Objects.nonNull(body)) {
log.error("OpenAI sseсоединятьаномальныйdata:{},аномальный:{}", body.string(), t);
} else {
log.error("OpenAI sseсоединятьаномальныйdata:{},аномальный:{}", response, t);
}
eventSource.cancel();
}
} catch (Throwable var5) {
throw var5;
}
}
}
Конечно, способ возврата данных в ответ также может быть реализован с помощью потока ответов, представленного в начале статьи. Недостатком является то, что вам все равно придется избегать проблем с безопасностью потоков. Я попробовал добавить аннотацию @Async; используя этот метод локально, проблем не обнаружено, но при развертывании на сервере обнаруживается, что этот метод не работает. Прежде чем возвращать данные, он будет ждать, пока не будут возвращены все данные.