14 Commits

Author SHA1 Message Date
ageerle
07bdc5e585 Merge pull request #292 from yangzhen233/feature/rerank-model
Feature/rerank model
2026-04-20 21:06:27 +08:00
yangzhen
e1b8a5f011 新增千问3重排序模型,并附带新增sql文件 2026-04-20 16:07:02 +08:00
杨振
80ca76ea37 添加重排序功能 2026-04-20 01:02:09 +08:00
wangle
2c6ff66830 fix: 修正application.yml演示模式message缩进
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 13:42:39 +08:00
wangle
4f79a66559 feat: 添加小米MiMo、DeepSeek、自定义厂商等provider支持
- 新增小米MiMo服务实现类(MiMoServiceImpl)
- ChatModeType添加XIAOMI枚举
- 更新SQL初始化脚本,新增多家厂商(provider)和模型数据
- 添加2026-04-19数据库更新脚本
- application.yml演示模式排除路径增加attach/fragment/info接口
- 删除独立的minimax_provider.sql(数据已合并到主SQL)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 13:42:05 +08:00
wangle
22883b4334 Merge branch pr-280: 添加MiniMax作为LLM提供商
解决冲突:
- README: 保留Qdrant向量库信息 + 合并MiniMax模型接入
- pom.xml: 保留spring-boot-starter-test + 添加MiniMax测试依赖
- ChatModeType: 保留CUSTOM_API + 新增MINIMAX枚举
- MinimaxServiceImpl: 保留MyChatModelListener监听器

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:47:04 +08:00
wangle
081da6d18d feat: 添加MiniMax作为LLM提供商,合并PR#280并补充监听
合并PR#280的MiniMax provider实现,解决与main分支的冲突,
并在MinimaxServiceImpl中补充MyChatModelListener监听,
与其他provider保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 18:31:53 +08:00
ageerle
74eb5b2530 Merge pull request #290 from yangzhen233/feature/zhipu-embedding
添加智谱向量模型实现
2026-04-17 18:24:24 +08:00
ageerle
b0328fe0ef Merge pull request #291 from xiaonieli7/main
fate:增加自定义模型,调整前端模型选择下拉框
2026-04-17 18:16:59 +08:00
Administrator
2ee0aae57e fate:增加自定义模型,调整前端模型选择下拉框 2026-04-17 08:31:40 +08:00
杨振
d9c3de660a 添加智谱向量模型实现 2026-04-16 21:18:11 +08:00
ageerle
c4f7c1f5d0 Merge pull request #283 from MrWws/fix/docker-compose-all.yaml
fix: 修复运行docker-compose-all.yaml报错的问题
2026-04-14 14:36:14 +08:00
MrWws
b9097b4989 fix: 修复运行docker-compose-all.yaml报错的问题 2026-03-31 22:59:26 +08:00
octopus
5d14eb20af feat: add MiniMax as first-class LLM provider
Add MiniMax AI as the 7th LLM provider, supporting chat (M2.7, M2.5,
M2.5-highspeed) and embedding (embo-01) models via OpenAI-compatible API.

Changes:
- Add MINIMAX enum to ChatModeType
- Add MinimaxServiceImpl chat provider (OpenAI-compat streaming)
- Add MinimaxEmbeddingProvider for vector embeddings
- Add SQL migration for provider and model registration
- Add 14 unit tests + 3 integration tests
- Update README/README_EN with MiniMax in provider list
2026-03-21 16:14:19 +08:00
44 changed files with 2007 additions and 229 deletions

View File

@@ -31,7 +31,7 @@
| 模块 | 现有能力 | 模块 | 现有能力
|:----------:|--- |:----------:|---
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱/MiniMax)、多模态理解、Coze/DIFY/FastGPT平台集成
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析 | **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点

View File

@@ -34,7 +34,7 @@
| Module | Current Capabilities | | Module | Current Capabilities |
|:---:|---| |:---:|---|
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | | **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu/MiniMax), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing | | **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | | **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes | | **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |

View File

@@ -65,7 +65,7 @@ services:
- "28080:8080" - "28080:8080"
environment: environment:
QUERY_DEFAULTS_LIMIT: 25 QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
PERSISTENCE_DATA_PATH: /var/lib/weaviate PERSISTENCE_DATA_PATH: /var/lib/weaviate
DEFAULT_VECTORIZER_MODULE: none DEFAULT_VECTORIZER_MODULE: none
ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai

View File

