前言
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的需求也可以找小编,欢迎咨询。