SpringBoot接入通义千问实现个人ChatGPT

1、ChatGPT的热度

ChatGPT是由美国人工智能实验室OpenAI开发的一个对话AI模型,于2022年11月正式推出。自推出以来,ChatGPT因其出色的文本生成和对话交互能力而在全球范围内迅速走红。上线短短两个月,ChatGPT已获得1亿月度活跃用户,成为历史上增长最快的面向消费者的应用。

ChatGPT的爆火在业界掀起了惊涛骇浪,其用户增长速度刷新了消费级应用程序的记录。不少和ChatGPT“聊过天”的网友纷纷感叹,“只有你想不到,没有ChatGPT办不成的”。在一位工程师的诱导下,ChatGPT竟写出了毁灭人类的计划书,这进一步引发了人们对其潜在危险性的担忧。

ChatGPT的火热也带动了资本市场相关上市公司股票的普涨,包括AIGC、芯片算力、光模块等板块的普遍上涨。同时,国内互联网公司接连宣布类似ChatGPT的项目存在,如百度的类ChatGPT项目“文心一言”、阿里的“通义千问”。

2、前言准备

在国内也有许多的GPT平台,要使用的步骤都是一样的,先开通服务,再申请Key。
在使用的过程中,需要与流式编程搭配使用才能得到最好的效果,所以了解和掌握流式编程也是很重要的一步。

2.1、开通服务

(1)登录“阿里云”官网。
(2)搜索“通义千问”
SpringBoot接入通义千问实现个人ChatGPT_第1张图片(3)开通服务
SpringBoot接入通义千问实现个人ChatGPT_第2张图片
确认开通
SpringBoot接入通义千问实现个人ChatGPT_第3张图片
开通成功
SpringBoot接入通义千问实现个人ChatGPT_第4张图片

2.2、reactor流式响应

Spring流式编程是一种基于流的处理方式,它将数据流作为主要处理对象。

功能:

  • 数据处理:Spring流式编程能够处理大量的数据,并将数据转换成所需的形式或结构。
  • 异步处理:Spring流式编程支持异步处理,能够并行处理多个数据流,提高系统的吞吐量和响应速度。
  • 实时性:由于Spring流式编程支持异步处理和并行处理,因此它能够实现实时数据处理。

好处:

  • 提高性能:由于Spring流式编程采用异步处理和并行处理,因此它能够提高系统的性能和响应速度。
  • 简化开发:Spring流式编程提供了丰富的API和工具,简化了流式处理应用程序的开发过程。
  • 易于维护:由于Spring流式编程采用声明式编程风格,代码结构清晰简洁,易于维护和调试。
  • 灵活性强:Spring流式编程具有很强的灵活性,能够处理各种不同形式和结构的数据。

特点:

  • 流式处理:Spring流式编程将数据看作流来处理,可以同时处理多个数据流。
  • 事件驱动:Spring流式编程采用事件驱动的架构,能够快速响应用户输入和系统事件。
  • 异步处理:Spring流式编程支持异步处理,能够并行处理多个数据流,提高系统的吞吐量和响应速度。
  • 声明式编程:Spring流式编程采用声明式编程风格,通过简单的注解和XML配置来简化开发过程。

Flux 和 Mono 是 Reactor 中两个最基本的类型,是 Spring WebFlux 核心概念,表示 Reactor 中的数据流。

Flux和Mono本质上也是两个Publisher。

2.2.1、Flux流式对象

Flux 是 Project Reactor 中用于表示非确定性、0 到多个元素的类型。也就是说,Flux 可以是空的,也可以有一个或多个元素。它是响应式编程中的"热"流,类似于传统的迭代器,但更加强大和灵活。你可以把它想象成从一个数据源不断地流出的数据,可以监听这个数据流,当有新的数据出现时,会收到通知。
静态创建 Flux 的方法常见的包括 just()、range()、interval() 以及各种以 from- 为前缀的方法组等。