@@ -72,8 +72,9 @@ CREATE TABLE `chat_model` (
-- ---------------------------- -- ----------------------------
-- Records of chat_model -- Records of chat_model
-- ---------------------------- -- ----------------------------
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'deepseek/deepseek-v3.2', 'ppio', 'deepseek', NULL, 'Y', 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse AttentionDSA稀疏注意力机制在显著降低计算开销的同时优化长上下文性能通过可扩展强化学习框架整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro同时模型依托大型智能体任务合成管线具备更强的工具调用与多步骤决策能力并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0); INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse AttentionDSA稀疏注意力机制在显著降低计算开销的同时优化长上下文性能通过可扩展强化学习框架整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro同时模型依托大型智能体任务合成管线具备更强的工具调用与多步骤决策能力并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中BGE-M3展现出显著优势其性能在MIRACL、MKQA等国际基准测试中位居前列。此外针对长文档检索场景该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0); INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中BGE-M3展现出显著优势其性能在MIRACL、MKQA等国际基准测试中位居前列。此外针对长文档检索场景该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
-- ---------------------------- -- ----------------------------
-- Table structure for chat_provider -- Table structure for chat_provider
@@ -95,22 +96,26 @@ CREATE TABLE `chat_provider` (
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
`version` int NULL DEFAULT NULL COMMENT '版本', `version` int NULL DEFAULT NULL COMMENT '版本',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志0代表存在 1代表删除', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志',
`update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP', `update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC) USING BTREE, UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC, `del_flag` ASC) USING BTREE,
INDEX `idx_status`(`status` ASC) USING BTREE INDEX `idx_status`(`status` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2008460994477690882 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB AUTO_INCREMENT = 2045727230803255298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
-- ---------------------------- -- ----------------------------
-- Records of chat_provider -- Records of chat_provider
-- ---------------------------- -- ----------------------------
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (2, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (3, '智谱AI', 'zhipu', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/a43e98fb7b3b4861b8caa6184e6fa40a.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 3, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-06 00:49:14', '智谱AI厂商', NULL, '1', NULL, 0); INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (5, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 5, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (2000585060904435714, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
-- ---------------------------- -- ----------------------------
-- Table structure for chat_session -- Table structure for chat_session

View File

@@ -0,0 +1,92 @@
/*
Navicat Premium Dump SQL
Source Server : localhost-mysql
Source Server Type : MySQL
Source Server Version : 80045 (8.0.45)
Source Host : localhost:3306
Source Schema : ruoyi-ai
Target Server Type : MySQL
Target Server Version : 80045 (8.0.45)
File Encoding : 65001
Date: 19/04/2026 13:36:41
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for chat_model
-- ----------------------------
DROP TABLE IF EXISTS `chat_model`;
CREATE TABLE `chat_model` (
`id` bigint NOT NULL COMMENT '主键',
`category` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型分类',
`model_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型名称',
`provider_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型供应商',
`model_describe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型描述',
`model_dimension` int NULL DEFAULT NULL COMMENT '模型维度',
`model_show` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '是否显示',
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求地址',
`api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密钥',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '模型管理' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of chat_model
-- ----------------------------
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse AttentionDSA稀疏注意力机制在显著降低计算开销的同时优化长上下文性能通过可扩展强化学习框架整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro同时模型依托大型智能体任务合成管线具备更强的工具调用与多步骤决策能力并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中BGE-M3展现出显著优势其性能在MIRACL、MKQA等国际基准测试中位居前列。此外针对长文档检索场景该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
-- ----------------------------
-- Table structure for chat_provider
-- ----------------------------
DROP TABLE IF EXISTS `chat_provider`;
CREATE TABLE `chat_provider` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`provider_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商名称',
`provider_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商编码',
`provider_icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商图标',
`provider_desc` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商描述',
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'API地址',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '状态0正常 1停用',
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '创建者',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
`version` int NULL DEFAULT NULL COMMENT '版本',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志',
`update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC, `del_flag` ASC) USING BTREE,
INDEX `idx_status`(`status` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2045727230803255298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of chat_provider
-- ----------------------------
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,46 @@
/*
Navicat Premium Dump SQL
Source Server : localhost-mysql
Source Server Type : MySQL
Source Server Version : 80045 (8.0.45)
Source Host : localhost:3306
Source Schema : ruoyi-ai
Target Server Type : MySQL
Target Server Version : 80045 (8.0.45)
File Encoding : 65001
Date: 20/04/2026 15:30:00
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 新增重排序模型chat_model
-- ----------------------------
INSERT INTO `chat_model`
(id, category, model_name, provider_code, model_describe, model_dimension, model_show, api_host, api_key, create_dept, create_by, create_time, update_by, update_time, remark, tenant_id)
VALUES(2045071617578237953, 'rerank', 'rerank', 'zhipu', '智谱重排序', NULL, 'Y', 'https://open.bigmodel.cn', 'e9xx', 103, 1, '2026-04-17 17:27:24', 1, '2026-04-20 15:21:48', '智谱重排序', 0);
INSERT INTO `chat_model`
(id, category, model_name, provider_code, model_describe, model_dimension, model_show, api_host, api_key, create_dept, create_by, create_time, update_by, update_time, remark, tenant_id)
VALUES(2046119803482902530, 'rerank', 'qwen3-rerank', 'qianwen', '千问3重排序', NULL, NULL, 'https://dashscope.aliyuncs.com', 'sk-xx', 103, 1, '2026-04-20 14:52:31', 1, '2026-04-20 15:03:13', '千问3文本重排序', 0);
-- ----------------------------
-- 新增:字典类型 - 重排序模型分类
-- ----------------------------
INSERT INTO `sys_dict_data`
(dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, create_dept, create_by, create_time, update_by, update_time, remark)
VALUES(2045070879435259905, '000000', 4, '重排序', 'rerank', 'chat_model_category', NULL, '#000000', 'N', 103, 1, '2026-04-17 17:24:28', 1, '2026-04-19 01:02:20', '重排序模型');
-- ----------------------------
-- 修改表knowledge_info 增加重排序相关字段
-- ----------------------------
ALTER TABLE `knowledge_info` ADD COLUMN `enable_rerank` tinyint DEFAULT 0 NULL COMMENT '是否启用重排序0否 1是';
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_score_threshold` double NULL COMMENT '重排序相关性分数阈值';
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_top_n` int NULL COMMENT '重排序后返回的文档数量';
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_model` varchar(100) NULL COMMENT '重排序模型名称';
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -265,7 +265,7 @@ demo:
# 是否开启演示模式(开启后所有写操作将被拦截) # 是否开启演示模式(开启后所有写操作将被拦截)
enabled: false enabled: false
# 提示消息 # 提示消息
message: "演示模式,不允许进行写操作" message: "演示模式,不允许操作"
# 排除的路径(这些路径不受演示模式限制) # 排除的路径(这些路径不受演示模式限制)
excludes: excludes:
- /login - /login
@@ -276,7 +276,9 @@ demo:
- /chat/send - /chat/send
- /system/session/** - /system/session/**
- /system/message/** - /system/message/**
- /system/attach/**
- /system/fragment/**
- /system/info/**
--- # warm-flow工作流配置 --- # warm-flow工作流配置
warm-flow: warm-flow:
# 是否开启工作流默认true # 是否开启工作流默认true

View File

@@ -174,6 +174,23 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -9,6 +9,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.enums.ModelType; import org.ruoyi.enums.ModelType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@@ -23,6 +24,8 @@ import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil; import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.mybatis.core.page.TableDataInfo; import org.ruoyi.common.mybatis.core.page.TableDataInfo;
import java.util.LinkedHashMap;
/** /**
* 模型管理 * 模型管理
* *
@@ -55,6 +58,21 @@ public class ChatModelController extends BaseController {
return R.ok(chatModelService.queryList(bo)); return R.ok(chatModelService.queryList(bo));
} }
/**
* 获取模型供应商枚举
*/
@GetMapping("/providerOptions")
public R<List<LinkedHashMap<String, String>>> providerOptions() {
List<LinkedHashMap<String, String>> options = new java.util.ArrayList<>();
for (ChatModeType type : ChatModeType.values()) {
LinkedHashMap<String, String> item = new LinkedHashMap<>();
item.put("label", type.getDescription());
item.put("value", type.getCode());
options.add(item);
}
return R.ok(options);
}
/** /**
* 导出模型管理列表 * 导出模型管理列表
*/ */

View File

@@ -1,105 +0,0 @@
package org.ruoyi.controller.knowledge;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphInstanceBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphInstanceVo;
import org.ruoyi.service.knowledge.IKnowledgeGraphInstanceService;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
/**
* 知识图谱实例
*
* @author ageerle
* @date 2025-12-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/graphInstance")
public class KnowledgeGraphInstanceController extends BaseController {
private final IKnowledgeGraphInstanceService knowledgeGraphInstanceService;
/**
* 查询知识图谱实例列表
*/
@SaCheckPermission("system:graphInstance:list")
@GetMapping("/list")
public TableDataInfo<KnowledgeGraphInstanceVo> list(KnowledgeGraphInstanceBo bo, PageQuery pageQuery) {
return knowledgeGraphInstanceService.queryPageList(bo, pageQuery);
}
/**
* 导出知识图谱实例列表
*/
@SaCheckPermission("system:graphInstance:export")
@Log(title = "知识图谱实例", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(KnowledgeGraphInstanceBo bo, HttpServletResponse response) {
List<KnowledgeGraphInstanceVo> list = knowledgeGraphInstanceService.queryList(bo);
ExcelUtil.exportExcel(list, "知识图谱实例", KnowledgeGraphInstanceVo.class, response);
}
/**
* 获取知识图谱实例详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:graphInstance:query")
@GetMapping("/{id}")
public R<KnowledgeGraphInstanceVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(knowledgeGraphInstanceService.queryById(id));
}
/**
* 新增知识图谱实例
*/
@SaCheckPermission("system:graphInstance:add")
@Log(title = "知识图谱实例", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
return toAjax(knowledgeGraphInstanceService.insertByBo(bo));
}
/**
* 修改知识图谱实例
*/
@SaCheckPermission("system:graphInstance:edit")
@Log(title = "知识图谱实例", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
return toAjax(knowledgeGraphInstanceService.updateByBo(bo));
}
/**
* 删除知识图谱实例
*
* @param ids 主键串
*/
@SaCheckPermission("system:graphInstance:remove")
@Log(title = "知识图谱实例", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(knowledgeGraphInstanceService.deleteWithValidByIds(List.of(ids), true));
}
}

View File

@@ -1,105 +0,0 @@
package org.ruoyi.controller.knowledge;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphSegmentBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphSegmentVo;
import org.ruoyi.service.knowledge.IKnowledgeGraphSegmentService;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
/**
* 知识图谱片段
*
* @author ageerle
* @date 2025-12-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/graphSegment")
public class KnowledgeGraphSegmentController extends BaseController {
private final IKnowledgeGraphSegmentService knowledgeGraphSegmentService;
/**
* 查询知识图谱片段列表
*/
@SaCheckPermission("system:graphSegment:list")
@GetMapping("/list")
public TableDataInfo<KnowledgeGraphSegmentVo> list(KnowledgeGraphSegmentBo bo, PageQuery pageQuery) {
return knowledgeGraphSegmentService.queryPageList(bo, pageQuery);
}
/**
* 导出知识图谱片段列表
*/
@SaCheckPermission("system:graphSegment:export")
@Log(title = "知识图谱片段", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(KnowledgeGraphSegmentBo bo, HttpServletResponse response) {
List<KnowledgeGraphSegmentVo> list = knowledgeGraphSegmentService.queryList(bo);
ExcelUtil.exportExcel(list, "知识图谱片段", KnowledgeGraphSegmentVo.class, response);
}
/**
* 获取知识图谱片段详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:graphSegment:query")
@GetMapping("/{id}")
public R<KnowledgeGraphSegmentVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(knowledgeGraphSegmentService.queryById(id));
}
/**
* 新增知识图谱片段
*/
@SaCheckPermission("system:graphSegment:add")
@Log(title = "知识图谱片段", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
return toAjax(knowledgeGraphSegmentService.insertByBo(bo));
}
/**
* 修改知识图谱片段
*/
@SaCheckPermission("system:graphSegment:edit")
@Log(title = "知识图谱片段", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
return toAjax(knowledgeGraphSegmentService.updateByBo(bo));
}
/**
* 删除知识图谱片段
*
* @param ids 主键串
*/
@SaCheckPermission("system:graphSegment:remove")
@Log(title = "知识图谱片段", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(knowledgeGraphSegmentService.deleteWithValidByIds(List.of(ids), true));
}
}

View File

@@ -77,10 +77,33 @@ public class KnowledgeInfoBo extends BaseEntity {
*/ */
private String embeddingModel; private String embeddingModel;
/**
* 是否启用重排序0 否 1是
*/
private Integer enableRerank;
/**
* 重排序模型名称
*/
private String rerankModel;
/**
* 重排序后返回的文档数量
*/
private Integer rerankTopN;
/**
* 重排序相关性分数阈值
*/
private Double rerankScoreThreshold;
/** /**
* 备注 * 备注
*/ */
private String remark; private String remark;
} }

View File

@@ -0,0 +1,44 @@
package org.ruoyi.domain.bo.rerank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 重排序请求参数
*
* @author yang
* @date 2026-04-19
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RerankRequest {
/**
* 查询文本
*/
private String query;
/**
* 候选文档列表
*/
private List<String> documents;
/**
* 返回的文档数量topN
* 如果不指定,默认返回所有文档
*/
private Integer topN;
/**
* 是否返回原始文档内容
* 默认为 true
*/
@Builder.Default
private Boolean returnDocuments = true;
}

View File

@@ -0,0 +1,72 @@
package org.ruoyi.domain.bo.rerank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 重排序结果
*
* @author yang
* @date 2026-04-19
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RerankResult {
/**
* 重排序后的文档结果列表
*/
private List<RerankDocument> documents;
/**
* 原始请求中的文档总数
*/
private Integer totalDocuments;
/**
* 重排序耗时(毫秒)
*/
private Long durationMs;
/**
* 单个重排序文档结果
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RerankDocument {
/**
* 文档在原始列表中的索引位置
*/
private Integer index;
/**
* 相关性分数(通常 0-1 之间,越高越相关)
*/
private Double relevanceScore;
/**
* 文档内容
*/
private String document;
}
/**
* 创建空结果
*/
public static RerankResult empty() {
return RerankResult.builder()
.documents(List.of())
.totalDocuments(0)
.durationMs(0L)
.build();
}
}

View File

@@ -51,4 +51,30 @@ public class QueryVectorBo {
*/ */
private String baseUrl; private String baseUrl;
// ========== 重排序相关参数 ==========
/**
* 是否启用重排序
* 默认为 false
*/
private Boolean enableRerank = false;
/**
* 重排序模型名称
*/
private String rerankModelName;
/**
* 重排序后返回的文档数量topN
* 如果不指定,默认与 maxResults 相同
*/
private Integer rerankTopN;
/**
* 重排序相关性分数阈值
* 低于此阈值的文档将被过滤
*/
private Double rerankScoreThreshold;
} }

