基于SSE技术+通义大模型的Java最佳实践

阿里云官网有很多有优秀的产品,我学校在做课设时用到过OSS对象存储、阿里云服务器、模型服务、短信服务、DNS等

这里主要介绍一下我使用的模型服务过程。

 最终效果

动画

实现过程

一、获取Apikey

大家首先要在阿里云官网注册账号,进入官网主页后,点击搜索‘模型服务灵积

image-20240924142605689

在搜索结构中,点击‘产品控制台

image-20240924142816571

进入控制台后,点击API-KEY管理,创建自己的API-KEY,会生成一个API-KEY,我们必须要保存好!

image-20240924143022021

注意创建后一定要保存好!!!

image-20240924143450159

如果创建失败,大家可以自己去网上找一更详细的教程,我这里只是简单介绍一下。有志者,事竟成!!

二、学习文档开发

在控制台页面点击模型广场,这里有针对很多领域训练的模型,大家可以选择合适的模型开发。我这选择的是通义千问

image-20240924143939382

可以看到这里有两种调用通义千问的方式(OpenAi兼容和DashScope)

image-20240924161933225

我们要想通过OpenAi或DashScope调用的话,首先要获取API-KEY和配置环境变量(可选)。获取API-KEY我们在第一步已经完成,至于配置环境变量,大家可以选择性配置,这样做其实就是确保API-KEY的安全(我这里就没有配置了,直接在项目中使用)。然后还需安装SDK

image-20240924163756316

我们点击安装SDK,然后会跳转到以下界面

image-20240924163859509

大家可以看到Java SDK信息,我们可以先点击以下链接,查找到最新的SDK版本,复制到项目中

https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java

image-20240924164351907

我们再次点击左侧的导航栏,点击‘模型调用’选择‘通义千问

image-20240924175528945

往下滑找到‘DashScope’,选择流式输出,点击Java

image-20240924175835313

可以看到有示例代码、响应格式、请求体等(大家以后可以参考这些更深刻的完善自己的项目)。

image-20240924190334383

三、简单实践

接下来我们分析一下示例代码,从中学习调用大模型的具体过程,最后简单实践测试一下。

我们先看main方法

public static void main(String[] args) {
   try {
       //生成式对象,封装了调用大模型接口,底层使用的是OkHttp库管理
      Generation gen = new Generation();
       //Message这是一个对话消息实体,包括角色和信息,这里是用户角色提问
      Message userMsg = Message.builder().role(Role.USER.getValue()).content("你是谁?").build();
       //将生成式对象和对话消息传入streamCallWithMessage,调用大模型
      streamCallWithMessage(gen, userMsg);
       } catch (ApiException | NoApiKeyException | InputRequiredException  e) {
            logger.error("An exception occurred: {}", e.getMessage());
       }
      System.exit(0);
    }

Generation·这个对象并未在阿里云上找到详情说明,但我们通过查看源码,发现有很多类似于OkHttp中call方法,所以猜测它是封装了okhttp库的请求类。我们可以查看方法调用链,发现它最终使用的是OkHttp3,所以基本确定它就是封装好的调用大模型的请求类,我们只需配置好信息,然后使用它封装的方法即可完成调用。

image-20240924201549112

我们再看streamCallWithMessage方法

public static void streamCallWithMessage(Generation gen, Message userMsg) throws Exception {
        //将对话消息实体传入buildGenerationParam方法获取参数对象
        GenerationParam param = buildGenerationParam(userMsg);
        //调用生成式对象中call方法,完成大模型调用,结果保存在result中
        Flowable<GenerationResult> result = gen.streamCall(param);
        //我们服务器与阿里大模型服务之间也采用了sse,所以大模型服务会将已经回答消息流式推送我们的服务器
        result.blockingForEach(message -> handleGenerationResult(message));
    }

result.blockingForEach这个方法会监听阿里大模型服务推送过来的消息,只要有新的消息就会调用handleGenerationResult方法,将大模型回答的消息打印出来

 private static void handleGenerationResult(GenerationResult message) {
        System.out.println(JsonUtils.toJson(message));
 }
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":1,"total_tokens":12},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"我是"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":2,"total_tokens":13},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"通"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":3,"total_tokens":14},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"义"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":8,"total_tokens":19},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"千问,由阿里"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":16,"total_tokens":27},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"云开发的AI助手。我被"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":24,"total_tokens":35},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"设计用来回答各种问题、提供信息"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":32,"total_tokens":43},"output":{"choices":[{"finish_reason":"null","message":{"role":"assistant","content":"和与用户进行对话。有什么我可以"}}]}}
{"requestId":"xxx","usage":{"input_tokens":11,"output_tokens":36,"total_tokens":47},"output":{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"帮助你的吗?"}}]}}

