springboot vue 实现AI智能体应用

演示地址:https://ai.ywonchian.cn/

什么是百炼

阿里云的大模型服务平台百炼是一站式的大模型开发及应用构建平台。不论是开发者还是业务人员,都能深入参与大模型应用的设计和构建。您可以通过简单的界面操作,在5分钟内开发出一款大模型应用,或在几小时内训练出一个专属模型,从而将更多精力专注于应用创新。

前期准备

注册账号:如果没有阿里云账号,您需要先注册阿里云账号。

开通百炼:使用阿里云主账号前往百炼控制台,如果页面顶部显示以下消息,您需要开通百炼的模型服务,以获得免费额度

获取API Key:前往API-KEY页面,单击创建我的API-KEY,然后在已创建的API Key操作列,单击查看,获取API KEY,用于通过API调用大模型。

基础使用

步骤 1:配置Java环境

创建 springboot 项目,安装 DashScope Java SDK

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dashscope-sdk-java</artifactId>
    <!-- 请将 'the-latest-version' 替换为最新版本号:https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
    <version>the-latest-version</version>
</dependency>

步骤 2:调用大模型API

import java.util.Arrays;
import java.lang.System;
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.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
public class Main {
    public static GenerationResult callWithMessage() throws ApiException, NoApiKeyException, InputRequiredException {
        Generation gen = new Generation();
        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()
                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                // 模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models
                .model("qwen-plus")
                .messages(Arrays.asList(systemMsg, userMsg))
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .build();
        return gen.call(param);
    }
    public static void main(String[] args) {
        try {
            GenerationResult result = callWithMessage();
            System.out.println(result.getOutput().getChoices().get(0).getMessage().getContent());
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            System.err.println("错误信息:"+e.getMessage());
            System.out.println("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code");
        }
        System.exit(0);
    }
}

注:关于基础是使用就不多描述,参考官网文档

API参考

进阶使用

大语言模型无法直接回答私有知识领域的问题,但您可以借助百炼的智能体应用构建能力和私有知识文档,零代码构建一个能回答私有领域问题的大模型问答应用

构建百炼智能体应用

官方文档地址:https://help.aliyun.com/zh/model-studio/build-knowledge-base-qa-assistant-without-coding

获取应用ID

创建应用:

[图片上传失败...(image-6efc35-1743649406275)]

获取ID

[图片上传失败...(image-5ed6a0-1743649406275)]

代码实现

后端 SpringBoot

**SseEmiter **

SseEmiter 是 Spring Framework 提供的一个类,用于支持服务器发送事件(Server-Sent Events,SSE)。SSE 是一种基于 HTTP 的协议,允许服务器向客户端推送实时数据,而不需要客户端不断地轮询服务器。SSE 特别适用于需要实时更新数据的场景,例如实时通知、实时数据流等。

主要特点

单向通信:SSE 是单向的,服务器可以向客户端推送数据,但客户端不能向服务器发送数据。
基于 HTTP:SSE 使用标准的 HTTP 协议,因此不需要额外的协议支持。
自动重连:如果连接中断,客户端会自动尝试重新连接服务器。
事件流格式:SSE 使用简单的文本格式来传输数据,每条消息以 data: 开头,并以两个换行符 \n\n 结束。

SseUtils.java

import com.ywonchian.common.dto.ResponseDto;
import com.ywonchian.utils.service.RedissonService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author ywonchian_z@163.com
 * @date 2025年02月27日 17:11
 * @Description: SSE工具类
 */

@Slf4j
@Component
public class SseUtils {

    private static final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    @Resource
    private RedissonService redissonService;

    /**
     * 创建连接
     */
    public SseEmitter connect(String userId) {
        // 删除已有连接,确保单用户单连接
        sseEmitterMap.compute(userId, (key, oldEmitter) -> {
            if (oldEmitter != null) {
                return oldEmitter;
            }
            return createNewEmitter(userId);
        });
        log.info("创建SSE连接成功,用户:{},当前连接数:{}", userId, sseEmitterMap.size());
        return sseEmitterMap.get(userId);
    }