View File

@@ -0,0 +1,55 @@
package org.ruoyi.domain.dto.request;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
/**
* 阿里百炼重排序请求DTOOpenAI兼容格式
*
* @author yang
* @date 2026-04-20
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record AliBaiLianRerankRequest(
String model,
List<String> documents,
String query,
@JsonProperty("top_n")
Integer topN,
String instruct,
@JsonProperty("return_documents")
Boolean returnDocuments
) {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* 创建文本重排序请求
*/
public static AliBaiLianRerankRequest create(String modelName, String query,
List<String> documents, Integer topN,
Boolean returnDocuments) {
return new AliBaiLianRerankRequest(
modelName,
documents,
query,
topN != null ? topN : documents.size(),
null,
returnDocuments != null ? returnDocuments : true
);
}
/**
* 转换为JSON字符串
*/
public String toJson() {
try {
return OBJECT_MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("序列化阿里百炼重排序请求失败", e);
}
}
}

View File

@@ -0,0 +1,48 @@
package org.ruoyi.domain.dto.request;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
/**
* 智谱AI重排序请求DTO
*
* @author yang
* @date 2026-04-19
*/
public record ZhipuRerankRequest(
String model,
String query,
List<String> documents,
Integer top_n,
Boolean return_documents
) {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* 创建智谱重排序请求
*/
public static ZhipuRerankRequest create(String modelName, String query,
List<String> documents, Integer topN,
Boolean returnDocuments) {
return new ZhipuRerankRequest(
modelName,
query,
documents,
topN != null ? topN : documents.size(),
returnDocuments != null ? returnDocuments : true
);
}
/**
* 转换为JSON字符串
*/
public String toJson() {
try {
return OBJECT_MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("序列化智谱重排序请求失败", e);
}
}
}

View File

@@ -0,0 +1,81 @@
package org.ruoyi.domain.dto.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.ruoyi.domain.bo.rerank.RerankResult;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 阿里百炼重排序响应DTOOpenAI兼容格式
*
* @author yang
* @date 2026-04-20
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record AliBaiLianRerankResponse(
String id,
String object,
List<ResultItem> results,
UsageInfo usage
) {
/**
* 单个重排序结果项
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record ResultItem(
Integer index,
@JsonProperty("relevance_score")
Double relevanceScore,
Object document
) {
/**
* 获取文档文本内容
*/
public String getDocumentText() {
if (document == null) return null;
if (document instanceof String) return (String) document;
if (document instanceof Map) {
Object text = ((Map<?, ?>) document).get("text");
return text != null ? text.toString() : null;
}
return document.toString();
}
}
/**
* Token使用信息
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record UsageInfo(
@JsonProperty("total_tokens")
Integer totalTokens,
@JsonProperty("prompt_tokens")
Integer promptTokens
) {}
/**
* 转换为通用RerankResult
*/
public RerankResult toRerankResult(int totalDocs, long durationMs) {
if (results == null || results.isEmpty()) {
return RerankResult.empty();
}
List<RerankResult.RerankDocument> documents = results.stream()
.map(item -> RerankResult.RerankDocument.builder()
.index(item.index())
.relevanceScore(item.relevanceScore())
.document(item.getDocumentText())
.build())
.collect(Collectors.toList());
return RerankResult.builder()
.documents(documents)
.totalDocuments(totalDocs)
.durationMs(durationMs)
.build();
}
}

View File