我们刚刚讨论了调用大模型服务的过程,我们再来看看它需要的配置参数,我们可以先看它的方法

private static GenerationParam buildGenerationParam(Message userMsg) {
        return GenerationParam.builder()
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .model("qwen-turbo")
                .messages(Arrays.asList(userMsg))
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .incrementalOutput(true)
                .build();
}

我们可以看到需要设置有:

  • apiKey(我们之前申请的秘钥)

  • model(调用的大模型名称)

  • messages(对话信息)

  • resultFormat(设置返回的消息格式)

  • incrementalOutput(流式增量推送,这里选择true表开启)

apiKey我们已经申请过了就不再追诉,resultFormat和incrementalOutput我们不需要更改(但是如果有需要的话我们可以像之前那样找到文档描述,结合实际情况去修改)

image-20240924210214918

我们要注意的是调用的大模型名model对话消息messages,大家知道通义千问系列的大模型有多种,比如

qwen-plus、qwen-turbo...大家可以在模型列表中自行查看

image-20240924211028750

而对话消息messages,我们参考官方文档教程,重点关注以下三种对话信息类型

image-20240924211910653

我们可以使用Json格式列表展示对话结构,让大家对此有更好的理解

#对话信息messages
messages = [
    {'role': 'system', 'content': 'You are a helpful assistant.'},   --指定模型角色
    {'role': 'user', 'content': '你是谁?'}                          --用户提问
    {'role':'assistant','content': '我是智能助手...'}                --Ai回答
    {...}                                                            --用户提问
    {...}                                                            --Ai回答
    ]

到这里相信大家已经对我们调用大模型的过程有了一定的了解,我们再回过头去看实例代码是不是清晰了许多。

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.utils.JsonUtils;
import io.reactivex.Flowable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
​
import java.util.Arrays;
​
public class Main {
    private static final Logger logger = LoggerFactory.getLogger(Main.class);
​
    private static void handleGenerationResult(GenerationResult message) {
        System.out.println(JsonUtils.toJson(message));
    }
​
    public static void streamCallWithMessage(Generation gen, Message userMsg)throws Exception {
        GenerationParam param = buildGenerationParam(userMsg);
        Flowable<GenerationResult> result = gen.streamCall(param);
        result.blockingForEach(message -> handleGenerationResult(message));
    }
​
    private static GenerationParam buildGenerationParam(Message userMsg) {
        return GenerationParam.builder()
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .model("qwen-plus")
                .messages(Arrays.asList(userMsg))
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .incrementalOutput(true)
                .build();
    }
​
    public static void main(String[] args) {
        try {
            Generation gen = new Generation();
            Message userMsg = Message.builder().role(Role.USER.getValue()).content("你是谁?").build();
            streamCallWithMessage(gen, userMsg);
        } catch (Exception e) {
            logger.error("An exception occurred: {}", e.getMessage());
        }
        System.exit(0);
    }
}

我们在项目pom文件中导入SDK,编写一个测试案例,只需将以上代码中的apiKey替换成你自己的,点击运行,查看结果

image-20240924213932262

image-20240924213952915

如图实现了简单调用

四、进阶文档参考

我们在模型服务灵积控制台页面,左侧导航栏中找到‘文档中心’,点击后会出现右侧弹窗,我们点击‘文档中心打开’

image-20240925084412357

我们在文档中心页面中,在左侧导航栏位置找到‘实践教程’,点击‘java sdk最佳实践’,这里可以看到官方对java sdk的最佳使用方式

image-20240925084649742

官方对我们生成式对象Generation最佳使用介绍

image-20240925085203485

对象池示例(org.apache.commons:commons-pool2)

public class PooledDashScopeObjectFactory extends BasePooledObjectFactory<Generation> {

    //创建生成式对象
    @Override
    public Generation create() throws Exception {
        return new Generation();
    }

    //将创建好的Generation对象包装成DefaultPooledObject<Generation>对象
    //包装后的对象可以被对象池管理和维护,包括对象的借用、归还等操作。
    @Override
    public PooledObject<Generation> wrap(Generation obj) {
        return new DefaultPooledObject<>(obj);
    }    
}

对象池使用