    /**
     * 创建新的 SseEmitter 并配置回调
     */
    private SseEmitter createNewEmitter(String userId) {
        SseEmitter emitter = new SseEmitter(24 * 60 * 60 * 1000L);  // 24小时超时
        try {
            emitter.send(SseEmitter.event().id("").data("连接成功"));
        } catch (IOException e) {
            log.warn("用户{}连接初始化失败: {}", userId, e.getMessage());
            return null;
        }

        emitter.onCompletion(() -> closeConnection(userId, "正常结束"));
        emitter.onTimeout(() -> closeConnection(userId, "超时断开"));

        return emitter;
    }

    /**
     * 发送消息
     */
    public boolean sendMessage(String userId, String messageId, Object message) {
        SseEmitter emitter = sseEmitterMap.get(userId);
        if (emitter == null) {
            log.warn("用户{}未连接,消息发送失败", userId);
            return false;
        }

        try {
            emitter.send(SseEmitter.event().id(messageId).data(ResponseDto.success(message)));
            log.info("消息推送成功 - 用户:{},消息:{}", userId, message);
            return true;
        } catch (IOException e) {
            log.error("消息推送失败 - 用户:{},异常:{}", userId, e.getMessage());
            closeConnection(userId, "推送异常");
            return false;
        }
    }

    /**
     * 删除连接
     */
    public void deleteUser(String userId) {
        closeConnection(userId, "主动删除");
    }

    /**
     * 关闭连接
     */
    private void closeConnection(String userId, String reason) {
        RLock lock = redissonService.getRLock("SSE_LOCK_" + userId);
        boolean lockResult = false;
        try {
            lockResult = lock.tryLock();
            // 未获取到锁不执行
            if (!lockResult) {
                return;
            }
            SseEmitter emitter = sseEmitterMap.remove(userId);
            if (emitter != null) {
                emitter.complete();
                log.info("连接关闭 - 用户:{},原因:{}", userId, reason);
            }
        } catch (Exception e) {
            // 忽略 "Broken pipe" 异常
            if (e.getCause() instanceof IOException && "Broken pipe".equals(e.getCause().getMessage())) {
                log.debug("连接关闭时出现 Broken pipe 异常,忽略:{}", userId);
            } else {
                log.error("关闭连接异常 - 用户:{},异常:{}", userId, e.getMessage());
            }
        } finally {
            if (lockResult) {
                lock.unlock();
            }
        }
    }

    /**
     * 获取当前连接信息
     */
    public Map<String, SseEmitter> listSseConnect() {
        return sseEmitterMap;
    }

    /**
     * SSE心跳检测,防止超时断开
     */
    @Scheduled(cron = "0/10 * * * * ?")
    public void sseHeartBeatCheck() {
        sseEmitterMap.forEach((userId, emitter) -> {
            try {
                if (emitter != null) {
                    emitter.send(SseEmitter.event().id("heartbeat").data("heartbeat"));
                    log.debug("心跳检测成功 - 用户:{}", userId);
                }
            } catch (IOException e) {
                log.warn("心跳检测异常 - 用户:{},异常:{}", userId, e.getMessage());
                closeConnection(userId, "心跳失败");
            }
        });
    }
}

SseController.java

import com.ywonchian.chat.utils.SseUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * @author ywonchian_z@sntransmt.com
 * @date 2025年02月27日 17:12
 * @Description: SSE 控制器
 */


@RestController
@RequestMapping("/sse")
@RequiredArgsConstructor
@Tag(name = "sse控制器")
public class SseController {

    private final SseUtils sseUtils;


    //设置Content-Type:text/event-stream
    @Operation(description = "创建sse连接")
    @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter createSseConnect(@RequestParam(name = "clientId") String clientId) {
        return sseUtils.connect(clientId);
    }

}

流式多轮会话

AiChatController

import com.ywonchian.chat.dto.AliChatDTO;
import com.ywonchian.chat.sevice.AiChatService;
import com.ywonchian.common.dto.ResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author ywonchian_z@sntransmt.com
 * @date 2025年02月27日 14:28
 * @Description: AI会话相关接口
 */