@@ -0,0 +1,68 @@
package org.ruoyi.domain.dto.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.ruoyi.domain.bo.rerank.RerankResult;
import java.util.List;
import java.util.stream.Collectors;
/**
* 智谱AI重排序响应DTO
*
* @author yang
* @date 2026-04-19
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record ZhipuRerankResponse(
String model,
String object,
List<ResultItem> results,
UsageInfo usage
) {
/**
* 单个重排序结果项
*/
public record ResultItem(
Integer index,
@JsonProperty("relevance_score")
Double relevanceScore,
String document
) {}
/**
* Token使用信息
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record UsageInfo(
@JsonProperty("total_tokens")
Integer totalTokens,
@JsonProperty("input_tokens")
Integer inputTokens,
@JsonProperty("output_tokens")
Integer outputTokens
) {}
/**
* 转换为通用RerankResult
*/
public RerankResult toRerankResult(int totalDocs, long durationMs) {
if (results == null || results.isEmpty()) {
return RerankResult.empty();
}
List<RerankResult.RerankDocument> documents = results.stream()
.map(item -> RerankResult.RerankDocument.builder()
.index(item.index())
.relevanceScore(item.relevanceScore())
.document(item.document())
.build())
.collect(Collectors.toList());
return RerankResult.builder()
.documents(documents)
.totalDocuments(totalDocs)
.durationMs(durationMs)
.build();
}
}

View File

@@ -78,6 +78,26 @@ public class KnowledgeInfo extends BaseEntity {
*/ */
private String embeddingModel; private String embeddingModel;
/**
* 是否启用重排序0 否 1是
*/
private Integer enableRerank;
/**
* 重排序模型名称
*/
private String rerankModel;
/**
* 重排序后返回的文档数量
*/
private Integer rerankTopN;
/**
* 重排序相关性分数阈值
*/
private Double rerankScoreThreshold;
/** /**
* 备注 * 备注
*/ */

View File

@@ -94,6 +94,30 @@ public class KnowledgeInfoVo implements Serializable {
@ExcelProperty(value = "向量模型") @ExcelProperty(value = "向量模型")
private String embeddingModel; private String embeddingModel;
/**
* 是否启用重排序0 否 1是
*/
@ExcelProperty(value = "是否启用重排序")
private Integer enableRerank;
/**
* 重排序模型名称
*/
@ExcelProperty(value = "重排序模型")
private String rerankModel;
/**
* 重排序后返回的文档数量
*/
@ExcelProperty(value = "重排序返回数量")
private Integer rerankTopN;
/**
* 重排序相关性分数阈值
*/
@ExcelProperty(value = "重排序分数阈值")
private Double rerankScoreThreshold;
/** /**
* 备注 * 备注
*/ */

View File

@@ -15,7 +15,10 @@ public enum ChatModeType {
DEEP_SEEK("deepseek", "深度求索"), DEEP_SEEK("deepseek", "深度求索"),
QIAN_WEN("qianwen", "通义千问"), QIAN_WEN("qianwen", "通义千问"),
OPEN_AI("openai", "openai"), OPEN_AI("openai", "openai"),
PPIO("ppio", "ppio"); PPIO("ppio", "ppio"),
CUSTOM_API("custom_api", "自定义API"),
MINIMAX("minimax", "MiniMax"),
XIAOMI("xiaomi", "小米MiMo");
private final String code; private final String code;
private final String description; private final String description;

View File

@@ -0,0 +1,106 @@
package org.ruoyi.factory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.ruoyi.service.rerank.RerankModelService;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 重排序模型工厂服务类
* 参考设计模式EmbeddingModelFactory
* 负责创建和管理重排序模型实例
*
* @author yang
* @date 2026-04-19
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RerankModelFactory {
private final ApplicationContext applicationContext;
private final IChatModelService chatModelService;
/**
* 模型缓存使用ConcurrentHashMap保证线程安全
*/
private final Map<String, RerankModelService> modelCache = new ConcurrentHashMap<>();
/**
* 创建重排序模型实例
* 如果模型已存在于缓存中,则直接返回;否则创建新的实例
*
* @param rerankModelName 重排序模型名称
*/
public RerankModelService createModel(String rerankModelName) {
return modelCache.computeIfAbsent(rerankModelName, name -> {
ChatModelVo modelConfig = chatModelService.selectModelByName(rerankModelName);
if (modelConfig == null) {
throw new IllegalArgumentException("未找到重排序模型配置name=" + name);
}
return createModelInstance(modelConfig.getProviderCode(), modelConfig);
});
}
/**
* 刷新模型缓存
* 根据给定的模型ID从缓存中移除对应的模型
*
* @param modelId 模型的唯一标识ID
*/
public void refreshModel(Long modelId) {
modelCache.remove(modelId);
}
/**
* 获取所有支持模型工厂的列表
*
* @return 支持的模型工厂名称列表
*/
public List<String> getSupportedFactories() {
return new ArrayList<>(applicationContext.getBeansOfType(RerankModelService.class)
.keySet());
}
/**
* 创建具体的模型实例
* 根据提供的工厂名称和配置信息创建并配置模型实例
*
* @param factory 工厂名称用于标识模型类型providerCode
* @param config 模型配置信息
* @return RerankModelService 配置好的模型实例
* @throws IllegalArgumentException 当无法获取指定的模型实例时抛出
*/
private RerankModelService createModelInstance(String factory, ChatModelVo config) {
try {
// 优先尝试使用 providerCode + "Rerank" 作为 Bean 名称
// 例如zhipu -> zhipuRerankjina -> jinaRerank
String rerankBeanName = factory + "Rerank";
RerankModelService model = applicationContext.getBean(rerankBeanName, RerankModelService.class);
model.configure(config);
log.info("成功创建重排序模型: factory={}, modelName={}", rerankBeanName, config.getModelName());
return model;
} catch (NoSuchBeanDefinitionException e) {
// 如果找不到,尝试使用原始的 providerCode
try {
RerankModelService model = applicationContext.getBean(factory, RerankModelService.class);
model.configure(config);
log.info("成功创建重排序模型: factory={}, modelName={}", factory, config.getModelName());
return model;
} catch (NoSuchBeanDefinitionException ex) {
throw new IllegalArgumentException("获取不到重排序模型: " + factory + "" + factory + "Rerank", ex);
}
}
}
}

View File

@@ -1,9 +1,13 @@
package org.ruoyi.service.chat; package org.ruoyi.service.chat;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import java.time.Duration;
/** /**
* 聊天消息Service接口 * 聊天消息Service接口
* *
@@ -21,6 +25,23 @@ public interface AbstractChatService {
*/ */
StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest); StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest);
/**
* 创建同步聊天模型(供 Agent/SupervisorAgent 使用)
* 默认实现使用 OpenAI 兼容协议,适用于 OpenAI、DeepSeek、PPIO 等兼容接口的 provider。
* ZhiPu、QianWen、Ollama 等需覆盖此方法使用各自 SDK。
*
* @param chatModelVo 模型配置
* @return 同步聊天模型实例
*/
default ChatModel buildChatModel(ChatModelVo chatModelVo) {
return OpenAiChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.timeout(Duration.ofSeconds(120))
.build();
}
/** /**
* 获取服务提供商名称 * 获取服务提供商名称
*/ */

View File

@@ -54,6 +54,7 @@ import org.ruoyi.service.chat.AbstractChatService;
import org.ruoyi.service.chat.IChatMessageService; import org.ruoyi.service.chat.IChatMessageService;
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore; import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
import org.ruoyi.service.knowledge.IKnowledgeInfoService; import org.ruoyi.service.knowledge.IKnowledgeInfoService;
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
import org.ruoyi.service.vector.VectorStoreService; import org.ruoyi.service.vector.VectorStoreService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -89,6 +90,8 @@ public class ChatServiceFacade implements IChatService {
private final VectorStoreService vectorStoreService; private final VectorStoreService vectorStoreService;
private final KnowledgeRetrievalService knowledgeRetrievalService;
private final SseEmitterManager sseEmitterManager; private final SseEmitterManager sseEmitterManager;
private final IChatMessageService chatMessageService; private final IChatMessageService chatMessageService;
@@ -452,8 +455,8 @@ public class ChatServiceFacade implements IChatService {
// 构建向量查询参数 // 构建向量查询参数
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel); QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
// 获取向量查询结果(知识库内容作为系统上下文,放在历史消息之后 // 使用知识库检索服务(支持重排序
List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo); List<String> nearestList = knowledgeRetrievalService.retrieveTexts(queryVectorBo);
for (String prompt : nearestList) { for (String prompt : nearestList) {
// 知识库内容作为系统上下文添加 // 知识库内容作为系统上下文添加
messages.add(new AiMessage(prompt)); messages.add(new AiMessage(prompt));
@@ -480,6 +483,13 @@ public class ChatServiceFacade implements IChatService {
queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModel()); queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModel());
queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel()); queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel());
queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit()); queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit());
// 设置重排序参数
queryVectorBo.setEnableRerank(knowledgeInfoVo.getEnableRerank() != null && knowledgeInfoVo.getEnableRerank() == 1);
queryVectorBo.setRerankModelName(knowledgeInfoVo.getRerankModel());
queryVectorBo.setRerankTopN(knowledgeInfoVo.getRerankTopN());
queryVectorBo.setRerankScoreThreshold(knowledgeInfoVo.getRerankScoreThreshold());
return queryVectorBo; return queryVectorBo;
} }