public class PooledDashScopeObjectUsage {
    public static void main(String[] args) throws Exception {
        
        //创建对象池实例
        PooledDashScopeObjectFactory pooledDashScopeObjectFactory =
                new PooledDashScopeObjectFactory();
        
        //对象池配置实例
        GenericObjectPoolConfig<Generation> config = new GenericObjectPoolConfig<>();
        // 对于语音服务,websocket协议,保持下面值相同
        config.setMaxTotal(32);
        config.setMaxIdle(32);
        config.setMinIdle(32);
        
        //对象池装配该配置
        GenericObjectPool<Generation> generationPool =
                new GenericObjectPool<>(pooledDashScopeObjectFactory, config);
        
        //挂一个空的Generation,便于后面从对象池中取实例
        Generation gen = null;
        
        //设置apiKey
         Constants.apiKey="你的apiKey";
        
        try {
            //创建系统对话消息对象
            Message systemMsg = Message.builder().role(Role.SYSTEM.getValue())
                    .content("You are a helpful assistant.").build();
            
            //创建用户对话消息对象
            Message userMsg = Message.builder().role(Role.USER.getValue()).content("你好").build();
            
            //调用大模型时需要的配置参数对象(这里我们之前介绍过)
            GenerationParam param = GenerationParam.builder()
                    .model("qwen-plus")
                    .messages(Arrays.asList(systemMsg, userMsg))
                 .resultFormat(GenerationParam.ResultFormat.MESSAGE).topP(0.8).enableSearch(true)
                    .build();
            
            //从对象池中取出一个生成式对象实例
            gen = generationPool.borrowObject();
            //调用大模型回答
            GenerationResult result = gen.call(param);
            //打印
            System.out.println(result);
        } finally {
            //调用结束归还到对象池
            if (gen != null) {
                generationPool.returnObject(gen);
            }
        }
        System.out.println("completed");
        generationPool.close();
    }

}

将以上代码复制到项目中测试时,代码中替换你自己的apiKey,即可完成简单调用。

我们已经确定Generation底层使用的就是OkHttp库管理连接,所以我也可以修改连接配置,我们参考官方案例

image-20240925085303588

Constants.connectionConfigurations = ConnectionConfigurations.builder()
        .connectTimeout(Duration.ofSeconds(120))  // set connection timeout, default 120s
        .readTimeout(Duration.ofSeconds(300)) // set read timeout, default 300s
        .writeTimeout(Duration.ofSeconds(60)) // set read timeout, default 60s
        .connectionIdleTimeout(Duration.ofSeconds(300)) // connection pool idle timeout, default 300s
        .connectionPoolSize(32) // idle connections in the okhttp connection pool.
        .maximumAsyncRequests(32)  // async requests limit. 
        .maximumAsyncRequestsPerHost(32) // async request host limit.
        .proxyHost("The http proxy host") // set proxy host, if set will use proxy. default null.
        .proxyPort(443) // set proxy port, default 443
        .proxyAuthenticator(null) // you can customize you proxy authenticator. default null.
        .build();
// 更多 connectionPoolSize and connectionIdleTimeout, 可以参考 
// ref: https://square.github.io/okhttp/3.x/okhttp/okhttp3/ConnectionPool.html
// maximumAsyncRequests and maximumAsyncRequestsPerHost 只对streamCall, audio相关服务,以及您自行设置
// 的使用websocket连接对象,详细参考:https://square.github.io/okhttp/3.x/okhttp/okhttp3/Dispatcher.html

五、项目实践

接下来我们创建一个项目,在pom文件中引入SDK

image-20240925093631018

      <!--阿里云模型服务SDK-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.16.3</version>
        </dependency>

       <!--对象池管理依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.8.0</version>
        </dependency>

我们新建config包,创建MyCofig类如图

image-20240925094549251

代码:

@Configuration
public class MyConfig {

    @Value("${aliyunInfo.apikey}")
    private String apiKey;

    @PostConstruct
    public void config(){
        //设置APIKEY
        Constants.apiKey=apiKey;
    }
}

我们继续创建一个vo包,在该包下创建QianWenModelType枚举类

image-20240925095626653

@Getter
public enum QianWenModelType {

    /**
     * 模型名和描述
     */
    QWEN_TOURBO("qwen-turbo", "千问Touber"),
    QWEN_PLUS("qwen-plus", "千问Plus");

    private final String modelName;
    private final String modelDescription;

