商务合作
首页 > 教程文章 > Java文章 > 正文

AI项目实战:智趣AI|SpringBoot3+SpringAI+Uniapp

前言

AI不仅仅是风口,也是今后的时代潮流。本人花心血开发了一套AI实战项目,可商用。支持h5,小程序,app三端。可拿来二开,也可直接上架。不用担心版权问题,但是如果是倒卖源码,本人会追究其责任

如果你是大学生,也可以抓住这个机会学习AI,源码并不难,都是java那一套。本人也会提供免费学习指导

一. 详细功能

  • 支持文本流失对话(流失)+ 图片生成。
  • 支持用户创建自己的智能体。
  • 支持动态添加应用,只需简单配置就能生成一个新的AI应用,无需任何代码编写。
  • 动态调优应用的prompt提示词,历史对话轮数,System Role等大模型属性。
  • 内置应用的提示词都是大厂优化的提示词,有相当高的参考价值。
  • 支持用户自己切换大模型,可以任意切换成qwen,gpt4,coze等大模型厂商。
  • 支持插件模式。动态对接新的大模型API(无需重启应用就可以新增一个大模型api的对接)
  • 动态修改大模型属性:base-url和token
  • 支持用户会员模式(包月)+ 非会员模式
  • 实现了邮箱验证码登录

应用部分截图

视频演示功能


https://githubs.xyz/show/206.mp4


源码下载

https://githubs.xyz/boot/?app=206


二. 技术架构

前台

  • uniapp
  • websocket

后台

  • SpringBoot3.3.0
  • 大模型底座: SpringAI + 通义千问 + Coze
  • JDK17
  • Sa Token
  • Websocket
  • MyBatisPlus+MySQL

三. 数据库表

四. 代码搭建步骤

用mysql新建一个数据库ai-waiter,然后执行语句:ai-waiter.sql。

修改application.yml配置

主要是修改邮箱服务器信息和数据库地址信息。

然后启动main函数:AiWaiterAppApplication

项目会监听两个协议,http协议+ws协议,端口都是 8999。

前端启动直接用Hbuilder导入项目,然后编译运行即可。

注意: 初始化的大模型只有3.5,需要增加4的token的可以联系小编。

五. 主要技术点讲解(开发人员)

插件开发

背景: 市面上有很多种大模型api。比如:openai,千问api,扣子api,讯飞星火等, 插件开发就是为了更好的分离对接代码,以及做到 不重启服务 就可以实现对接各大厂商api。

系统内置插件 OpenAIModelPlugin

系统源代码内置了一个SpringAI实现的插件: com. aiwaiter. openai. plugins. sys.OpenAIModelPlugin


public class OpenAIModelPlugin extends AbstractAIModelPlugin {
    @Override
    public void streamcall(Configure configure) {
        System.out.println("请求 openai-AI>> " + configure);
        OpenAiApi api = new OpenAiApi(configure.getBaseUrl(), configure.getToken());
        OpenAiChatOptions options = OpenAiChatOptions.builder().withModel(configure.getModel()).withTemperature(0.7f) //温度系数
                .build();
        ChatModel myChatModel = new OpenAiChatModel(api, options);
        ChatClient.Builder builder = ChatClient.builder(myChatModel);
        builder.defaultSystem(configure.getRole());
        ChatClient chatClient = builder.build();
        // 构建用户消息
        String curmsg = getUserText(configure);
        List<Message> messages = new ArrayList<>();
        if(!CollectionUtils.isEmpty(configure.getHistory())){
            // 添加历史消息
            for(DTO.History his : configure.getHistory()){
                if(!StringUtils.isEmpty(his.getBotContent())){
                    messages.add(new AssistantMessage(his.getBotContent()));
                    continue;
                }
                messages.add(new UserMessage(his.getUserContent()));
            }
        }
        messages.add(new UserMessage(curmsg)) ;
        Flux<ChatResponse> stream = chatClient.prompt().messages(messages).stream().chatResponse();
        stream.toStream().forEach(response -> {
            response.getResults().forEach(item -> {
                AssistantMessage output = item.getOutput();
                boolean finish = false;
                if (output.getMetadata().containsKey("finishReason") && output.getMetadata().get("finishReason").equals("STOP")) {
                    finish = true;
                    configure.getListerner().onCall("", finish);
                    return;
                }
                configure.getListerner().onCall(output.getContent(), finish);
            });
        });
    }