View File

@@ -0,0 +1,72 @@
package org.ruoyi.service.chat.impl.provider;
import cn.hutool.core.util.StrUtil;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
/**
* 自定义 API 服务调用
*
* 适用于 OpenAI 兼容接口或仅通过通用 HTTP 协议接入的第三方大模型服务。
* 通过模型配置中的 apiHost / apiKey / modelName 即可复用,不需要再写死具体供应商。
*
* @author better
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class CustomApiServiceImpl implements AbstractChatService {
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(180);
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return OpenAiStreamingChatModel.builder()
.baseUrl(normalizeBaseUrl(chatModelVo.getApiHost()))
.apiKey(defaultIfBlank(chatModelVo.getApiKey(), "EMPTY"))
.modelName(chatModelVo.getModelName())
.timeout(DEFAULT_TIMEOUT)
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking())
.build();
}
@Override
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
return OpenAiChatModel.builder()
.baseUrl(normalizeBaseUrl(chatModelVo.getApiHost()))
.apiKey(defaultIfBlank(chatModelVo.getApiKey(), "EMPTY"))
.modelName(chatModelVo.getModelName())
.timeout(DEFAULT_TIMEOUT)
.build();
}
@Override
public String getProviderName() {
return ChatModeType.CUSTOM_API.getCode();
}
private String normalizeBaseUrl(String baseUrl) {
if (StrUtil.isBlank(baseUrl)) {
throw new IllegalArgumentException("自定义API的请求地址(apiHost)不能为空");
}
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
}
private String defaultIfBlank(String value, String defaultValue) {
return StrUtil.isBlank(value) ? defaultValue : value;
}
}

View File

@@ -0,0 +1,47 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 小米MiMo服务调用
* <p>
* 小米提供OpenAI兼容的API接口支持MiMo等模型。
*
* @author ageerle
* @date 2026/4/19
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class MiMoServiceImpl implements AbstractChatService {
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return OpenAiStreamingChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking())
.build();
}
@Override
public String getProviderName() {
return ChatModeType.XIAOMI.getCode();
}
}

View File

@@ -0,0 +1,44 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* MiniMax服务调用
* <p>
* MiniMax提供OpenAI兼容的API接口支持MiniMax-M2.7、MiniMax-M2.5等模型。
* API地址https://api.minimax.io/v1
*
* @author octopus
* @date 2026/3/21
*/
@Service
@Slf4j
public class MinimaxServiceImpl implements AbstractChatService {
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return OpenAiStreamingChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking())
.build();
}
@Override
public String getProviderName() {
return ChatModeType.MINIMAX.getCode();
}
}

View File

@@ -1,7 +1,9 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -37,6 +39,14 @@ public class OllamaServiceImpl implements AbstractChatService {
.build(); .build();
} }
@Override
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
return OllamaChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.modelName(chatModelVo.getModelName())
.build();
}
@Override @Override
public String getProviderName() { public String getProviderName() {
return ChatModeType.OLLAMA.getCode(); return ChatModeType.OLLAMA.getCode();

View File

@@ -1,7 +1,9 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -38,6 +40,14 @@ public class QianWenChatServiceImpl implements AbstractChatService {
.build(); .build();
} }
@Override
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
return QwenChatModel.builder()
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.build();
}
@Override @Override
public String getProviderName() { public String getProviderName() {
return ChatModeType.QIAN_WEN.getCode(); return ChatModeType.QIAN_WEN.getCode();

View File

@@ -1,7 +1,9 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.community.model.zhipu.ZhipuAiChatModel;
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -35,6 +37,14 @@ public class ZhiPuChatServiceImpl implements AbstractChatService {
.build(); .build();
} }
@Override
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
return ZhipuAiChatModel.builder()
.apiKey(chatModelVo.getApiKey())
.model(chatModelVo.getModelName())
.build();
}
@Override @Override
public String getProviderName() { public String getProviderName() {
return ChatModeType.ZHI_PU.getCode(); return ChatModeType.ZHI_PU.getCode();

View File

@@ -0,0 +1,17 @@
package org.ruoyi.service.embed.impl;
import org.springframework.stereotype.Component;
/**
* MiniMax嵌入模型兼容OpenAI接口
* <p>
* 支持embo-01模型1536维度向量。
* API地址https://api.minimax.io/v1
*
* @author octopus
* @date 2026/3/21
*/
@Component("minimax")
public class MinimaxEmbeddingProvider extends OpenAiEmbeddingProvider {
}

View File

@@ -0,0 +1,48 @@
package org.ruoyi.service.embed.impl;
import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ModalityType;
import org.ruoyi.service.embed.BaseEmbedModelService;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
/**
* @Author:yang
* @Date:
* @Description: 智谱AI嵌入模型
*/
@Component("zhipu")
public class ZhipuAiEmbeddingProvider implements BaseEmbedModelService {
protected ChatModelVo chatModelVo;
@Override
public void configure(ChatModelVo config) {
this.chatModelVo = config;
}
@Override
public Set<ModalityType> getSupportedModalities() {
return Set.of(ModalityType.TEXT);
}
@Override
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
EmbeddingModel model = ZhipuAiEmbeddingModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.model(chatModelVo.getModelName())
.dimensions(chatModelVo.getModelDimension())
.build();
return model.embedAll(textSegments);
}
}

View File

@@ -0,0 +1,70 @@
package org.ruoyi.service.rerank;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.model.scoring.ScoringModel;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.domain.bo.rerank.RerankRequest;
import org.ruoyi.domain.bo.rerank.RerankResult;
import java.util.List;
/**
* 重排序模型服务接口
* 继承 langchain4j 的 ScoringModel 接口
* 参考设计模式BaseEmbedModelService
*
* @author Yzm
* @date 2026-04-19
*/
public interface RerankModelService extends ScoringModel {
/**
* 根据配置信息配置重排序模型
*
* @param config 包含模型配置信息的 ChatModelVo 对象
*/
void configure(ChatModelVo config);
/**
* 执行重排序(批量文档)
* 这是业务层使用的便捷方法
*
* @param rerankRequest 重排序请求,包含查询文本和候选文档列表
* @return 重排序结果,包含排序后的文档和相关性分数
*/
RerankResult rerank(RerankRequest rerankRequest);
/**
* 实现 ScoringModel 接口的 scoreAll 方法
* 将 ScoringModel 的调用转换为重排序调用
*/
@Override
default Response<List<Double>> scoreAll(List<TextSegment> segments, String query) {
// 将 TextSegment 转换为文档字符串列表
List<String> documents = segments.stream()
.map(TextSegment::text)
.toList();
RerankRequest request = RerankRequest.builder()
.query(query)
.documents(documents)
.topN(documents.size())
.returnDocuments(false)
.build();
RerankResult result = rerank(request);
// 提取分数列表,按原始顺序排列
List<Double> scores = new java.util.ArrayList<>(
java.util.Collections.nCopies(documents.size(), 0.0));
for (RerankResult.RerankDocument doc : result.getDocuments()) {
if (doc.getIndex() != null && doc.getIndex() < documents.size()) {
scores.set(doc.getIndex(), doc.getRelevanceScore());
}
}
return Response.from(scores);
}
}

View File

@@ -0,0 +1,115 @@
package org.ruoyi.service.rerank.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.domain.bo.rerank.RerankRequest;
import org.ruoyi.domain.bo.rerank.RerankResult;
import org.ruoyi.domain.dto.request.AliBaiLianRerankRequest;
import org.ruoyi.domain.dto.response.AliBaiLianRerankResponse;
import org.ruoyi.service.rerank.RerankModelService;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 阿里百炼重排序模型实现
* 参考设计模式AliBaiLianMultiEmbeddingProvider
*
* @author yang
* @date 2026-04-20
*/
@Slf4j
@Component("qianwenRerank")
public class AliBaiLianRerankModelService implements RerankModelService {
private final OkHttpClient okHttpClient;
private final ObjectMapper objectMapper = new ObjectMapper();
private ChatModelVo chatModelVo;
public AliBaiLianRerankModelService() {
this.okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
}
@Override
public void configure(ChatModelVo config) {
this.chatModelVo = config;
}
@Override
public RerankResult rerank(RerankRequest rerankRequest) {
long startTime = System.currentTimeMillis();
try {
// 构建请求
AliBaiLianRerankRequest request = buildRequest(rerankRequest);
AliBaiLianRerankResponse response = executeRequest(request);
return response.toRerankResult(
rerankRequest.getDocuments().size(),
System.currentTimeMillis() - startTime
);
} catch (Exception e) {
log.error("阿里百炼重排序失败: {}", e.getMessage(), e);
throw new RuntimeException("重排序服务调用失败: " + e.getMessage(), e);
}
}
/**
* 构建请求对象
*/
private AliBaiLianRerankRequest buildRequest(RerankRequest rerankRequest) {
return AliBaiLianRerankRequest.create(
chatModelVo.getModelName(),
rerankRequest.getQuery(),
rerankRequest.getDocuments(),
rerankRequest.getTopN(),
rerankRequest.getReturnDocuments()
);
}
/**
* 执行HTTP请求并解析响应
*/
private AliBaiLianRerankResponse executeRequest(AliBaiLianRerankRequest request) throws IOException {
String jsonBody = request.toJson();
RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json"));
// 阿里百炼重排序 OpenAI兼容端点
String url = chatModelVo.getApiHost() + "/compatible-api/v1/reranks";
Request httpRequest = new Request.Builder()
.url(url)
.addHeader("Authorization", "Bearer " + chatModelVo.getApiKey())
.addHeader("Content-Type", "application/json")
.post(body)
.build();
try (Response response = okHttpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
String err = response.body() != null ? response.body().string() : "无错误信息";
throw new IllegalArgumentException("阿里百炼API调用失败: " + response.code() + " - " + err);
}
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new IllegalArgumentException("响应体为空");
}
return parseResponse(responseBody.string());
}
}
/**
* 解析响应
*/
private AliBaiLianRerankResponse parseResponse(String responseBody) throws IOException {
return objectMapper.readValue(responseBody, AliBaiLianRerankResponse.class);
}
}

