SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!
作者:mmseoamin日期:2024-02-03

一、前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

最近ChatGPT非常受欢迎,尤其是在编写代码方面,我每天都在使用。随着使用时间的增长,我开始对其原理产生了一些兴趣。虽然我无法完全理解这些AI大型模型的算法和模型,但我认为可以研究一下其中的交互逻辑。特别是,我想了解它是如何实现在发送一个问题后不需要等待答案完全生成,而是通过不断追加的方式实现实时回复的。

F12打开控制台后,我发现在点击发送后,它会发送一个普通的请求。但是回复的方式却不同,它的类型是eventsource。一次请求会不断地获取数据,然后前端的聊天组件会动态地显示回复内容,回复的内容是用Markdown格式来展示的。

SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,第1张

在了解了前面的这些东西后我就萌生了自己写一个小demo的想法。起初,我打算使用openai的接口,并写一个小型的UI组件。然而,由于openai账号申请复杂且存在网络问题,很多人估计搞不定,所以我最终选择了通义千问。通义千问有两个优点:一是它是国内的且目前调用是免费的,二是它提供了Java-SDK和API文档,开发起来容易。

作为后端开发人员,按照API文档调用模型并不难,但真正难到我的是前端UI组件的编写。我原以为市面上会有很多支持EventStream的现成组件,但事实上并没有。不知道是因为这个功能太容易还是太难,总之,对接通义千问只花了不到一小时,而编写一个UI对话组件却花了整整两天的时间!接下来,我将分享一些我之前的经验,希望可以帮助大家少走坑。

首先展示一下我的成品效果

SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,在这里插入图片描述,第2张

二、通义千问开发Key申请

1. 登录阿里云,搜索通义千问

SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,第3张

2. 点击"开通DashScope"

SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,第4张

3. 创建一个API-KEY

SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,第5张

4. 对接流程

(1)API文档地址

https://help.aliyun.com/zh/dashscope/developer-reference/api-details

(2)Java-SDK依赖


  com.alibaba
  dashscope-sdk-java
  2.8.2

三、支持EventStream格式的接口

1. 什么是EventStream

EventStream是一种流式数据格式,用于实时传输事件数据。它是基于HTTP协议的,但与传统的请求-响应模型不同,它是一个持续的、单向的数据流。它可用于推送实时数据、日志、通知等,所以EventStream很适合这种对话式的场景。在Spring Boot中,主要有以下框架和模块支持EventStream格式:

  • Spring WebFlux:Spring WebFlux是Spring框架的一部分,用于构建反应式Web应用程序。
  • Reactor:Reactor是一个基于响应式流标准的库,是Spring WebFlux的核心组件。
  • Spring Cloud Stream:Spring Cloud Stream是一个用于构建消息驱动的微服务应用的框架。

    这次我使用的是reactor-core框架。

    2. 写一个例子

    maven依赖

    
    
      io.projectreactor
      reactor-core
      3.4.6
    
    

    代码如下

    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());
        }
    }
    

    调用一下接口后就可以看到浏览器上在不断地打印时间戳了

    SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,第6张

    四、项目实现

    这个就不BB了,直接贴代码!

    1. 项目结构

    SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,第7张

    2. pom.xml

    
    
        4.0.0
        
            org.springframework.boot
            spring-boot-starter-parent
            2.7.17
             
        
        com.chatrobot
        demo
        0.0.1-SNAPSHOT
        demo
        Demo project for Spring Boot
        
            1.8
        
        
            
            
                com.alibaba
                dashscope-sdk-java
                2.8.2
            
            
            
                io.projectreactor
                reactor-core
                3.4.6
            
            
            
                org.springframework.boot
                spring-boot-starter-web
                
                    
                        logback-classic
                        ch.qos.logback
                    
                
            
        
    
    

    3. 代码

    (1)后端代码

    DemoApplication.java
    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);
        }
    }
    
    EventController.java
    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)前端代码

    chat.html
    
    
    
        
        
        ChatBot
        
    
    
    
    SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,Logo,第8张 通义千问

    另外还有两个头像,大家可以替换成自己喜欢的,好了文章到这里也就结束了,再秀一下我的成品👉

    SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!,第9张