(1)combineLatest方法
用于组合多个 Flux(反应式流)的值,当这些流中的任何一个发出新的值时,它就会发射一个新的组合值。

public static void main(String[] args) {  
   // 创建三个 Flux  
   Flux<String> flux1 = Flux.just("Hello");  
   Flux<String> flux2 = Flux.just("World");  
   Flux<String> flux3 = Flux.just("!");  

   // 使用 combineLatest 组合这三个 Flux  
   Flux<String> combined = Operators.combineLatest(flux1, flux2, flux3, (s1, s2, s3) -> s1 + s2 + s3);  

   // 输出结果:HelloWorld!  
   combined.subscribe(System.out::println);  
}  

(2)concat类型方法
Flux对象的一个操作符,用于按顺序连接两个或多个Flux流,以便它们可以像单个流一样被消费。这意味着第一个Flux流的所有元素都被发射后,第二个Flux流的元素才会开始发射,依此类推,直到所有的Flux流都被完全消费。

public static void main(String[] args) {  
      Flux<Integer> flux1 = Flux.just(1, 2, 3);  
      Flux<Integer> flux2 = Flux.just(4, 5, 6);  

      // 使用concat按顺序连接flux1和flux2  
      Flux<Integer> concatenatedFlux = Flux.concat(flux1, flux2);  

      // 订阅并打印结果  
      concatenatedFlux.subscribe(System.out::println);  
      // 输出将是:1, 2, 3, 4, 5, 6  
  }  

(3)create方法
这个方法允许你创建一个新的 Flux,并允许你直接控制其发射的元素。

public static void main(String[] args) {
     Flux<Object> objectFlux = Flux.create(c -> {
         for (int i = 0; i < 5; i++) c.next(i); // 添加元素
         c.complete(); // 添加完成
     });
     // 01234
     objectFlux.subscribe(System.out::println);
 }

(4)push方法
用于将元素推入到Flux中。与传统的Flux.next方法不同,Flux.push方法允许更低级别的控制和优化。
Flux.push方法的使用需要具备一定的反应式编程经验和技能,因为它涉及到低级别的并发控制和线程安全问题。在大多数情况下,使用Flux.next方法已经足够满足需求,而Flux.push方法更适合于需要更精细控制或优化性能的场景。

public static void main(String[] args) {
    Flux<Object> push = Flux.push(emitter -> {
        for (int i = 0; i < 5; i++) emitter.next(i); // 添加元素
        emitter.complete(); // 添加完成
    });
    // 01234
    push.subscribe(System.out::println);
}

(5)defer方法
Flux.defer()方法在Reactor中是用来延迟创建Flux的。这个方法返回一个新的Flux,这个Flux在订阅发生时才开始创建并执行原始的Flux。

public static void main(String[] args) {
    Flux<String> flux = Flux.defer(() -> Flux.just("create and executor"));
    // 此时才会真的创建并执行:01234
    flux.subscribe(System.out::println);
}

(6)empty方法
创建一个空的Flux对象。

(7)error方法
Flux.error()方法在Reactor中是用来创建一个在订阅后立即发射一个错误的Flux的。这个方法接收一个Throwable参数,这个参数表示错误。当订阅这个Flux时,它会立即发射这个错误给订阅者。

public Flux<String> getFlux() {  
    return Flux.just("Request")  
            .flatMap(request -> {  
                if (request.equals("Invalid")) {  
                    return Flux.error(new IllegalArgumentException("Invalid request"));  
                } else {  
                    return Flux.just("Response");  
                }  
            });  
}

(8)from类型方法
Flux.from()方法是一个将其他数据源转换为Flux流的方法。它可以将各种数据源(如集合、迭代器、异步数据源等)转换为Flux对象,以便在反应式编程中使用。

public static void main(String[] args) {
    Integer[] array = new Integer[]{1,2,3,4,5};
	// from、fromArray、fromStream、fromIterator
    Flux<Integer> flux = Flux.fromArray(array);
    flux.subscribe(System.out::println);
}