View File

@@ -0,0 +1,163 @@
package org.ruoyi.service.rerank.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.MacAlgorithm;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.domain.bo.rerank.RerankRequest;
import org.ruoyi.domain.bo.rerank.RerankResult;
import org.ruoyi.domain.dto.request.ZhipuRerankRequest;
import org.ruoyi.domain.dto.response.ZhipuRerankResponse;
import org.ruoyi.service.rerank.RerankModelService;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
/**
* 智谱AI 重排序模型实现
* 参考设计模式AliBaiLianMultiEmbeddingProvider
*
* @author yang
* @date 2026-04-19
*/
@Slf4j
@Component("zhipuRerank")
public class ZhiPuRerankModelService implements RerankModelService {
private final OkHttpClient okHttpClient;
private final ObjectMapper objectMapper = new ObjectMapper();
private ChatModelVo chatModelVo;
public ZhiPuRerankModelService() {
this.okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
}
@Override
public void configure(ChatModelVo config) {
this.chatModelVo = config;
}
@Override
public RerankResult rerank(RerankRequest rerankRequest) {
long startTime = System.currentTimeMillis();
try {
// 构建请求
ZhipuRerankRequest request = buildRequest(rerankRequest);
ZhipuRerankResponse response = executeRequest(request);
return response.toRerankResult(
rerankRequest.getDocuments().size(),
System.currentTimeMillis() - startTime
);
} catch (Exception e) {
log.error("智谱重排序失败: {}", e.getMessage(), e);
throw new RuntimeException("重排序服务调用失败: " + e.getMessage(), e);
}
}
/**
* 构建请求对象
*/
private ZhipuRerankRequest buildRequest(RerankRequest rerankRequest) {
return ZhipuRerankRequest.create(
chatModelVo.getModelName(),
rerankRequest.getQuery(),
rerankRequest.getDocuments(),
rerankRequest.getTopN(),
rerankRequest.getReturnDocuments()
);
}
/**
* 执行HTTP请求并解析响应
*/
private ZhipuRerankResponse executeRequest(ZhipuRerankRequest request) throws IOException {
String jsonBody = request.toJson();
RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json"));
// 生成智谱认证Token
String token = generateToken(chatModelVo.getApiKey());
// 智谱重排序固定端点路径
String url = chatModelVo.getApiHost() + "/api/paas/v4/rerank";
Request httpRequest = new Request.Builder()
.url(url)
.addHeader("Authorization", token)
.post(body)
.build();
try (Response response = okHttpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
String err = response.body() != null ? response.body().string() : "无错误信息";
throw new IllegalArgumentException("智谱API调用失败: " + response.code() + " - " + err);
}
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new IllegalArgumentException("响应体为空");
}
return parseResponse(responseBody.string());
}
}
/**
* 解析响应
*/
private ZhipuRerankResponse parseResponse(String responseBody) throws IOException {
return objectMapper.readValue(responseBody, ZhipuRerankResponse.class);
}
/**
* 生成智谱JWT Token
*/
private String generateToken(String apiKey) {
try {
String[] apiKeyParts = apiKey.split("\\.");
String keyId = apiKeyParts[0];
String secret = apiKeyParts[1];
long expireMillis = 1000L * 60 * 30; // 30分钟
java.util.Map<String, Object> payload = new java.util.HashMap<>();
payload.put("api_key", keyId);
payload.put("exp", System.currentTimeMillis() + expireMillis);
payload.put("timestamp", System.currentTimeMillis());
// 使用反射创建 MacAlgorithm兼容不同版本的 jjwt
MacAlgorithm macAlgorithm;
try {
Class<?> c = Class.forName("io.jsonwebtoken.impl.security.DefaultMacAlgorithm");
Constructor<?> ctor = c.getDeclaredConstructor(String.class, String.class, int.class);
ctor.setAccessible(true);
macAlgorithm = (MacAlgorithm) ctor.newInstance("HS256", "HmacSHA256", 128);
} catch (Exception e) {
macAlgorithm = Jwts.SIG.HS256;
}
String token = Jwts.builder()
.header()
.add("alg", "HS256")
.add("sign_type", "SIGN")
.and()
.content(objectMapper.writeValueAsString(payload))
.signWith(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"), macAlgorithm)
.compact();
return "Bearer " + token;
} catch (Exception e) {
throw new RuntimeException("生成智谱Token失败: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,24 @@
package org.ruoyi.service.retrieval;
import org.ruoyi.domain.bo.vector.QueryVectorBo;
import java.util.List;
/**
* 知识库检索服务接口
* 整合粗召回(向量检索)和重排序流程
*
* @author yang
* @date 2026-04-19
*/
public interface KnowledgeRetrievalService {
/**
* 执行知识库检索,返回文本内容
* 流程:向量粗召回 -> 重排序(可选) -> 返回结果
*
* @param queryVectorBo 查询参数
* @return 文本内容列表
*/
List<String> retrieveTexts(QueryVectorBo queryVectorBo);
}

View File

@@ -0,0 +1,135 @@
package org.ruoyi.service.retrieval.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.domain.bo.rerank.RerankRequest;
import org.ruoyi.domain.bo.rerank.RerankResult;
import org.ruoyi.domain.bo.vector.QueryVectorBo;
import org.ruoyi.factory.RerankModelFactory;
import org.ruoyi.service.rerank.RerankModelService;
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
import org.ruoyi.service.vector.VectorStoreService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 知识库检索服务实现
* 整合粗召回(向量检索)和重排序流程
*
* @author yang
* @date 2026-04-19
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class KnowledgeRetrievalServiceImpl implements KnowledgeRetrievalService {
private final VectorStoreService vectorStoreService;
private final RerankModelFactory rerankModelFactory;
/**
* 粗召回默认扩大倍数
* 如果启用重排序,粗召回会获取更多结果供重排序筛选
*/
private static final int RERANK_EXPANSION_FACTOR = 3;
@Override
public List<String> retrieveTexts(QueryVectorBo queryVectorBo) {
log.info("开始知识库检索, kid={}, query={}", queryVectorBo.getKid(), queryVectorBo.getQuery());
// 1. 粗召回阶段 - 向量检索
List<String> coarseResults = coarseRetrieval(queryVectorBo);
log.debug("粗召回返回 {} 条结果", coarseResults.size());
if (coarseResults.isEmpty()) {
return coarseResults;
}
// 2. 重排序阶段(可选)
if (Boolean.TRUE.equals(queryVectorBo.getEnableRerank()) &&
queryVectorBo.getRerankModelName() != null) {
return rerank(queryVectorBo, coarseResults);
}
return coarseResults;
}
/**
* 粗召回阶段 - 向量检索
*/
private List<String> coarseRetrieval(QueryVectorBo queryVectorBo) {
// 如果启用重排序,扩大粗召回数量
int originalMaxResults = queryVectorBo.getMaxResults();
int expandedResults = originalMaxResults;
if (Boolean.TRUE.equals(queryVectorBo.getEnableRerank()) &&
queryVectorBo.getRerankModelName() != null) {
expandedResults = originalMaxResults * RERANK_EXPANSION_FACTOR;
log.debug("启用重排序,粗召回数量从 {} 扩大到 {}", originalMaxResults, expandedResults);
}
// 临时修改查询数量
queryVectorBo.setMaxResults(expandedResults);
try {
return vectorStoreService.getQueryVector(queryVectorBo);
} finally {
// 恢复原始值
queryVectorBo.setMaxResults(originalMaxResults);
}
}
/**
* 重排序阶段
*/
private List<String> rerank(QueryVectorBo queryVectorBo, List<String> coarseResults) {
long startTime = System.currentTimeMillis();
try {
// 1. 通过工厂获取重排序模型
RerankModelService rerankModel = rerankModelFactory.createModel(queryVectorBo.getRerankModelName());
// 2. 构建重排序请求
int topN = queryVectorBo.getRerankTopN() != null ?
queryVectorBo.getRerankTopN() : queryVectorBo.getMaxResults();
RerankRequest rerankRequest = RerankRequest.builder()
.query(queryVectorBo.getQuery())
.documents(coarseResults)
.topN(topN)
.returnDocuments(true)
.build();
log.info("执行重排序, model={}, documents={}, topN={}",
queryVectorBo.getRerankModelName(), coarseResults.size(), topN);
// 3. 执行重排序
RerankResult rerankResult = rerankModel.rerank(rerankRequest);
// 4. 转换重排序结果
List<String> finalResults = new ArrayList<>();
for (RerankResult.RerankDocument doc : rerankResult.getDocuments()) {
// 应用分数阈值过滤
if (queryVectorBo.getRerankScoreThreshold() != null &&
doc.getRelevanceScore() < queryVectorBo.getRerankScoreThreshold()) {
continue;
}
if (doc.getDocument() != null) {
finalResults.add(doc.getDocument());
}
}
long duration = System.currentTimeMillis() - startTime;
log.info("重排序完成, 返回 {} 条结果, 耗时 {}ms", finalResults.size(), duration);
return finalResults;
} catch (Exception e) {
log.error("重排序失败: {}", e.getMessage(), e);
// 重排序失败时返回原始粗召回结果(截取到期望数量)
int limit = Math.min(queryVectorBo.getMaxResults(), coarseResults.size());
return new ArrayList<>(coarseResults.subList(0, limit));
}
}
}

View File

@@ -0,0 +1,42 @@
package org.ruoyi.enums;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for ChatModeType enum
*/
class ChatModeTypeTest {
@Test
void minimaxEnumExists() {
ChatModeType minimax = ChatModeType.MINIMAX;
assertNotNull(minimax);
}
@Test
void minimaxCode_isMinimax() {
assertEquals("minimax", ChatModeType.MINIMAX.getCode());
}
@Test
void minimaxDescription_isMiniMax() {
assertEquals("MiniMax", ChatModeType.MINIMAX.getDescription());
}
@Test
void allProviders_haveUniqueCode() {
ChatModeType[] values = ChatModeType.values();
long uniqueCodes = java.util.Arrays.stream(values)
.map(ChatModeType::getCode)
.distinct()
.count();
assertEquals(values.length, uniqueCodes, "All providers must have unique codes");
}
@Test
void valueOf_minimax() {
assertEquals(ChatModeType.MINIMAX, ChatModeType.valueOf("MINIMAX"));
}
}

View File

@@ -0,0 +1,70 @@
package org.ruoyi.integration;
import dev.langchain4j.model.chat.StreamingChatModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.service.chat.impl.provider.MinimaxServiceImpl;
import static org.junit.jupiter.api.Assertions.*;
/**
* Integration tests for MiniMax provider.
* These tests require a valid MINIMAX_API_KEY environment variable.
*/
@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".+")
class MinimaxIntegrationTest {
private MinimaxServiceImpl minimaxService;
private String apiKey;
@BeforeEach
void setUp() {
minimaxService = new MinimaxServiceImpl();
apiKey = System.getenv("MINIMAX_API_KEY");
}
@Test
void buildStreamingChatModel_withRealApiKey_M27() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey(apiKey);
modelVo.setModelName("MiniMax-M2.7");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model, "Should create streaming model with real API key");
}
@Test
void buildStreamingChatModel_withRealApiKey_M25() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey(apiKey);
modelVo.setModelName("MiniMax-M2.5");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model, "Should create streaming model with M2.5");
}
@Test
void buildStreamingChatModel_withRealApiKey_M25Highspeed() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey(apiKey);
modelVo.setModelName("MiniMax-M2.5-highspeed");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model, "Should create streaming model with M2.5-highspeed");
}
}