    QianWenModelType(String modelName, String modelDescription) {
        this.modelName = modelName;
        this.modelDescription = modelDescription;
    }
}

然后我们创建一个entity包,并创建ChatClientEntity、MessageEntity两个类

image-20240925100855276

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatClientEntity {
    private String uuid;
    private List<MessageEntity> messages;
}

image-20240925101301232

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageEntity {
    private String role;
    private String content;
}

接下来我们还需创建utils包,里面有两个类

  • PooledDashScopeObjectFactory类,这个类是官网案例,也就是我们前面的讨论的对象池

    image-20240925101608286

  • QianWenReplyUtil类

    @Slf4j
    public class QianWenReplyUtil {
    
        /**
         * 对象池
         */
        private static GenericObjectPool<Generation> generationPool;
    
    
        /**
         * 设置参数(这个方法我们之前已经讨论过)
         * @param messages 消息
         * @return 参数实例
         */
        public static GenerationParam createGenerationParam(List<Message> messages) {
            return GenerationParam.builder()
                    .model(QianWenModelType.QWEN_TOURBO.getModelName())
                    .messages(messages)
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .topP(0.8)
                    .incrementalOutput(true)
                    .build();
        }
    
        
        /**
         * 消息转换(我们前后端传输使用的是自定义的MessageEntity类,需要将其转换成Message,即规定的类)
         */
        public static List<Message> createMessage(List<MessageEntity> messageEntities) {
           List<Message> messages = new ArrayList<>();
           for (MessageEntity messageEntity : messageEntities) {
            Message message = Message.builder().role(messageEntity.getRole()).content(messageEntity.getContent()).build();
            messages.add(message);
           }
           return messages;
        }
    
    
        /**
         * 开启连接池(这里我们使用的是单例模式)
         */
        public static GenericObjectPool<Generation> getGenerationPool() {
            if (Objects.isNull(generationPool)) {
                //开启连接池
                PooledDashScopeObjectFactory pooledDashScopeObjectFactory = new PooledDashScopeObjectFactory();
                GenericObjectPoolConfig<Generation> config = new GenericObjectPoolConfig<>();
                // 对于语音服务,websocket协议,保持下面值相同
                config.setMaxTotal(32);
                config.setMaxIdle(32);
                config.setMinIdle(32);
                generationPool = new GenericObjectPool<>(pooledDashScopeObjectFactory, config);
            }
            return generationPool;
        }
    
    
        /**
         * 生成式文本敏感词过滤(大家根据自己实际情况去编写)
         */
        public static String filterSensitiveWords(String text) {
            // TODO
            return text;
        }
    }

最后创建一个Controller包,并创建QianWenAiController类

如果说大家对sse技术不是很了解,这推荐参考:

@RestController
@Slf4j
@CrossOrigin
public class QianWenAiController {

    /**
     * 客户端sse连接池
     */
    public static Map<String, SseEmitter> sseEmitterMap = new HashMap<>();

    /**
     * 开启sse连接
     */
    @GetMapping("/open")
    public SseEmitter startSse(@RequestParam String uuid) {
        // 判断sse连接池中是否存在该连接
        SseEmitter sseEmitter = sseEmitterMap.get(uuid);
        if (Objects.isNull(sseEmitter)) {
            sseEmitter = new SseEmitter(0L);
            sseEmitter.onCompletion(() -> sseEmitterMap.remove(uuid));
            sseEmitter.onTimeout(() -> sseEmitterMap.remove(uuid));
            sseEmitterMap.put(uuid, sseEmitter);
        }
        log.info("客户端{}开启了sse连接", uuid);
        return sseEmitter;
    }

    /**
     * 关闭sse连接
     */
    @GetMapping("/close")
    public void closeSse(@RequestBody String uuid) {
        // 判断sse连接池中是否存在该连接
        SseEmitter sseEmitter = sseEmitterMap.get(uuid);
        if (Objects.nonNull(sseEmitter)) {
            sseEmitter.complete();
            sseEmitterMap.remove(uuid);
        }
    }

