diff --git a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql index 71e1d9e..c031ad9 100644 --- a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql +++ b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql @@ -42,6 +42,7 @@ CREATE TABLE `tb_ai_knowledge` ( `embedding_model_provider` VARCHAR(100) DEFAULT NULL COMMENT '向量模型提供商', `rerank_model` VARCHAR(100) DEFAULT NULL COMMENT 'Rerank模型名称', `rerank_model_provider` VARCHAR(100) DEFAULT NULL COMMENT 'Rerank模型提供商', + `reranking_enable` TINYINT(1) DEFAULT 0 COMMENT '是否启用Rerank(0否 1是)', `retrieval_top_k` INT(11) DEFAULT 2 COMMENT '检索Top K(返回前K个结果)', `retrieval_score_threshold` DECIMAL(3,2) DEFAULT 0.00 COMMENT '检索分数阈值(0.00-1.00)', `vector_id` VARCHAR(100) DEFAULT NULL COMMENT '向量ID(用于向量检索)', diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalModel.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalModel.java index 987912b..cad94ee 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalModel.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalModel.java @@ -19,24 +19,24 @@ public class RetrievalModel { @JSONField(name = "search_method") private String searchMethod; - /** - * Rerank模型提供商 - */ - @JSONField(name = "reranking_provider_name") - private String rerankingProviderName; - - /** - * Rerank模型名称 - */ - @JSONField(name = "reranking_model") - private String rerankingModel; - /** * Rerank是否启用 */ @JSONField(name = "reranking_enable") private Boolean rerankingEnable; + /** + * Rerank模式(字符串,值为 "reranking_model") + */ + @JSONField(name = "reranking_mode") + private String rerankingMode; + + /** + * Rerank模型配置(当 reranking_enable=true 时必须设置) + */ + @JSONField(name = "reranking_model") + private RerankingModel rerankingModel; + /** * Top K(返回前K个结果) */ @@ -54,5 +54,23 @@ public class RetrievalModel { */ @JSONField(name = "score_threshold_enabled") private Boolean scoreThresholdEnabled; + + /** + * Rerank模型配置(嵌套对象) + */ + @Data + public static class RerankingModel { + /** + * Rerank模型提供商 + */ + @JSONField(name = "reranking_provider_name") + private String rerankingProviderName; + + /** + * Rerank模型名称 + */ + @JSONField(name = "reranking_model_name") + private String rerankingModelName; + } } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java index 0a0b474..df6f388 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java @@ -29,6 +29,8 @@ import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.common.vo.UserDeptRoleVO; import org.xyzh.system.utils.LoginUtil; +import com.alibaba.fastjson2.JSON; + import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -118,7 +120,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { // 设置检索模型配置(Rerank、Top K、Score 阈值) RetrievalModel retrievalModel = new RetrievalModel(); - retrievalModel.setSearchMethod("hybrid_search"); // 默认使用混合搜索 + retrievalModel.setSearchMethod("hybrid_search"); // 必填字段 // Top K 配置 if (knowledge.getRetrievalTopK() != null && knowledge.getRetrievalTopK() > 0) { @@ -136,20 +138,40 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { retrievalModel.setScoreThresholdEnabled(false); } - // Rerank 模型配置 - if (StringUtils.hasText(knowledge.getRerankModel())) { - retrievalModel.setRerankingEnable(true); - retrievalModel.setRerankingModel(knowledge.getRerankModel()); - retrievalModel.setRerankingProviderName(knowledge.getRerankModelProvider()); - log.info("创建知识库 - 启用Rerank: model={}, provider={}", - knowledge.getRerankModel(), knowledge.getRerankModelProvider()); + // Rerank 模型配置(以前端传参为准) + Boolean rerankEnable = knowledge.getRerankingEnable() != null ? + knowledge.getRerankingEnable() : false; + retrievalModel.setRerankingEnable(rerankEnable); + + if (rerankEnable) { + // 启用 Rerank 时,model 和 provider 必须有值 + if (!StringUtils.hasText(knowledge.getRerankModel())) { + throw new IllegalArgumentException("启用Rerank后必须指定rerankModel"); + } + if (!StringUtils.hasText(knowledge.getRerankModelProvider())) { + throw new IllegalArgumentException("启用Rerank后必须指定rerankModelProvider"); + } + + // 设置 reranking_mode 为固定值 "reranking_model" + retrievalModel.setRerankingMode("reranking_model"); + + // 创建 RerankingModel 对象(嵌套在 reranking_model 字段中) + RetrievalModel.RerankingModel rerankingModel = new RetrievalModel.RerankingModel(); + rerankingModel.setRerankingProviderName(knowledge.getRerankModelProvider()); + rerankingModel.setRerankingModelName(knowledge.getRerankModel()); + retrievalModel.setRerankingModel(rerankingModel); + + log.info("创建知识库 - 启用Rerank: enable={}, mode=reranking_model, model={}, provider={}", + rerankEnable, knowledge.getRerankModel(), knowledge.getRerankModelProvider()); } else { - retrievalModel.setRerankingEnable(false); + // 禁用 Rerank(不设置 rerankingMode 和 rerankingModel) + log.info("创建知识库 - 禁用Rerank"); } difyRequest.setRetrievalModel(retrievalModel); // 调用Dify API创建知识库(使用知识库API Key) + log.info("创建知识库 - 请求参数: {}", JSON.toJSONString(difyRequest)); DatasetCreateResponse difyResponse = difyApiClient.createDataset(difyRequest); difyDatasetId = difyResponse.getId(); @@ -300,6 +322,82 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { } needUpdateDify = true; } + + // 检索配置变化(Rerank、Top K、Score阈值) + boolean retrievalConfigChanged = false; + + // 检测 Rerank 开关状态变化 + Boolean newRerankEnable = knowledge.getRerankingEnable(); + Boolean existingRerankEnable = existing.getRerankingEnable(); + boolean rerankEnableChanged = (newRerankEnable != null && !newRerankEnable.equals(existingRerankEnable)); + + // 检测 Rerank 模型变化 + String newRerankModel = knowledge.getRerankModel(); + String existingRerankModel = existing.getRerankModel(); + boolean rerankModelChanged = (newRerankModel != null && !newRerankModel.equals(existingRerankModel)); + + if (rerankEnableChanged || rerankModelChanged || + (knowledge.getRetrievalTopK() != null && !knowledge.getRetrievalTopK().equals(existing.getRetrievalTopK())) || + (knowledge.getRetrievalScoreThreshold() != null && !knowledge.getRetrievalScoreThreshold().equals(existing.getRetrievalScoreThreshold()))) { + retrievalConfigChanged = true; + } + + if (retrievalConfigChanged) { + RetrievalModel retrievalModel = new RetrievalModel(); + retrievalModel.setSearchMethod("hybrid_search"); // 必填字段 + + // Top K + if (knowledge.getRetrievalTopK() != null) { + retrievalModel.setTopK(knowledge.getRetrievalTopK()); + } else { + retrievalModel.setTopK(existing.getRetrievalTopK() != null ? existing.getRetrievalTopK() : 2); + } + + // Score 阈值 + Double scoreThreshold = knowledge.getRetrievalScoreThreshold() != null ? + knowledge.getRetrievalScoreThreshold() : + (existing.getRetrievalScoreThreshold() != null ? existing.getRetrievalScoreThreshold() : 0.0); + retrievalModel.setScoreThreshold(scoreThreshold); + retrievalModel.setScoreThresholdEnabled(scoreThreshold > 0); + + // Rerank 配置(以前端传参为准) + Boolean finalRerankEnable = newRerankEnable != null ? newRerankEnable : + (existingRerankEnable != null ? existingRerankEnable : false); + String finalRerankModel = newRerankModel != null ? newRerankModel : existingRerankModel; + String finalRerankProvider = knowledge.getRerankModelProvider() != null ? + knowledge.getRerankModelProvider() : existing.getRerankModelProvider(); + + // 直接使用前端传入的开关状态 + retrievalModel.setRerankingEnable(finalRerankEnable); + + if (finalRerankEnable) { + // 启用 Rerank 时,model 和 provider 必须有值 + if (!StringUtils.hasText(finalRerankModel)) { + throw new IllegalArgumentException("启用Rerank后必须指定rerankModel"); + } + if (!StringUtils.hasText(finalRerankProvider)) { + throw new IllegalArgumentException("启用Rerank后必须指定rerankModelProvider"); + } + + // 设置 reranking_mode 为固定值 "reranking_model" + retrievalModel.setRerankingMode("reranking_model"); + + // 创建 RerankingModel 对象(嵌套在 reranking_model 字段中) + RetrievalModel.RerankingModel rerankingModel = new RetrievalModel.RerankingModel(); + rerankingModel.setRerankingProviderName(finalRerankProvider); + rerankingModel.setRerankingModelName(finalRerankModel); + retrievalModel.setRerankingModel(rerankingModel); + + log.info("更新Rerank配置: 启用 - enable={}, mode=reranking_model, model={}, provider={}", + finalRerankEnable, finalRerankModel, finalRerankProvider); + } else { + // 禁用 Rerank(不设置 rerankingMode 和 rerankingModel) + log.info("更新Rerank配置: 禁用 - enable={}", finalRerankEnable); + } + + updateRequest.setRetrievalModel(retrievalModel); + needUpdateDify = true; + } // 同步到Dify if (needUpdateDify && StringUtils.hasText(existing.getDifyDatasetId())) { diff --git a/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml b/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml index 881a736..a203c49 100644 --- a/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml +++ b/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml @@ -21,6 +21,7 @@ + @@ -40,7 +41,7 @@ id, title, avatar, description, content, source_type, source_id, file_name, file_path, category, tags, dify_dataset_id, dify_indexing_technique, embedding_model, embedding_model_provider, - rerank_model, rerank_model_provider, retrieval_top_k, retrieval_score_threshold, + rerank_model, rerank_model_provider, reranking_enable, retrieval_top_k, retrieval_score_threshold, vector_id, document_count, total_chunks, status, creator, creator_dept, updater, create_time, update_time, delete_time, deleted @@ -170,13 +171,13 @@ INSERT INTO tb_ai_knowledge ( id, title, avatar, description, content, source_type, source_id, file_name, file_path, category, tags, dify_dataset_id, dify_indexing_technique, embedding_model, embedding_model_provider, - rerank_model, rerank_model_provider, retrieval_top_k, retrieval_score_threshold, + rerank_model, rerank_model_provider, reranking_enable, retrieval_top_k, retrieval_score_threshold, vector_id, document_count, total_chunks, status, creator, creator_dept, updater, create_time, update_time, deleted ) VALUES ( #{ID}, #{title}, #{avatar}, #{description}, #{content}, #{sourceType}, #{sourceID}, #{fileName}, #{filePath}, #{category}, #{tags}, #{difyDatasetId}, #{difyIndexingTechnique}, #{embeddingModel}, #{embeddingModelProvider}, - #{rerankModel}, #{rerankModelProvider}, #{retrievalTopK}, #{retrievalScoreThreshold}, + #{rerankModel}, #{rerankModelProvider}, #{rerankingEnable}, #{retrievalTopK}, #{retrievalScoreThreshold}, #{vectorID}, #{documentCount}, #{totalChunks}, #{status}, #{creator}, #{creatorDept}, #{updater}, #{createTime}, #{updateTime}, #{deleted} ) @@ -202,6 +203,7 @@ embedding_model_provider = #{embeddingModelProvider}, rerank_model = #{rerankModel}, rerank_model_provider = #{rerankModelProvider}, + reranking_enable = #{rerankingEnable}, retrieval_top_k = #{retrievalTopK}, retrieval_score_threshold = #{retrievalScoreThreshold}, vector_id = #{vectorID}, diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java index 88abb23..b231bd3 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java @@ -93,6 +93,11 @@ public class TbAiKnowledge extends BaseDTO { */ private String rerankModelProvider; + /** + * @description 是否启用Rerank + */ + private Boolean rerankingEnable; + /** * @description 检索Top K(返回前K个结果) */ @@ -322,6 +327,14 @@ public class TbAiKnowledge extends BaseDTO { this.rerankModelProvider = rerankModelProvider; } + public Boolean getRerankingEnable() { + return rerankingEnable; + } + + public void setRerankingEnable(Boolean rerankingEnable) { + this.rerankingEnable = rerankingEnable; + } + public Integer getRetrievalTopK() { return retrievalTopK; } diff --git a/schoolNewsWeb/src/apis/ai/knowledge.ts b/schoolNewsWeb/src/apis/ai/knowledge.ts index e648bea..78373a4 100644 --- a/schoolNewsWeb/src/apis/ai/knowledge.ts +++ b/schoolNewsWeb/src/apis/ai/knowledge.ts @@ -142,6 +142,15 @@ export const knowledgeApi = { async getAvailableEmbeddingModels(): Promise> { const response = await api.get('/ai/knowledge/embedding-models'); return response.data; + }, + + /** + * 获取可用的Rerank模型列表 + * @returns Promise> + */ + async getAvailableRerankModels(): Promise> { + const response = await api.get('/ai/knowledge/rerank-models'); + return response.data; } }; diff --git a/schoolNewsWeb/src/types/ai/index.ts b/schoolNewsWeb/src/types/ai/index.ts index 5d096ef..8d2f46c 100644 --- a/schoolNewsWeb/src/types/ai/index.ts +++ b/schoolNewsWeb/src/types/ai/index.ts @@ -75,6 +75,16 @@ export interface AiKnowledge extends BaseDTO { embeddingModel?: string; /** Embedding模型提供商 */ embeddingModelProvider?: string; + /** Rerank模型 */ + rerankModel?: string; + /** Rerank模型提供商 */ + rerankModelProvider?: string; + /** 是否启用Rerank */ + rerankingEnable?: boolean; + /** 检索Top K(返回前K个结果) */ + retrievalTopK?: number; + /** 检索分数阈值(0.00-1.00) */ + retrievalScoreThreshold?: number; /** 向量ID */ vectorID?: string; /** 文档数量 */ diff --git a/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue index 155c242..51f82ac 100644 --- a/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue +++ b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue @@ -25,14 +25,32 @@ 索引方式: {{ getIndexingText(knowledge?.difyIndexingTechnique) }} -
- Embedding模型: - {{ knowledge?.embeddingModel || '-' }} -
-
- 文档数量: - {{ knowledge?.documentCount || 0 }} -
+
+ Embedding模型: + {{ knowledge?.embeddingModel || '-' }} +
+
+ Rerank状态: + + {{ knowledge?.rerankingEnable ? '已启用' : '未启用' }} + +
+
+ Rerank模型: + {{ knowledge.rerankModel }} +
+
+ 检索Top K: + {{ knowledge?.retrievalTopK || 2 }} +
+
+ 检索阈值: + {{ knowledge?.retrievalScoreThreshold }} +
+
+ 文档数量: + {{ knowledge?.documentCount || 0 }} +
创建时间: {{ formatDate(knowledge?.createTime) }} @@ -138,6 +156,81 @@
+ + + +
+ Rerank可以对检索结果进行重新排序,提高精确度 +
+
+ + + + + + {{ getModelLabel(model) }} + + 上下文: {{ model.contextSize }} + + + + +
+ 选择用于重新排序的Rerank模型 +
+
+ + + + +
+ 返回前K个最相关的检索结果,建议范围:2-10 +
+
+ + + +
+ 只返回分数高于此阈值的结果(0.00-1.00),0表示不启用 +
+
+ (); const submitting = ref(false); const modelsLoading = ref(false); +const rerankModelsLoading = ref(false); const embeddingModels = ref([]); +const rerankModels = ref([]); // 表单数据 const formData = reactive>({ @@ -209,6 +304,11 @@ const formData = reactive>({ difyIndexingTechnique: 'high_quality', embeddingModel: '', embeddingModelProvider: '', + rerankModel: '', + rerankModelProvider: '', + rerankingEnable: false, + retrievalTopK: 2, + retrievalScoreThreshold: 0.0, difyDatasetId: '', status: 1 }); @@ -221,6 +321,18 @@ const rules: FormRules = { ], difyIndexingTechnique: [ { required: true, message: '请选择索引方式', trigger: 'change' } + ], + rerankModel: [ + { + validator: (rule: any, value: any, callback: any) => { + if (formData.rerankingEnable && !value) { + callback(new Error('启用Rerank后必须选择Rerank模型')); + } else { + callback(); + } + }, + trigger: 'change' + } ] }; @@ -235,12 +347,25 @@ watch(() => props.knowledge, (newVal) => { difyIndexingTechnique: newVal.difyIndexingTechnique || 'high_quality', embeddingModel: newVal.embeddingModel, embeddingModelProvider: newVal.embeddingModelProvider, + rerankModel: newVal.rerankModel, + rerankModelProvider: newVal.rerankModelProvider, + rerankingEnable: newVal.rerankingEnable ?? false, + retrievalTopK: newVal.retrievalTopK ?? 2, + retrievalScoreThreshold: newVal.retrievalScoreThreshold ?? 0.0, difyDatasetId: newVal.difyDatasetId, status: newVal.status ?? 1 }); } }, { immediate: true }); +// 监听 rerankingEnable 变化,触发表单验证 +watch(() => formData.rerankingEnable, () => { + if (formRef.value) { + // 触发 rerankModel 字段的验证 + formRef.value.validateField('rerankModel', () => {}); + } +}); + // 处理头像更新 function handleAvatarUpdate(val: string) { formData.avatar = val; @@ -295,12 +420,43 @@ async function loadEmbeddingModels() { } } +// 加载Rerank模型列表 +async function loadRerankModels() { + try { + rerankModelsLoading.value = true; + const result = await knowledgeApi.getAvailableRerankModels(); + if (result.success && result.data) { + // 按提供商分组 + const providers = result.data.providers || []; + rerankModels.value = providers.map((provider: any) => ({ + provider: provider.provider, + label: provider.label?.zh_Hans || provider.label?.en_US || provider.provider, + models: (provider.models || []).map((model: any) => ({ + model: model.model, + provider: model.provider || provider.provider, + label: model.label?.zh_Hans || model.label?.en_US || model.model, + contextSize: model.model_properties?.context_size, + status: model.status + })) + })); + } else { + ElMessage.warning('获取Rerank模型列表失败'); + rerankModels.value = []; + } + } catch (error: any) { + console.error('加载Rerank模型列表失败:', error); + rerankModels.value = []; + } finally { + rerankModelsLoading.value = false; + } +} + // 获取模型显示标签 function getModelLabel(model: any): string { return model.label || model.model; } -// 处理模型变化 +// 处理Embedding模型变化 function handleModelChange(modelName: string) { if (!modelName) { formData.embeddingModelProvider = ''; @@ -312,7 +468,25 @@ function handleModelChange(modelName: string) { const foundModel = providerGroup.models.find((m: any) => m.model === modelName); if (foundModel) { formData.embeddingModelProvider = foundModel.provider; - console.log('选择模型:', modelName, '提供商:', foundModel.provider); + console.log('选择Embedding模型:', modelName, '提供商:', foundModel.provider); + break; + } + } +} + +// 处理Rerank模型变化 +function handleRerankModelChange(modelName: string) { + if (!modelName) { + formData.rerankModelProvider = ''; + return; + } + + // 查找选中模型的提供商 + for (const providerGroup of rerankModels.value) { + const foundModel = providerGroup.models.find((m: any) => m.model === modelName); + if (foundModel) { + formData.rerankModelProvider = foundModel.provider; + console.log('选择Rerank模型:', modelName, '提供商:', foundModel.provider); break; } } @@ -320,6 +494,7 @@ function handleModelChange(modelName: string) { // 组件挂载时加载模型列表 loadEmbeddingModels(); +loadRerankModels(); // 提交表单 async function handleSubmit() {