View File

@@ -0,0 +1,76 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for MinimaxServiceImpl
*/
class MinimaxServiceImplTest {
private MinimaxServiceImpl minimaxService;
@BeforeEach
void setUp() {
minimaxService = new MinimaxServiceImpl();
}
@Test
void getProviderName_returnsMinimaxCode() {
assertEquals("minimax", minimaxService.getProviderName());
assertEquals(ChatModeType.MINIMAX.getCode(), minimaxService.getProviderName());
}
@Test
void buildStreamingChatModel_returnsNonNull() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey("test-api-key");
modelVo.setModelName("MiniMax-M2.7");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model);
}
@Test
void buildStreamingChatModel_withThinkingEnabled() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey("test-api-key");
modelVo.setModelName("MiniMax-M2.5");
ChatRequest request = new ChatRequest();
request.setEnableThinking(true);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model);
}
@Test
void buildStreamingChatModel_withHighspeedModel() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey("test-api-key");
modelVo.setModelName("MiniMax-M2.5-highspeed");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model);
}
@Test
void implementsAbstractChatService() {
assertInstanceOf(org.ruoyi.service.chat.AbstractChatService.class, minimaxService);
}
}

View File

@@ -0,0 +1,55 @@
package org.ruoyi.service.embed.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ModalityType;
import org.ruoyi.service.embed.BaseEmbedModelService;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for MinimaxEmbeddingProvider
*/
class MinimaxEmbeddingProviderTest {
private MinimaxEmbeddingProvider provider;
@BeforeEach
void setUp() {
provider = new MinimaxEmbeddingProvider();
}
@Test
void implementsBaseEmbedModelService() {
assertInstanceOf(BaseEmbedModelService.class, provider);
}
@Test
void extendsOpenAiEmbeddingProvider() {
assertInstanceOf(OpenAiEmbeddingProvider.class, provider);
}
@Test
void getSupportedModalities_returnsText() {
Set<ModalityType> modalities = provider.getSupportedModalities();
assertNotNull(modalities);
assertTrue(modalities.contains(ModalityType.TEXT));
assertEquals(1, modalities.size());
}
@Test
void configure_setsModelConfig() {
ChatModelVo config = new ChatModelVo();
config.setApiHost("https://api.minimax.io/v1");
config.setApiKey("test-api-key");
config.setModelName("embo-01");
config.setModelDimension(1536);
provider.configure(config);
// configure sets internal state; verify no exception thrown
assertNotNull(provider);
}
}