    /**
     * 推送消息,调用大模型流式回答接口,将返回的消息推送
     */
    @PostMapping("/push")
    public void pushMessage(@RequestBody ChatClientEntity chatClientEntity, HttpServletResponse response) {
        // 关闭Nginx缓存,直接推流
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("X-Accel-Buffering", "no");
        // 流式事件流响应
        response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
        // 判断sse连接池中是否存在该连接
        SseEmitter sseEmitter = sseEmitterMap.get(chatClientEntity.getUuid());
        Generation gen = null;
        //异步回答
        if (Objects.nonNull(sseEmitter)) {
            try {
                // 从对象池中获取一个连接
                gen = QianWenReplyUtil.getGenerationPool().borrowObject();
                
                //semaphore用于标记异步调用是否完成或出错
                Semaphore semaphore = new Semaphore(0);
                
                //保存完整的回答信息
                StringBuilder fullContent = new StringBuilder();
                
                // 创建参数实例
                GenerationParam param = QianWenReplyUtil.createGenerationParam(QianWenReplyUtil.createMessage(chatClientEntity.getMessages()));
                // 调用大模型
                gen.streamCall(param, new ResultCallback<>() {
                    @Override
                    public void onEvent(GenerationResult message) {
                        fullContent.append(message.getOutput().getChoices().get(0).getMessage().getContent());
                        log.info("生成信息:{}", JsonUtils.toJson(message));
                        // 推送消息
                        // 生成式文本敏感词过滤(此处代码省略)
                        try {
                            sseEmitter.send(JsonUtils.toJson(message));
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    @Override
                    public void onError(Exception err) {
                        log.error("发生错误{}", err.getMessage());
                        try {
                            sseEmitter.send("服务器正在升级中,请稍后再试!");
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                        semaphore.release();
                    }
                    @Override
                    public void onComplete() {
                        log.info("回答完成");
                        semaphore.release();
                    }
                });
                log.info("Full content: \n{}", fullContent);
            }catch (Exception e){
                log.error("发生错误{}", e.getMessage());
            }finally {
                //异步线程调用完成,执行主线程,确保gen对象归还到对象池
                semaphore.acquire();
                QianWenReplyUtil.getGenerationPool().returnObject(gen);
            }
        }
    }
}

这就是我们后端项目的实现代码,下面是前端实现代码,使用的是vue3,大家可以自行复制到项目中直接使用

<template>
    <transition name="el-zoom-in-center">
        <div class="robot-container" v-show="show">
            <div class="window-container" ref="scrollDiv">
                <div v-if="chatData.length !== 0" v-for="(item, index) in chatData ">
                    <div :class="{
                        'info-card-left': item.role === 'assistant',
                        'info-card-right': item.role === 'user'
                    }">
                        <!--头像  -->
                        <div class="user-avatar" v-if="item.role === 'assistant'">
                            <el-avatar shape="square" :size="50"
                                src="https://i03piccdn.sogoucdn.com/c477d031f53cfe05" />
                        </div>

                        <!-- 内容 -->
                        <div class="user-content">
                            <div v-html="markdown.render(item.content)"></div>
                        </div>

                        <!-- 头像 -->
                        <div class="user-avatar" v-if="item.role !== 'assistant'">
                            <el-avatar shape="square" :size="50" :src="userAvatar" />
                        </div>
                    </div>
                </div>
            </div>

            <div class="bottom">
                <el-input :disabled="isReplying" v-model="content" style="width: 98%;margin-left: 10px;" type="textarea"
                    placeholder="输入提问信息" />
                <div class="send">
                    <el-button :disabled="isReplying" type="primary" style="width: 150px;margin-top: 10px;"
                        :icon="Promotion" @click="handleSend">提问</el-button>
                </div>
            </div>
        </div>
    </transition>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import { Promotion } from '@element-plus/icons-vue'
import axios from 'axios'
import { v4 as uuid4 } from 'uuid'
import store from '@/store';
import MarkdownIt from 'markdown-it';

const markdown = new MarkdownIt()
const userAvatar = ref('https://pic4.zhimg.com/80/v2-5287de47e7e537125b7200fb99b84d53_720w.webp')
const uuid = ref('')
const source = ref(null)
const content = ref('')
const isReplying = ref(false)
const replyContent = ref('')
const show = ref(false)
const chatData = ref([])
const scrollDiv = ref(null)

//滚动到底部
const scrollToBottom = () => {
    //滚动到底部
    let scrollElem = scrollDiv.value;
    scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' });
}

/** 生成一个以字母开始的唯一标识符 */
const generateLetterPrefixedId = () => {
    let id = '';
    do {
        // 生成一个UUID并取其前几个字符  
        id = uuid4().substring(0, 8).toUpperCase();
        // 如果第一个字符不是字母,则继续循环  
    }
    while (!/^[a-zA-Z]/.test(id));
    return id;
}

//发送前处理数据
const handleData = () => {
    isReplying.value = true
    chatData.value.push({
        role: 'user',
        content: content.value
    })
    chatData.value.push({
        role: 'assistant',
        content: '正在思考...'
    })
    content.value = ''
    replyContent.value = ''
}

//sse 连接
const SSE = () => {
    if (window.EventSource) {
        
        // 建立连接
        source.value = new EventSource('http://localhost:8080/open?uuid=' + uuid.value)
        
        /**
         * 连接一旦建立,就会触发open事件
         */
        source.value.onopen = function (e) {
            console.log('建立连接', e)
        }
        /**
         * 客户端收到服务器发来的数据
         */
        source.value.onmessage = function (e) {
            // 滚动到底部
            scrollToBottom()
            console.log(JSON.parse(e.data).output.choices[0].message.content)
            replyContent.value = replyContent.value + JSON.parse(e.data).output.choices[0].message.content
            chatData.value[chatData.value.length - 1].content = replyContent.value
            store.commit('setMessages', chatData.value)
            isReplying.value = false
        }
       
        /**
         * 如果发生通信错误(比如连接中断),就会触发error事件
         */
        source.value.onerror = function (e) {
            if (e.readyState === EventSource.CLOSED) {
                console.log('连接关闭')
            } else {
                console.log(e)
            }
        }
    } else {
        console.log('浏览器不支持SSE')
    }
}

// 发送消息
const handleSend = async () => {
    // 处理数据
    await handleData()
    // 开启sse连接
    SSE()
    // 滚动到底部
    scrollToBottom()
    await axios({
        url: 'http://localhost:8080/push',
        headers: {
            'Content-Type': 'application/json'
        },
        data: {
            'uuid': uuid.value,
            'messages': chatData.value
        },
        method: 'post',
    })
}

onMounted(() => {
    //模仿登录用户的唯一标识
    uuid.value = generateLetterPrefixedId()
    chatData.value = store.getters.getMessages
    show.value = true
})

</script>

<style scoped>
/* 隐藏滚动条 */
::-webkit-scrollbar {
    width: 0;
}

.robot-container {
    margin: 30px auto;
    width: 900px;
    height: 660px;
    border-radius: 10px;
    background-color: rgb(184, 203, 239);
    overflow: hidden;
    box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
    transition: all 0.5s;
    padding: 5px;

    .window-container {
        width: 99%;
        margin: 0 auto;
        margin-top: 10px;
        height: 440px;
        padding: 10px;
        background-color: rgb(245, 245, 245);
        overflow: scroll;

        .info-card-left {
            width: 70%;
            height: 20%;
            display: flex;
            justify-content: left;
            margin-bottom: 10px;

            .user-avatar {
                display: flex;
                align-items: center;
                margin-left: 10px;
                height: 50px;
                box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
            }

            .user-content {
                background-color: white;
                border-radius: 10px;
                box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
                padding: 10px;
                margin-left: 10px;
                font-size: 16px;
                align-items: center;
                display: flex;
            }

            .user-content>>>h1 {
                font-weight: bold;
            }

            .user-content>>>h2 {
                font-weight: bold;
            }

            .user-content>>>h3 {
                font-weight: bold;
            }

            .user-content>>>h4 {
                font-weight: bold;
            }
        }

        .info-card-right {
            width: 70%;
            height: 20%;
            display: flex;
            justify-content: right;
            margin-bottom: 10px;
            margin-left: auto;

            .user-avatar {
                display: flex;
                align-items: center;
                height: 50px;
                margin-right: 10px;
                box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
            }

            .user-content {
                background-color: white;
                border-radius: 10px;
                box-shadow: 0 2px 3px rgb(0 0 0 / 10%);
                padding: 10px;
                margin-right: 10px;
                font-size: 16px;
                background-color: rgb(71, 225, 99);
                align-items: center;
                display: flex;
                text-align: right;
            }
        }
    }

    .bottom {
        width: 99%;
        margin: 0 auto;
        padding-top: 10px;
        height: 180px;
        background-color: rgb(250, 250, 250);

        .send {
            width: 100%;
            height: 50px;
            display: flex;
            justify-content: right;
            align-items: center;
            padding-right: 10px;
        }
    }

    ::v-deep .el-textarea__inner {
        height: 120px;
        font-size: 18px;
    }
}
</style>

七、源码地址

有用的话,朋友,请点个赞或关注,谢谢!

一眼万年/chat_ai_qianwen - 码云 - 开源中国 (gitee.com)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值