@RestController
@RequestMapping("/ai")
@Slf4j
@Tag(name = "AI会话相关接口")
public class AiChatController {

    @Resource
    private AiChatService aiChatService;
    

    @Operation(description = "AI会话-流式生成")
    @PostMapping(value = "/chat-stream")
    public ResponseDto<String> stream(@RequestBody AliChatDTO dto) {
        aiChatService.stream(dto);
        return ResponseDto.success();
    }

}

AiChatService.java

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dashscope.app.Application;
import com.alibaba.dashscope.app.ApplicationOutput;
import com.alibaba.dashscope.app.ApplicationParam;
import com.alibaba.dashscope.app.ApplicationResult;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.google.gson.JsonObject;
import com.ywonchian.chat.dto.AliChatDTO;
import com.ywonchian.chat.utils.SseUtils;
import com.ywonchian.utils.config.ApiResultException;
import io.reactivex.Flowable;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

/**
 * @author ywonchian_z@sntransmt.com
 * @date 2025年03月21日 15:05
 * @Description: AI会话实现类
 */

@Service
@Slf4j
public class AiChatService {

    @Resource
    private SseUtils sseUtils;

    /**
     * ali-api-key
     */
    @Value("${ai-model.ali.apiKey:''}")
    private String apiKey;

    /**
     * ali-appId
     */
    @Value("${ai-model.ali.appId:''}")
    private String appId;


    public void stream(@RequestBody AliChatDTO dto) {
        // 调用流式生成逻辑
        ApplicationParam param = buildApplicationParam(dto);
        Application application = new Application();
        try {
            Flowable<ApplicationResult> result = application.streamCall(param);
            // 处理流式输出
            result.blockingForEach(data -> {
                if (data != null && data.getOutput() != null) {
                    //发送给前端
                    boolean b = sseUtils.sendMessage(dto.getClientId(), String.valueOf(System.currentTimeMillis()), data.getOutput());
                    if (!b) {
                        throw new ApiResultException(400, "未建立连接或连接失败,请重试");
                    }
                }
            });
        } catch (Exception e) {
            log.error("stream-error:{}", e.getMessage());
            throw new ApiResultException(e.getMessage());
        }

    }

    private ApplicationParam buildApplicationParam(AliChatDTO dto) {
        ApplicationParam param = ApplicationParam.builder()
                .apiKey(apiKey)
                .appId(appId)
                .incrementalOutput(true)
                .header("X-DashScope-SSE", "enable")
                .hasThoughts(dto.isHasThoughts())
                .build();
        if (StringUtils.isNotEmpty(dto.getAppId())) {
            param.setAppId(dto.getAppId());
        } else {
            param.setAppId(appId);
        }
        if (StringUtils.isNotEmpty(dto.getSessionId())) {
            param.setSessionId(dto.getSessionId());
        }
        if (StringUtils.isNotEmpty(dto.getContent())) {
            param.setPrompt(dto.getContent());
        } else {
            param.setPrompt("您好");
        }
        JsonObject bizParam = new JsonObject();
        if (StringUtils.isNotBlank(dto.getAge())) {
            bizParam.addProperty("age", dto.getAge());
        }
        if (StringUtils.isNotBlank(dto.getSex())) {
            bizParam.addProperty("sex", dto.getSex());
        }
        if (ObjectUtil.isNotNull(bizParam)) {
            param.setBizParams(bizParam);
        }
//        if(dto.isHasThoughts()){
//            param.setHasThoughts(true);
//        }else {
//            param.setHasThoughts(false);
//        }
        if (CollUtil.isNotEmpty(dto.getImageList())) {
            param.setImages(dto.getImageList());
        }
        return param;
    }

}

前端 Vue 核心代码

sse.js

import { notification } from 'ant-design-vue';
import { h } from 'vue';
import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';

const BASE_URL = 'http://127.0.0.1:7201/chat/sse/connect?clientId='

class SSEClient {
    constructor(clientId, onMessage, onError, autoConnect = true) {
        this.url = BASE_URL + clientId;
        this.eventSource = null;
        this.onMessage = onMessage;
        this.onError = onError;
        this.autoConnect = autoConnect;
        this.reconnectDelay = 3000; // 3秒自动重连
        this.isConnected = false; // 防止重复连接
        if (this.autoConnect) {
            this.connect();
        }
    }

