专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
军武次位面  ·  每日囧图丨二师兄​,我们来救你了 ·  2 天前  
解放军报  ·  起床号 ·  3 天前  
51好读  ›  专栏  ›  Java基基

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

Java基基  · 公众号  ·  · 2024-10-13 16:14

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本

来源:juejin.cn/post/
7304265842954469413


一、前言

最近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/

二、通义千问开发Key申请

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

2. 点击"开通DashScope"

3. 创建一个API-KEY

4. 对接流程

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格式的接口

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依赖


<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了,直接贴代码!

1. 项目结构

2. pom.xml


<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>

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.classargs);
    }

}
  • 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
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;
            width100%;
            max-width800px;
            margin50px auto;
            box-shadow0 0 10px rgba(0000.1);
            border-radius8px;
            overflow: hidden;
            font-family"Roboto", sans-serif;
            background#f5f5f5;
        }

        .chat-bot-header {
            backgroundlinear-gradient(to right, #1791ee, #9fdbf1);
            color: white;
            text-align: center;
            padding15px;
            font-size24px;
            font-weight500;
        }

        .chat-bot-messages {
            flex1;
            padding20px;
            min-height400px;
            overflow-y: auto;
        }

        .userName {
            margin0 10px;
        }

        .message-wrapper {
            display: flex;
            align-items: flex-start;
            margin-bottom10px;
            border-radius20px;
        }

        .message-wrapper.user {
            justify-content: flex-end;
            border-radius20px;
        }

        .message-avatar {
            width30px;
            height30px;
            border-radius50%;
            background-color#ccc;
            margin-right10px;
            margin-bottom10px;
            /* 添加这一行 */
            order: -1;
            /* 添加这一行 */
            text-align: right;
        }

        .message-avatar.user {
            background-color: transparent;
            display: flex;
            justify-content: flex-end;
            width100%;
            margin-right0;
            align-items: center;
        }

        .message-avatar.bot {
            background-color: transparent;
            display: flex;
            justify-content: flex-start;
            width100%;
            margin-right0;
            align-items: center;
        }

        .message-avatar-inner.user {
            background-imageurl("./luge.jpeg");
            background-size: cover;
            background-position: center;
            width30px;
            height30px;
            border-radius50%;
        }

        .message-avatar-inner.bot {
            background-imageurl("./logo.svg");
            background-size: cover;
            background-position: center;
            width30px;
            height30px;
            border-radius50%;
        }

        .message {
            padding10px 20px;
            border-radius15px;
            font-size16px;
            background-color#d9edf7;
            order1;
            /* 添加这一行 */
        }

        .bot {
            background-color#e9eff5;
            /* 添加这一行 */
        }

        .user {
            background-color#d9edf7;
            color#111111;
            order1;
            /* 添加这一行 */
        }

        .chat-bot-input {
            display: flex;
            align-items: center;
            border-top1px solid #ccc;
            padding10px;
            background-color#fff;
        }

        .chat-bot-input input {
            flex1;
            padding10px 15px;
            border: none;
            font-size16px;
            outline: none;
        }

        .chat-bot-input button {
            padding10px 20px;
            background-color#007bff;
            border: none;
            border-radius50px;
            color: white;
            font-weight500;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .chat-bot-input button:hover






请到「今天看啥」查看全文