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

05-14 阅读 0评论

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(3)开通服务

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

确认开通

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

开通成功

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

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 flux1 = Flux.just("Hello");  
           Flux flux2 = Flux.just("World");  
           Flux flux3 = Flux.just("!");  
           // 使用 combineLatest 组合这三个 Flux  
           Flux 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 flux1 = Flux.just(1, 2, 3);  
              Flux flux2 = Flux.just(4, 5, 6);  
              // 使用concat按顺序连接flux1和flux2  
              Flux 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 objectFlux = Flux.create(c -> {
                 for (int i = 0; i  
        

        (4)push方法

        用于将元素推入到Flux中。与传统的Flux.next方法不同,Flux.push方法允许更低级别的控制和优化。

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

        public static void main(String[] args) {
            Flux push = Flux.push(emitter -> {
                for (int i = 0; i  
        

        (5)defer方法

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

        public static void main(String[] args) {
            Flux 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 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 flux = Flux.fromArray(array);
            flux.subscribe(System.out::println);
        }
        

        (9)just方法

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

        Flux 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、导入依赖

            org.springframework.boot
            spring-boot-starter-web
        
        
        
           io.projectreactor
           reactor-core
        
        
        
           com.alibaba
           dashscope-sdk-java
           2.10.1
        
        

        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 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 result = generation.streamCall(qwenParam);
                return Flux.from(result)
                        .map(m -> {
                        // GenerationResult对象中输出流(GenerationOutput)的choices是一个列表,存放着生成的数据。
                            String content = m.getOutput().getChoices().get(0).getMessage().getContent();
                            return ServerSentEvent.builder().data(content).build();
                        })
                        .publishOn(Schedulers.boundedElastic())
                        .doOnError(e -> {
                            Map 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
          
          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

          4、总结

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

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

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


免责声明
本网站所收集的部分公开资料来源于AI生成和互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,人围观)

还没有评论,来说两句吧...

目录[+]