    // 连接 SSE
    connect() {
        if (this.isConnected) return; // 防止重复连接
        this.isConnected = true;
        this.eventSource = new EventSource(this.url);

        this.eventSource.onopen = () => {
            console.log("✅ SSE 连接成功");
        };

        this.eventSource.onmessage = (event) => {
            if (this.onMessage) this.onMessage(event.data);
        };

        this.eventSource.onerror = () => {
            notification.open({
                message: 'SSE 连接错误,尝试重连...',
                duration: 3,
                icon: h(ExclamationCircleOutlined, { style: 'color:rgb(221, 59, 72)' }),
            });
            console.error("❌ SSE 连接错误,尝试重连...");
            this.disconnect();
            setTimeout(() => this.connect(), this.reconnectDelay);
        };
    }

    // 断开 SSE
    disconnect() {
        if (this.eventSource) {
            this.eventSource.close();
            this.eventSource = null;
            this.isConnected = false;
            console.log("🔴 SSE 连接已断开");
        }
    }
}

// 单例模式,保证全局唯一的 SSE 连接
let sseInstance = null;

export const initSSE = (clientId, onMessage) => {
    console.log("🚀 SSE 初始化");
    if (!sseInstance) {
        sseInstance = new SSEClient(clientId, onMessage);
    }
};

export const getSSEInstance = () => sseInstance;

export const closeSSE = () => {
    if (sseInstance) {
        sseInstance.disconnect();
        sseInstance = null;
    }
};


在App.vue初始化 Sse连接

import { ref, onMounted, onBeforeUnmount } from 'vue'
import { initSSE, closeSSE } from '@/utils/sse'

onBeforeUnmount(() => {
  closeSSE()
});

onMounted(() => {
  initSSE(id, (message) => {
    console.log('🌍 [SSE] 新消息:', message)
  })
})


在聊天页面进行发送消息和监听消息

import { nextTick, ref, onMounted, onBeforeUnmount } from 'vue';
import { getSSEInstance } from '@/utils/sse.js'

onMounted(() => {
    sseClient = getSSEInstance()
    if (sseClient) {
        console.log("开始监听消息", sseClient);
        sseClient.onMessage = (message) => {
            handleSseMessage(message);
        }
    }
});

// 处理消息
const handleSseMessage = (message) => {
    if (message == '连接成功' || message == 'heartbeat') {
        return;
    }
    const data = JSON.parse(message);
    console.log('data:', data)
    sessionId.value = data.data?.sessionId;
};

// 发送消息
const sendMessage = () => {
    if (!newMessage.value.trim()) {
        return; // 防止发送空消息
    }
    if (isReplying.value) {
        notification.open({
            message: '正在回复中请稍后...',
            duration: 3,
            icon: h(ExclamationCircleOutlined, { style: 'color:rgb(221, 59, 72)' }),
        });
        return;
    }
    if (!sseClient) {
        notification.open({
            message: '聊天未连接,请刷新重试',
            duration: 3,
            icon: h(ExclamationCircleOutlined, { style: 'color:rgb(221, 59, 72)' }),
        });
        return;
    }
    const data = {
        clientId: userStore.clientId,
        type: 1,
        sessionId: sessionId.value,
        content: newMessage.value,
        appId: prop.selectModle.appId,
        hasThoughts: true
    };
    messages.value.push(data);
    // 确保 DOM 更新后再滚动
    nextTick(() => {
        scrollToBottom();
    })
    newMessage.value = '';
    isReplying.value = true; // 禁用输入框
    // 设置为 loading 状态
    isLoading.value = true;
    sendMsg(data)
        .then(() => {
            // 处理成功逻辑
            chatImages.value = [];
            fileList.value = [];
        })
        .catch((error) => {
            console.error('发送消息失败:', error);
            isReplying.value = false; // 避免因异常导致输入框一直禁用
            isLoading.value = false; // 停止 loading
        });
};

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容