大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和程式碼有表述不當之處,還請不吝賜教。
最近ChatGPT非常受歡迎,尤其是在編寫程式碼方面,我每天都在使用。隨著使用時間的增長,我開始對其原理產生了一些興趣。雖然我無法完全理解這些AI大型模型的演演算法和模型,但我認為可以研究一下其中的互動邏輯。特別是,我想了解它是如何實現在傳送一個問題後不需要等待答案完全生成,而是通過不斷追加的方式實現實時回覆的。
F12開啟控制檯後,我發現在點選傳送後,它會傳送一個普通的請求。但是回覆的方式卻不同,它的型別是eventsource。一次請求會不斷地獲取資料,然後前端的聊天元件會動態地顯示回覆內容,回覆的內容是用Markdown格式來展示的。
在瞭解了前面的這些東西后我就萌生了自己寫一個小demo的想法。起初,我打算使用openai的介面,並寫一個小型的UI元件。然而,由於openai賬號申請複雜且存在網路問題,很多人估計搞不定,所以我最終選擇了通義千問。通義千問有兩個優點:一是它是國內的且目前呼叫是免費的,二是它提供了Java-SDK和API檔案,開發起來容易。
作為後端開發人員,按照API檔案呼叫模型並不難,但真正難到我的是前端UI元件的編寫。我原以為市面上會有很多支援EventStream的現成元件,但事實上並沒有。不知道是因為這個功能太容易還是太難,總之,對接通義千問只花了不到一小時,而編寫一個UI對話元件卻花了整整兩天的時間!接下來,我將分享一些我之前的經驗,希望可以幫助大家少走坑。
首先展示一下我的成品效果
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.8.2</version>
</dependency>
EventStream是一種流式資料格式,用於實時傳輸事件資料。它是基於HTTP協定的,但與傳統的請求-響應模型不同,它是一個持續的、單向的資料流。它可用於推播實時資料、紀錄檔、通知等,所以EventStream很適合這種對話式的場景。在Spring Boot中,主要有以下框架和模組支援EventStream格式:
這次我使用的是reactor-core
框架。
maven依賴
<!-- Reactor Core -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.6</version>
</dependency>
程式碼如下
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.time.LocalTime;
@RestController
@RequestMapping("/event-stream")
public class EventStreamController {
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> getEventStream() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> "Event " + sequence + " at " + LocalTime.now());
}
}
呼叫一下介面後就可以看到瀏覽器上在不斷地列印時間戳了
這個就不BB了,直接貼程式碼!
<?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.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.chatrobot</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 通義千問SDK -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.8.2</version>
</dependency>
<!-- Reactor Core -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.6</version>
</dependency>
<!-- Web元件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
package com.chatrobot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
package com.chatrobot.controller;
import java.time.Duration;
import java.time.LocalTime;
import java.util.Arrays;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.aigc.generation.models.QwenParam;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import io.reactivex.Flowable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/events")
@CrossOrigin
public class EventController {
@Value("${api.key}")
private String apiKey;
@GetMapping(value = "/streamAsk", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamAsk(String q) throws Exception {
Generation gen = new Generation();
// 建立使用者訊息物件
Message userMsg = Message
.builder()
.role(Role.USER.getValue())
.content(q)
.build();
// 建立QwenParam物件,設定引數
QwenParam param = QwenParam.builder()
.model(Generation.Models.QWEN_PLUS)
.messages(Arrays.asList(userMsg))
.resultFormat(QwenParam.ResultFormat.MESSAGE)
.topP(0.8)
.enableSearch(true)
.apiKey(apiKey)
// get streaming output incrementally
.incrementalOutput(true)
.build();
// 呼叫生成介面,獲取Flowable物件
Flowable<GenerationResult> result = gen.streamCall(param);
// 將Flowable轉換成Flux<ServerSentEvent<String>>並進行處理
return Flux.from(result)
// add delay between each event
.delayElements(Duration.ofMillis(1000))
.map(message -> {
String output = message.getOutput().getChoices().get(0).getMessage().getContent();
System.out.println(output); // print the output
return ServerSentEvent.<String>builder()
.data(output)
.build();
})
.concatWith(Flux.just(ServerSentEvent.<String>builder().comment("").build()))
.doOnError(e -> {
if (e instanceof NoApiKeyException) {
// 處理 NoApiKeyException
} else if (e instanceof InputRequiredException) {
// 處理 InputRequiredException
} else if (e instanceof ApiException) {
// 處理其他 ApiException
} else {
// 處理其他異常
}
});
}
@GetMapping(value = "test", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> testEventStream() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> "Event " + sequence + " at " + LocalTime.now());
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatBot</title>
<style>
body {
background: #f9f9f9;
/* 替換為您想要的背景顏色或圖片 */
}
.chat-bot {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
margin: 50px auto;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
font-family: "Roboto", sans-serif;
background: #f5f5f5;
}
.chat-bot-header {
background: linear-gradient(to right, #1791ee, #9fdbf1);
color: white;
text-align: center;
padding: 15px;
font-size: 24px;
font-weight: 500;
}
.chat-bot-messages {
flex: 1;
padding: 20px;
min-height: 400px;
overflow-y: auto;
}
.userName {
margin: 0 10px;
}
.message-wrapper {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
border-radius: 20px;
}
.message-wrapper.user {
justify-content: flex-end;
border-radius: 20px;
}
.message-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #ccc;
margin-right: 10px;
margin-bottom: 10px;
/* 新增這一行 */
order: -1;
/* 新增這一行 */
text-align: right;
}
.message-avatar.user {
background-color: transparent;
display: flex;
justify-content: flex-end;
width: 100%;
margin-right: 0;
align-items: center;
}
.message-avatar.bot {
background-color: transparent;
display: flex;
justify-content: flex-start;
width: 100%;
margin-right: 0;
align-items: center;
}
.message-avatar-inner.user {
background-image: url("./luge.jpeg");
background-size: cover;
background-position: center;
width: 30px;
height: 30px;
border-radius: 50%;
}
.message-avatar-inner.bot {
background-image: url("./logo.svg");
background-size: cover;
background-position: center;
width: 30px;
height: 30px;
border-radius: 50%;
}
.message {
padding: 10px 20px;
border-radius: 15px;
font-size: 16px;
background-color: #d9edf7;
order: 1;
/* 新增這一行 */
}
.bot {
background-color: #e9eff5;
/* 新增這一行 */
}
.user {
background-color: #d9edf7;
color: #111111;
order: 1;
/* 新增這一行 */
}
.chat-bot-input {
display: flex;
align-items: center;
border-top: 1px solid #ccc;
padding: 10px;
background-color: #fff;
}
.chat-bot-input input {
flex: 1;
padding: 10px 15px;
border: none;
font-size: 16px;
outline: none;
}
.chat-bot-input button {
padding: 10px 20px;
background-color: #007bff;
border: none;
border-radius: 50px;
color: white;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s;
}
.chat-bot-input button:hover {
background-color: #0056b3;
}
@media (max-width: 768px) {
.chat-bot {
margin: 20px;
}
.chat-bot-header {
font-size: 20px;
}
.message {
font-size: 14px;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-spinner {
width: 15px;
height: 15px;
border-radius: 50%;
border: 2px solid #d9edf7;
border-top-color: transparent;
animation: spin 1s infinite linear;
}
</style>
</head>
<body>
<div class="chat-bot">
<div class="chat-bot-header">
<img src="./logo.svg">
本文來自部落格園,作者:sum墨,轉載請註明原文連結:https://www.cnblogs.com/wlovet/p/17849779.html