(9)just方法
用于创建一个包含指定元素的Flux。这个方法可以指定序列中包含的所有元素,并且创建出来的Flux序列在发布这些元素之后会自动结束。

Flux<String> flux = Flux.just("Hello", "World", "!");

(10)其他常用方法

方法名称 描述
Flux.merge 用于合并多个Flux流
Flux.range 用于生成指定范围内整数序列的Flux
Flux.using 用于在Flux的生命周期内使用一个外部资源
Flux.collect 用于将Flux中的元素收集到某种容器或数据结构中
Flux.distinct 用于从Flux中过滤掉重复的元素
Flux.doOnEach 用于在Flux中的每个元素上执行特定的操作 ,这些操作将在每个元素上单独执行,并且不会影响Flux流的其他操作。
Flux.filter 用于对Flux中的元素进行过滤
Flux.flatMap 用于将Flux中的每个元素进行一对多的转换。它可以将每个元素映射成一个新的Flux,然后将所有这些Flux合并成一个单一的Flux。
Flux.groupBy 用于将Flux中的元素按照指定的键进行分组

2.2.2、Mono流式对象

Mono 是 Project Reactor 中用于表示 0 或 1 个元素的类型。也就是说,Mono 可以是空的,也可以有一个元素。它是响应式编程中的"冷"流,它可能不会产生任何数据,或者在某些情况下可能会产生大量的数据。你可以把它想象成从数据源获取一个数据,然后你可以在任何时候获取这个数据。

Flux对象有的方法Mono也基本都有。

2.3、前端EventSource

EventSource是一种在HTML5中用于实现服务器推送事件的技术。它允许服务器发送事件流(Server-Sent Events)到客户端,而无需客户端主动向服务器发送请求。

EventSource提供了一种简单的方式来接收服务器端发送的事件数据。它通过建立长连接,在服务器有新的数据时,会自动将数据推送给客户端。与传统的轮询方式相比,EventSource使用了长连接,可以节省带宽和资源,同时提供更好的实时性。

在HTML中,使用EventSource可以通过创建一个EventSource对象来实现。该对象可以指定服务器的URL,然后通过监听不同的事件来接收服务器发送的数据。例如,当服务器发送一个名为"message"的事件时,客户端可以监听该事件并执行相应的操作。

new EventSource(url, ?EventSourceInitDict);
// url:需要监听的地址
// EventSourceInitDict:携带的参数

3、接入通义千问

通义千问是阿里云推出的一个超大规模的语言模型,具有多轮对话、文案创作、逻辑推理、多模态理解、多语言支持等多种功能。它能够跟人类进行多轮的交互,也融入了多模态的知识理解,且有文案创作能力,能够续写小说、编写邮件等。通义千问在2023年4月7日开始邀请测试,4月11日在2023阿里云峰会上揭晓。4月18日,钉钉正式接入阿里巴巴“通义千问”大模型。2023年9月13日,阿里云宣布通义千问大模型已首批通过备案,并正式向公众开放。通义千问APP在各大手机应用市场正式上线,所有人都可以通过APP直接体验最新模型能力。此外,通义千问在2023年12月22日成为首个“大模型标准符合性评测”中首批通过评测的四款国产大模型之一,在通用性、智能性等维度均达到国家相关标准要求。

3.1、后端

后端使用SpringBoot + Reactor实现。

3.1.1、导入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>


<dependency>
   <groupId>io.projectreactorgroupId>
   <artifactId>reactor-coreartifactId>
dependency>


<dependency>
   <groupId>com.alibabagroupId>
   <artifactId>dashscope-sdk-javaartifactId>
   <version>2.10.1version>
dependency>

3.1.2、配置

(1)在application.yaml文件中编写API-KEY。

server:
  port: 8081

ai-api-key: YOUR KEY

(2)注入Generation对象