View File

@@ -0,0 +1,126 @@
package org.ruoyi.service.rerank.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.domain.bo.rerank.RerankRequest;
import org.ruoyi.domain.bo.rerank.RerankResult;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* 阿里百炼重排序模型测试类
* 运行前请设置环境变量 DASHSCOPE_API_KEY 或直接修改 apiKey
*/
class AliBaiLianRerankModelServiceTest {
private AliBaiLianRerankModelService service;
// 请替换为你的 API Key
private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");
private static final String API_HOST = "https://dashscope.aliyuncs.com";
private static final String MODEL_NAME = "qwen3-rerank";
@BeforeEach
void setUp() {
service = new AliBaiLianRerankModelService();
}
@Test
void testConfigure() {
ChatModelVo config = createConfig();
service.configure(config);
assertNotNull(service);
}
@Test
void testRerank() {
// 跳过测试如果没有配置 API Key
if (API_KEY == null || API_KEY.isEmpty()) {
System.out.println("跳过测试: 未设置环境变量 DASHSCOPE_API_KEY");
return;
}
ChatModelVo config = createConfig();
service.configure(config);
List<String> documents = Arrays.asList(
"文本排序模型广泛用于搜索引擎和推荐系统中,它们根据文本相关性对候选文本进行排序",
"量子计算是计算科学的一个前沿领域",
"预训练语言模型的发展给文本排序模型带来了新的进展"
);
RerankRequest request = RerankRequest.builder()
.query("什么是文本排序模型")
.documents(documents)
.topN(2)
.returnDocuments(true)
.build();
RerankResult result = service.rerank(request);
System.out.println("=== 重排序结果 ===");
System.out.println("总文档数: " + result.getTotalDocuments());
System.out.println("耗时: " + result.getDurationMs() + "ms");
result.getDocuments().forEach(doc -> {
System.out.println("索引: " + doc.getIndex() +
", 相关性分数: " + doc.getRelevanceScore() +
", 文档: " + doc.getDocument());
});
assertNotNull(result);
assertNotNull(result.getDocuments());
assertFalse(result.getDocuments().isEmpty());
assertEquals(2, result.getDocuments().size());
}
@Test
void testRerankWithFullDocuments() {
if (API_KEY == null || API_KEY.isEmpty()) {
System.out.println("跳过测试: 未设置环境变量 DASHSCOPE_API_KEY");
return;
}
ChatModelVo config = createConfig();
service.configure(config);
List<String> documents = Arrays.asList(
"Java是一种广泛使用的编程语言",
"Python是人工智能领域最流行的语言",
"Go语言由Google开发适合并发编程"
);
RerankRequest request = RerankRequest.builder()
.query("哪种语言适合AI开发")
.documents(documents)
.build();
RerankResult result = service.rerank(request);
System.out.println("=== 重排序结果2 ===");
result.getDocuments().forEach(doc -> {
System.out.println("索引: " + doc.getIndex() +
", 分数: " + doc.getRelevanceScore() +
", 文档: " + doc.getDocument());
});
assertNotNull(result);
assertEquals(3, result.getDocuments().size());
// Python相关文档应该排在前面
assertEquals(1, result.getDocuments().get(0).getIndex());
assertTrue(result.getDocuments().get(0).getRelevanceScore() > 0.5);
}
private ChatModelVo createConfig() {
ChatModelVo config = new ChatModelVo();
config.setApiHost(API_HOST);
config.setApiKey(API_KEY != null ? API_KEY : "test-api-key");
config.setModelName(MODEL_NAME);
return config;
}
}

View File

@@ -0,0 +1,73 @@
package org.ruoyi.service.rerank.impl;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.domain.bo.rerank.RerankRequest;
import org.ruoyi.domain.bo.rerank.RerankResult;
import java.util.Arrays;
import java.util.List;
/**
* 阿里百炼重排序模型测试 - Main方法直接运行
* 运行前请设置 API_KEY
*/
public class AliBaiLianRerankTestMain {
// 请替换为你的 API Key
private static final String API_KEY = "sk-your-api-key-here";
private static final String API_HOST = "https://dashscope.aliyuncs.com";
private static final String MODEL_NAME = "qwen3-rerank";
public static void main(String[] args) {
AliBaiLianRerankModelService service = new AliBaiLianRerankModelService();
// 配置
ChatModelVo config = new ChatModelVo();
config.setApiHost(API_HOST);
config.setApiKey(API_KEY);
config.setModelName(MODEL_NAME);
service.configure(config);
// 测试数据
List<String> documents = Arrays.asList(
"文本排序模型广泛用于搜索引擎和推荐系统中,它们根据文本相关性对候选文本进行排序",
"量子计算是计算科学的一个前沿领域",
"预训练语言模型的发展给文本排序模型带来了新的进展"
);
RerankRequest request = RerankRequest.builder()
.query("什么是文本排序模型")
.documents(documents)
.topN(2)
.returnDocuments(true)
.build();
System.out.println("=== 开始测试阿里百炼重排序 ===");
System.out.println("API Host: " + API_HOST);
System.out.println("Model: " + MODEL_NAME);
System.out.println("Query: 什么是文本排序模型");
System.out.println();
try {
RerankResult result = service.rerank(request);
System.out.println("=== 重排序结果 ===");
System.out.println("总文档数: " + result.getTotalDocuments());
System.out.println("耗时: " + result.getDurationMs() + "ms");
System.out.println();
result.getDocuments().forEach(doc -> {
System.out.println("索引: " + doc.getIndex());
System.out.println("相关性分数: " + doc.getRelevanceScore());
System.out.println("文档: " + doc.getDocument());
System.out.println("---");
});
System.out.println("=== 测试成功 ===");
} catch (Exception e) {
System.err.println("=== 测试失败 ===");
e.printStackTrace();
}
}
}