    @Override
    public void textToImge(Configure configure) {
        System.out.println("请求AI>> " + configure);
        OpenAiImageApi api = new OpenAiImageApi(configure.getBaseUrl(), configure.getToken(), RestClient.builder());
        OpenAiImageModel model = new OpenAiImageModel(api);
        ImageOptions options = ImageOptionsBuilder.builder()
                .withModel(OpenAiImageApi.ImageModel.DALL_E_3.getValue())
                .build() ;
        ImagePrompt imagePrompt = new ImagePrompt(getUserText(configure) , options);
        final String url = model.call(imagePrompt).getResult().getOutput().getUrl();
        configure.getListerner().onCall(url, true);
    }

}

自定定义插件

插件类必须继承 AbstractAIModelPlugin , 并且必须要实现下面两个方法:


    /***
     * 流式文字返回
     * @param configure
     */
    void streamcall(Configure configure);

    /***
     * 文字生成图片
     * @param configure
     * @return
     */
    void textToImge(Configure configure);

如果要设置流失回调监听器,只需要设置Configure类里面的StreamCallBackListerner即可监听流失回调:


Configure.StreamCallBackListerner

我们用编辑器写好插件代码,比如我写了一个对接千问api的插件


package com.aiwaiter.openai.plugins.sys;

import com.aiwaiter.openai.plugins.AbstractAIModelPlugin;
import com.aiwaiter.openai.Configure;
import com.aiwaiter.websocket.dto.DTO;
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.Constants;
import io.reactivex.Flowable;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @className: com.aiwaiter.openai.plugins.sys.QwenAIModelPlugin
 * @author: WX:hadluo QQ:657455400
 * @date: 2024/7/24 15:43
 * @Version: 1.0
 * @description:
 */