用户可以通过与Generation对象进行交互,获得自然、流畅、准确的回答或任务完成结果,从而更加高效地与机器进行交互。这种交互方式能够减少用户对传统搜索引擎或问答系统的依赖,提高信息获取和任务完成的效率。同时,Generation对象也可以用于实现自然语言生成、对话生成、文本摘要、文本改写等多种应用场景。

@Configuration
public class AiConfig {

    @Bean
    public Generation generation(){
        return new Generation();
    }
}

3.1.3、编写接口

@RestController
@RequestMapping(value = "/ai")
public class TestAi {

    @Value("${ai-api-key}")
    private String appKey;
    @Resource
    private Generation generation;

    @PostMapping(value = "/send", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> aiTalk(@RequestBody String question, HttpServletResponse response)
            throws NoApiKeyException, InputRequiredException {
        Message message = Message.builder()
                .role(Role.USER.getValue())
                .content(question).build();

        QwenParam qwenParam = QwenParam.builder()
                .model(Generation.Models.QWEN_PLUS)
                .messages(Collections.singletonList(message))
                .topP(0.8)
                .resultFormat(QwenParam.ResultFormat.MESSAGE)
                .enableSearch(true)
                .apiKey(appKey)
                .incrementalOutput(true)
                .build();
        Flowable<GenerationResult> result = generation.streamCall(qwenParam);


        return Flux.from(result)
                .map(m -> {
                // GenerationResult对象中输出流(GenerationOutput)的choices是一个列表,存放着生成的数据。
                    String content = m.getOutput().getChoices().get(0).getMessage().getContent();
                    return ServerSentEvent.<String>builder().data(content).build();
                })
                .publishOn(Schedulers.boundedElastic())
                .doOnError(e -> {
                    Map<String, Object> map = new HashMap<>(){{
                        put("code", "400");
                        put("message", "出现了异常,请稍后重试");
                    }};
                    try {
                        response.getOutputStream().print(JSONObject.toJSONString(map));
                    } catch (IOException ex) {
                        throw new RuntimeException(ex);
                    }
                });
    }
}

(1)Message对象
用户与模型的对话历史。list中的每个元素形式为{“role”:角色, “content”: 内容}。
role可以选值:

public enum Role {
    USER("user"),
    ASSISTANT("assistant"),
    BOT("bot"),
    SYSTEM("system"),
    ATTACHMENT("attachment");

    private final String value;

    private Role(String value) {
        this.value = value;
    }

    public String getValue() {
        return this.value;
    }
}

role 方法是用于设置消息的角色(或类型)的方法。这个方法允许您为消息指定一个特定的角色,以便在处理消息时可以对其进行分类或特殊处理。

(2)Model对象

指定用于对话的通义千问模型名。

public static class Models {
    /** @deprecated */
    @Deprecated
    public static final String QWEN_V1 = "qwen-v1";
    public static final String QWEN_TURBO = "qwen-turbo";
    public static final String BAILIAN_V1 = "bailian-v1";
    public static final String DOLLY_12B_V2 = "dolly-12b-v2";
    /** @deprecated */
    @Deprecated
    public static final String QWEN_PLUS_V1 = "qwen-plus-v1";
    public static final String QWEN_PLUS = "qwen-plus";
    public static final String QWEN_MAX = "qwen-max";

    public Models() {
    }
}

(3)topP/topK方法
topP:生成过程中核采样方法概率阈值,例如,取值为0.8时,仅保留概率加起来大于等于0.8的最可能token的最小集合作为候选集。取值范围为(0,1.0),取值越大,生成的随机性越高;取值越低,生成的确定性越高。

topK:生成时,采样候选集的大小。例如,取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。取值越大,生成的随机性越高;取值越小,生成的确定性越高。默认不传递该参数,取值为None或当top_k大于100时,表示不启用top_k策略,此时,仅有top_p策略生效。

(4)enableSearch方法
模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。取值如下:

  • True:启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
  • False(默认):关闭互联网搜索。

(5)incrementalOutput方法
控制流式输出模式,即后面内容会包含已经输出的内容;设置为True,将开启增量输出模式,后面输出不会包含已经输出的内容,您需要自行拼接整体输出。默认是false;

False:
I
I like
i like apple
True:
I
like
apple

该参数只能与stream输出模式配合使用。

更多参数描述请浏览阿里官方文档:https://help.aliyun.com/zh/dashscope/developer-reference/api-details?spm=a2c4g.11186623.0.nextDoc.24ba12b0zyzTIv

3.2、前端

前端使用的是目前市面上流行的框架-Vue。

3.2.1、安装EventSource

EventSource是HTML5内置的一个对象,但是EventSource只支持Get请求,在很多情况下Get请求并不能满足要求,所以我们需要安装支持Post请求的EventSource。

npm install @microsoft/fetch-event-source

使用

import { fetchEventSource } from '@microsoft/fetch-event-source';
export default{
	data(){
		return{
			show: false,
			list:[],
		}
	},
	mounted(){
		fetchEventSource('http://localhost:8000/user/ai/chat', {
			method: 'POST',
			headers: {
			  'Content-Type': 'application/json'
			},
			body: JSON.stringify({"question": "java是什么?"}),
			onmessage(event) {
				// 接收数据
				console.log(event);
			},
			onclose(){
				// 数据传输完毕后就会关闭流
			}
		})
	}
}

3.2.2、安装Markdown

为啥要安装Markdown咧?因为在AI生成的数据中,会有一些特殊的语法需要文本编辑器才能解析,所以就用Markdown才能更好的展示。

npm install markdown-it --save
<div v-html="markdown.render(item.answer)" class="answer_message">div>
import MarkdownIt from 'markdown-it'
export default{
	data(){
		return{
			markdown: new MarkdownIt(),
		}
	},
}

Markdown-it官网:https://markdown-it.docschina.org/

3.2.3、实现事件监听

search(){
	if(this.query.trim().length == 0){
		showNotify({ type: 'warning', message: '消息内容不能为空' }); return;
	}
	this.historyList.push({question: this.query, answer: ''});
	this.query = "";
	let thiz = this;
	let length = this.historyList.length;
	fetchEventSource(this.$api.CHAT, {
	    method: 'POST',
	    headers: {
	        'Content-Type': 'application/json',
	    },
	    body: JSON.stringify({question: thiz.historyList[length - 1].question}),
	    onmessage(event) {
	     	//在此处的this不是外部的this,而是方法的调用者的this,所以需要在外部定义一个变量指向this
			thiz.historyList[length - 1].answer += event.data;
	    },
	    onclose() {
			let temp = thiz.historyList[thiz.historyList.length - 1];
			let body = {
				sessionId: thiz.$route.params.sessionId,
				question: temp.question,
				answer: temp.answer
			}
			thiz.$http.post(thiz.$api.SYNC_MESSAGE, body).then(result => {
				console.log(result);
			})
		}
	})
}

3.3、效果

省略了CSS样式。
SpringBoot接入通义千问实现个人ChatGPT_第5张图片

4、总结

SpringBoot接入通义千问的实践过程,是一个富有挑战和收获的技术之旅。首先,我们需要理解通义千问的API接口和数据格式,这涉及到对其功能和数据模型的深入了解。在接入过程中,我们主要使用了SpringBoot提供的RestTemplate或WebClient进行API请求,通过JSON数据格式进行数据交互。

在这个过程中,我们面临的主要挑战是网络延迟和数据同步的问题。为了解决这些问题,我们采用了异步处理和缓存策略,优化了API请求的频率,提升了数据获取的效率。

从这次实践中,我们深刻体会到技术发展的快速和多变。未来,我们将继续关注通义千问的新特性和API变化,不断优化我们的接入方案,提升系统的稳定性和效率。同时,我们也会将这种技术应用于更多的业务场景,推动业务的智能化发展。

你可能感兴趣的:(SpringBoot,spring,boot,chatgpt,后端,spring,cloud,java,分布式,人工智能)