最近ChatGPT非常受欢迎,尤其是在编写代码方面,我每天都在使用。随着使用时间的增长,我开始对其原理产生了一些兴趣。虽然我无法完全理解这些AI大型模型的算法和模型,但我认为可以研究一下其中的交互逻辑。特别是,我想了解它是如何实现在发送一个问题后不需要等待答案完全生成,而是通过不断追加的方式实现实时回复的。
F12打开控制台后,我发现在点击发送后,它会发送一个普通的请求。但是回复的方式却不同,它的类型是eventsource。一次请求会不断地获取数据,然后前端的聊天组件会动态地显示回复内容,回复的内容是用Markdown格式来展示的。
在了解了前面的这些东西后我就萌生了自己写一个小demo的想法。起初,我打算使用openai的接口,并写一个小型的UI组件。然而,由于openai账号申请复杂且存在网络问题,很多人估计搞不定,所以我最终选择了通义千问。通义千问有两个优点:一是它是国内的且目前调用是免费的,二是它提供了Java-SDK和API文档,开发起来容易。
作为后端开发人员,按照API文档调用模型并不难,但真正难到我的是前端UI组件的编写。我原以为市面上会有很多支持EventStream的现成组件,但事实上并没有。不知道是因为这个功能太容易还是太难,总之,对接通义千问只花了不到一小时,而编写一个UI对话组件却花了整整两天的时间!接下来,我将分享一些我之前的经验,希望可以帮助大家少走坑。
首先展示一下我的成品效果
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
1)API文档地址
https://help.aliyun.com/zh/dashscope/developer-reference/api-details
2)Java-SDK依赖
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dashscope-sdk-javaartifactId>
<version>2.8.2version>
dependency>
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/yudao-cloud
-
视频教程:https://doc.iocoder.cn/video/
EventStream是一种流式数据格式,用于实时传输事件数据。它是基于HTTP协议的,但与传统的请求-响应模型不同,它是一个持续的、单向的数据流。它可用于推送实时数据、日志、通知等,所以EventStream很适合这种对话式的场景。在Spring Boot中,主要有以下框架和模块支持EventStream格式:
-
Spring WebFlux:Spring WebFlux是Spring框架的一部分,用于构建反应式Web应用程序。
-
Reactor:Reactor是一个基于响应式流标准的库,是Spring WebFlux的核心组件。
-
Spring Cloud Stream:Spring Cloud Stream是一个用于构建消息驱动的微服务应用的框架。
这次我使用的是reactor-core框架。
maven依赖
<dependency>
<groupId>io.projectreactorgroupId>
<artifactId>reactor-coreartifactId>
<version>3.4.6version>
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 getEventStream() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> "Event " + sequence + " at " + LocalTime.now());
}
}
调用一下接口后就可以看到浏览器上在不断地打印时间戳了
这个就不BB了,直接贴代码!
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.17version>
<relativePath/>
parent>
<groupId>com.chatrobotgroupId>
<artifactId>demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>demoname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dashscope-sdk-javaartifactId>
<version>2.8.2version>
dependency>
<dependency>
<groupId>io.projectreactorgroupId>
<artifactId>reactor-coreartifactId>
<version>3.4.6version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<artifactId>logback-classicartifactId>
<groupId>ch.qos.logbackgroupId>
exclusion>
exclusions>
dependency>
dependencies>
project>
1)后端代码
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> 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 result = gen.streamCall(param);
// 将Flowable转换成Flux>并进行处理
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.builder()
.data(output)
.build();
})
.concatWith(Flux.just(ServerSentEvent.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 testEventStream() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> "Event " + sequence + " at " + LocalTime.now());
}
}
2)前端代码
html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatBottitle>
<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