public class QwenAIModelPlugin extends AbstractAIModelPlugin {
    @Override
    public void streamcall(Configure configure) {
        System.out.println("请求 qwen-AI>> " + configure);
        Constants.apiKey = configure.getToken();
        // 系统角色
        Message sysMsg = Message.builder().role(Role.SYSTEM.getValue()).content(configure.getRole()).build();
        // 用户消息
        Message userMsg = Message.builder().role(Role.USER.getValue()).content(getUserText(configure)).build();
        List<Message> messages = new ArrayList<>();
        // 构建历史消息
        if(configure.getHistory() != null && !configure.getHistory().isEmpty()){
            // 添加历史消息
            for(DTO.History his : configure.getHistory()){
                if(!StringUtils.isEmpty(his.getBotContent())){
                    messages.add( Message.builder().role(Role.ASSISTANT.getValue()).content(his.getBotContent()).build());
                    continue;
                }
                messages.add( Message.builder().role(Role.USER.getValue()).content(his.getUserContent()).build());
            }
        }
        messages.add( Message.builder().role(Role.USER.getValue()).content(getUserText(configure)).build());
        GenerationParam param = GenerationParam.builder().model("qwen-turbo").messages(Arrays.asList(sysMsg, userMsg)).resultFormat(GenerationParam.ResultFormat.MESSAGE).topP(0.8).incrementalOutput(true).build();
        Generation gen = new Generation();
        try {
            Flowable<GenerationResult> result = gen.streamCall(param);
            result.blockingForEach(message -> {
                String finishReason = message.getOutput().getChoices().get(0).getFinishReason() ;
                boolean finish =false ;
                if(!StringUtils.isEmpty(finishReason) && "stop".equals(finishReason)){
                    finish = true ;
                }
                String centent = message.getOutput().getChoices().get(0).getMessage().getContent();
                configure.getListerner().onCall(centent , finish);
            });
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void textToImge(Configure configure) {
        //千问无法生成图片
    }
}

写好之后,我们测试没有问题,就将代码插入数据库 t_ai_robot 表中plugin


CREATE TABLE `t_ai_robot` (
  `robot_id` int(10) unsigned NOT NULL auto_increment COMMENT '自增主键',
  `token` varchar(400) default NULL COMMENT 'token',
  `bot_id` varchar(100) NOT NULL COMMENT 'bot_id',
  `model` varchar(255) default '' COMMENT '大模型底座',
  `plugin` mediumtext COMMENT '插件实现代码',
  `plugin_class` varchar(255) default NULL COMMENT '插件的全类名',
  `plugin_version` int(10) default NULL COMMENT '版本号,自定义插件必须从1开始递增,修改了plugin,必须新增版本号',
  `base_url` varchar(255) default '' COMMENT '请求地址',
  `acct` varchar(255) default NULL,
  `pwd` varchar(255) default NULL,
  PRIMARY KEY  USING BTREE (`robot_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COMMENT='机器人表';

这样就实现了新增千问对接。如果您后续修改了插件代码,只需要把表中的 plugin_version 版本号字段升级即可。

动态修改token和base-url

也是直接对 t_ai_robot 表进行操作,token字段就是api的key,base_url就是请求的地址。

动态添加,修改应用

是对t_ai_app应用表操作。里面关键字段是大模型的属性:

  • role: 传给大模型的 System 角色。
  • prompt: 传给大模型的UserMessage。此变量在传给大模型之前会将 ${usertext} 替换为用户输入的内容。
  • history_count : 传给大模型的历史对话轮数,为了节省token词,有最大值,在application.yml中配置。

动态调优提示词

只需要修改t_ai_app表的role和prompt字段即可。

提示词

比如内置的诗歌提示词


Role: 诗人
Profile
Author: YZFly
Version: 0.1
Language: 中文
Description: 诗人是创作诗歌的艺术家,擅长通过诗歌来表达情感、描绘景象、讲述故事,具有丰富的想象力和对文字的独特驾驭能力。诗人创作的作品可以是纪事性的,描述人物或故事,如荷马的史诗;也可以是比喻性的,隐含多种解读的可能,如但丁的《神曲》、歌德的《浮士德》。
擅长写现代诗:
现代诗形式自由,意涵丰富,意象经营重于修辞运用,是心灵的映现
更加强调自由开放和直率陈述与进行“可感与不可感之间”的沟通。
擅长写七言律诗
七言体是古代诗歌体裁
全篇每句七字或以七字句为主的诗体
它起于汉族民间歌谣
擅长写五言诗
全篇由五字句构成的诗
能够更灵活细致地抒情和叙事
在音节上,奇偶相配,富于音乐美
Rules
内容健康,积极向上
七言律诗和五言诗要押韵
Workflow
让用户以 "形式:[], 主题:[]" 的方式指定诗歌形式,主题。
针对用户给定的主题,创作诗歌,包括题目和诗句。
Initialization
作为角色 , 严格遵守 , 使用默认 与用户对话,友好的欢迎用户。然后介绍自己,并告诉用户 。

用户自创智能体

用户自己创建的智能体也是在 t_ai_app表里面,但是类目id必须是21 ,此值不能更改。与用户的关联表为:t_user_robot, 比如我创建的智能体:

整个代码截图

前端代码

前端代码就是通过uniapp实现的,没什么好讲的。截图吧


结尾语

如果您对项目感兴趣,请联系小编,如果您有AI的需求也可以找小编,欢迎咨询。