1 Commits

Author SHA1 Message Date
wangle
bc151e49c5 feat: 添加Dify平台集成支持
- 升级 dify-sdk-java 从 1.0.7 到 1.2.6
- 新增 ChatModeType.DIFY 枚举类型
- 新增 DifyChatServiceImpl、DifyConversationService、DifyWorkflowService 实现
- 新增 DifyStreamingChatModel 流式聊天模型
- 支持Dify工作流对话模式
- Dify自带RAG知识库时跳过本地向量库查询

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 19:19:26 +08:00
26 changed files with 408 additions and 1334 deletions

View File

@@ -1,46 +0,0 @@
/*
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

@@ -58,7 +58,7 @@
<langchain4j.community.version>1.13.0-beta23</langchain4j.community.version> <langchain4j.community.version>1.13.0-beta23</langchain4j.community.version>
<langgraph4j.version>1.5.3</langgraph4j.version> <langgraph4j.version>1.5.3</langgraph4j.version>
<weaviate.version>1.19.6</weaviate.version> <weaviate.version>1.19.6</weaviate.version>
<dify.version>1.0.7</dify.version> <dify.version>1.2.6</dify.version>
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 --> <!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
<grpc.version>1.62.2</grpc.version> <grpc.version>1.62.2</grpc.version>
<!-- Apache Commons Compress - 用于POI处理ZIP格式 --> <!-- Apache Commons Compress - 用于POI处理ZIP格式 -->

View File

@@ -77,33 +77,10 @@ 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

@@ -1,44 +0,0 @@
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

@@ -1,72 +0,0 @@
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,30 +51,4 @@ 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

@@ -1,55 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -1,81 +0,0 @@
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

@@ -1,68 +0,0 @@
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,26 +78,6 @@ 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,30 +94,6 @@ 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

@@ -18,7 +18,8 @@ public enum ChatModeType {
PPIO("ppio", "ppio"), PPIO("ppio", "ppio"),
CUSTOM_API("custom_api", "自定义API"), CUSTOM_API("custom_api", "自定义API"),
MINIMAX("minimax", "MiniMax"), MINIMAX("minimax", "MiniMax"),
XIAOMI("xiaomi", "小米MiMo"); XIAOMI("xiaomi", "小米MiMo"),
DIFY("dify", "Dify平台");
private final String code; private final String code;
private final String description; private final String description;

View File

@@ -1,106 +0,0 @@
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

@@ -47,6 +47,7 @@ import org.ruoyi.common.sse.core.SseEmitterManager;
import org.ruoyi.common.sse.utils.SseMessageUtils; import org.ruoyi.common.sse.utils.SseMessageUtils;
import org.ruoyi.domain.bo.vector.QueryVectorBo; import org.ruoyi.domain.bo.vector.QueryVectorBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo; import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.factory.ChatServiceFactory; import org.ruoyi.factory.ChatServiceFactory;
import org.ruoyi.mcp.service.core.ToolProviderFactory; import org.ruoyi.mcp.service.core.ToolProviderFactory;
import org.ruoyi.observability.*; import org.ruoyi.observability.*;
@@ -54,7 +55,6 @@ 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;
@@ -90,8 +90,6 @@ 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;
@@ -100,6 +98,8 @@ public class ChatServiceFacade implements IChatService {
private final ToolProviderFactory toolProviderFactory; private final ToolProviderFactory toolProviderFactory;
private final org.ruoyi.service.chat.impl.provider.DifyWorkflowService difyWorkflowService;
/** /**
* 内存实例缓存,避免同一会话重复创建 * 内存实例缓存,避免同一会话重复创建
* Key: sessionId, Value: MessageWindowChatMemory实例 * Key: sessionId, Value: MessageWindowChatMemory实例
@@ -166,6 +166,14 @@ public class ChatServiceFacade implements IChatService {
* @return 如果需要提前返回则返回SseEmitter否则返回null * @return 如果需要提前返回则返回SseEmitter否则返回null
*/ */
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest) { private SseEmitter handleSpecialChatModes(ChatRequest chatRequest) {
// 处理 Dify 工作流对话
if (chatRequest.getEnableWorkFlow()
&& chatRequest.getChatModelVo() != null
&& ChatModeType.DIFY.getCode().equals(chatRequest.getChatModelVo().getProviderCode())) {
log.info("处理Dify工作流对话,会话: {}", chatRequest.getSessionId());
return difyWorkflowService.streaming(chatRequest.getChatModelVo(), chatRequest);
}
// 处理工作流对话 // 处理工作流对话
if (chatRequest.getEnableWorkFlow()) { if (chatRequest.getEnableWorkFlow()) {
log.info("处理工作流对话,会话: {}", chatRequest.getSessionId()); log.info("处理工作流对话,会话: {}", chatRequest.getSessionId());
@@ -433,8 +441,12 @@ public class ChatServiceFacade implements IChatService {
} }
} }
// Dify 自带 RAG 知识库检索,跳过本地向量库查询
boolean isDifyProvider = chatRequest.getChatModelVo() != null
&& ChatModeType.DIFY.getCode().equals(chatRequest.getChatModelVo().getProviderCode());
// 从向量库查询相关历史消息(知识库内容作为上下文) // 从向量库查询相关历史消息(知识库内容作为上下文)
if (chatRequest.getKnowledgeId() != null) { if (chatRequest.getKnowledgeId() != null && !isDifyProvider) {
// 查询知识库信息 // 查询知识库信息
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId())); KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
if (knowledgeInfoVo == null) { if (knowledgeInfoVo == null) {
@@ -455,8 +467,8 @@ public class ChatServiceFacade implements IChatService {
// 构建向量查询参数 // 构建向量查询参数
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel); QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
// 使用知识库检索服务(支持重排序 // 获取向量查询结果(知识库内容作为系统上下文,放在历史消息之后
List<String> nearestList = knowledgeRetrievalService.retrieveTexts(queryVectorBo); List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo);
for (String prompt : nearestList) { for (String prompt : nearestList) {
// 知识库内容作为系统上下文添加 // 知识库内容作为系统上下文添加
messages.add(new AiMessage(prompt)); messages.add(new AiMessage(prompt));
@@ -483,13 +495,6 @@ 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,43 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
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.service.chat.AbstractChatService;
import org.ruoyi.service.chat.impl.provider.model.DifyStreamingChatModel;
import org.springframework.stereotype.Service;
/**
* Dify 平台对话服务
* <p>
* 通过 dify-java-client 接入 Dify 的对话型应用 (Chat App) 和
* 工作流编排对话应用 (Chatflow App),支持流式 SSE 响应。
*
* @author better
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class DifyChatServiceImpl implements AbstractChatService {
private final DifyConversationService difyConversationService;
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return new DifyStreamingChatModel(chatModelVo, chatRequest, difyConversationService);
}
@Override
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
throw new UnsupportedOperationException("Dify 不支持同步 ChatModel请使用流式模式");
}
@Override
public String getProviderName() {
return ChatModeType.DIFY.getCode();
}
}

View File

@@ -0,0 +1,35 @@
package org.ruoyi.service.chat.impl.provider;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
/**
* Dify 会话映射管理
* <p>
* 维护 ruoyi sessionId 与 Dify conversation_id 的映射关系,
* 确保多轮对话上下文连续。
*
* @author better
*/
@Service
public class DifyConversationService {
private final ConcurrentHashMap<Long, String> conversationMap = new ConcurrentHashMap<>();
public String getConversationId(Long sessionId) {
return conversationMap.get(sessionId);
}
public void saveMapping(Long sessionId, String difyConversationId) {
if (sessionId != null && difyConversationId != null) {
conversationMap.put(sessionId, difyConversationId);
}
}
public void clearMapping(Long sessionId) {
if (sessionId != null) {
conversationMap.remove(sessionId);
}
}
}

View File

@@ -0,0 +1,137 @@
package org.ruoyi.service.chat.impl.provider;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.imfangs.dify.client.DifyClientFactory;
import io.github.imfangs.dify.client.DifyWorkflowClient;
import io.github.imfangs.dify.client.enums.ResponseMode;
import io.github.imfangs.dify.client.event.ErrorEvent;
import io.github.imfangs.dify.client.event.WorkflowFinishedEvent;
import io.github.imfangs.dify.client.event.WorkflowTextChunkEvent;
import io.github.imfangs.dify.client.callback.WorkflowStreamCallback;
import io.github.imfangs.dify.client.model.workflow.WorkflowRunRequest;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.sse.utils.SseMessageUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* Dify 工作流执行服务
* <p>
* 通过 DifyWorkflowClient 调用 Dify 平台上部署的工作流应用,
* 并将节点事件通过 SSE 实时推送给前端。
*
* @author better
*/
@Service
@Slf4j
public class DifyWorkflowService {
/**
* 流式执行 Dify 工作流
*
* @param chatModelVo 模型配置apiHost= Dify 地址, apiKey= Dify 密钥)
* @param chatRequest 聊天请求
* @return SSE emitter
*/
public SseEmitter streaming(ChatModelVo chatModelVo, ChatRequest chatRequest) {
Long userId = chatRequest.getUserId();
String tokenValue = chatRequest.getTokenValue();
SseEmitter emitter = chatRequest.getEmitter();
// 构建 Dify 工作流请求参数
Map<String, Object> inputs = convertInputs(chatRequest.getWorkFlowRunner());
WorkflowRunRequest request = WorkflowRunRequest.builder()
.inputs(inputs)
.responseMode(ResponseMode.STREAMING)
.user(String.valueOf(userId))
.build();
DifyWorkflowClient client = DifyClientFactory.createWorkflowClient(
normalizeBaseUrl(chatModelVo.getApiHost()),
chatModelVo.getApiKey());
// 异步执行,避免阻塞请求线程
CompletableFuture.runAsync(() -> {
try {
client.runWorkflowStream(request, new WorkflowStreamCallback() {
@Override
public void onWorkflowTextChunk(WorkflowTextChunkEvent event) {
String text = event.getData() != null ? event.getData().getText() : null;
if (text != null) {
SseMessageUtils.sendContent(userId, text);
}
}
@Override
public void onWorkflowFinished(WorkflowFinishedEvent event) {
// 将最终输出作为内容发送
if (event.getData() != null && event.getData().getOutputs() != null) {
Map<String, Object> outputs = event.getData().getOutputs();
for (Map.Entry<String, Object> entry : outputs.entrySet()) {
SseMessageUtils.sendContent(userId,
entry.getKey() + ": " + entry.getValue() + "\n");
}
}
SseMessageUtils.sendDone(userId);
SseMessageUtils.completeConnection(userId, tokenValue);
}
@Override
public void onError(ErrorEvent event) {
SseMessageUtils.sendError(userId, event.getMessage());
}
@Override
public void onException(Throwable throwable) {
log.error("Dify 工作流执行异常", throwable);
SseMessageUtils.sendError(userId, throwable.getMessage());
SseMessageUtils.completeConnection(userId, tokenValue);
}
});
} catch (Exception e) {
log.error("Dify 工作流执行失败", e);
SseMessageUtils.sendError(userId, e.getMessage());
SseMessageUtils.completeConnection(userId, tokenValue);
}
});
return emitter;
}
/**
* 将 WorkFlowRunner.inputs (List<ObjectNode>) 转换为 Dify 所需的 Map
*/
private Map<String, Object> convertInputs(WorkFlowRunner runner) {
Map<String, Object> result = new HashMap<>();
if (runner == null || runner.getInputs() == null) {
return result;
}
for (ObjectNode node : runner.getInputs()) {
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
result.put(field.getKey(), field.getValue().asText());
}
}
return result;
}
private String normalizeBaseUrl(String baseUrl) {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Dify API 地址(apiHost)不能为空");
}
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
}
}

View File

@@ -0,0 +1,172 @@
package org.ruoyi.service.chat.impl.provider.model;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import io.github.imfangs.dify.client.DifyChatClient;
import io.github.imfangs.dify.client.DifyClientFactory;
import io.github.imfangs.dify.client.enums.ResponseMode;
import io.github.imfangs.dify.client.event.ErrorEvent;
import io.github.imfangs.dify.client.event.MessageEndEvent;
import io.github.imfangs.dify.client.event.MessageEvent;
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.service.chat.impl.provider.DifyConversationService;
import java.util.List;
/**
* Dify 流式聊天模型适配器
* <p>
* 将 Dify 的回调式流式响应适配为 langchain4j 的 StreamingChatModel 接口,
* 使 ChatServiceFacade 可以像其他 provider 一样统一调用。
*
* @author better
*/
@Slf4j
public class DifyStreamingChatModel implements StreamingChatModel {
private final ChatModelVo chatModelVo;
private final ChatRequest chatRequest;
private final DifyConversationService conversationService;
public DifyStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest,
DifyConversationService conversationService) {
this.chatModelVo = chatModelVo;
this.chatRequest = chatRequest;
this.conversationService = conversationService;
}
@Override
public void chat(List<ChatMessage> messages, StreamingChatResponseHandler handler) {
// 1. 从 langchain4j 消息列表中提取最后一条用户消息作为 query
String query = extractUserQuery(messages);
// 2. 获取 Dify conversation_id多轮对话连续性
String conversationId = null;
if (chatRequest.getSessionId() != null) {
conversationId = conversationService.getConversationId(chatRequest.getSessionId());
}
// 3. 构建 Dify 请求
io.github.imfangs.dify.client.model.chat.ChatMessage difyMessage = io.github.imfangs.dify.client.model.chat.ChatMessage.builder()
.query(query)
.user(String.valueOf(chatRequest.getUserId()))
.responseMode(ResponseMode.STREAMING)
.conversationId(conversationId)
.autoGenerateName(true)
.build();
// 4. 创建 Dify 客户端并发送流式请求
try {
DifyChatClient client = DifyClientFactory.createChatClient(
normalizeBaseUrl(chatModelVo.getApiHost()),
chatModelVo.getApiKey());
client.sendChatMessageStream(difyMessage, new DifyChatStreamAdapter(handler));
} catch (Exception e) {
log.error("Dify 流式对话调用失败", e);
handler.onError(e);
}
}
@Override
public void chat(String userMessage, StreamingChatResponseHandler handler) {
io.github.imfangs.dify.client.model.chat.ChatMessage difyMessage = io.github.imfangs.dify.client.model.chat.ChatMessage.builder()
.query(userMessage)
.user(String.valueOf(chatRequest.getUserId()))
.responseMode(ResponseMode.STREAMING)
.conversationId(chatRequest.getSessionId() != null
? conversationService.getConversationId(chatRequest.getSessionId()) : null)
.autoGenerateName(true)
.build();
try {
DifyChatClient client = DifyClientFactory.createChatClient(
normalizeBaseUrl(chatModelVo.getApiHost()),
chatModelVo.getApiKey());
client.sendChatMessageStream(difyMessage, new DifyChatStreamAdapter(handler));
} catch (Exception e) {
log.error("Dify 流式对话调用失败", e);
handler.onError(e);
}
}
/**
* 从 langchain4j 消息列表中提取最后一条用户消息文本
*/
private String extractUserQuery(List<ChatMessage> messages) {
for (int i = messages.size() - 1; i >= 0; i--) {
ChatMessage msg = messages.get(i);
if (msg instanceof UserMessage) {
return ((UserMessage) msg).singleText();
}
}
return "";
}
private String normalizeBaseUrl(String baseUrl) {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Dify API 地址(apiHost)不能为空");
}
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
}
/**
* Dify 回调适配器
* 将 Dify ChatStreamCallback 事件转发给 langchain4j StreamingChatResponseHandler
*/
private class DifyChatStreamAdapter implements io.github.imfangs.dify.client.callback.ChatStreamCallback {
private final StreamingChatResponseHandler handler;
private final StringBuilder fullResponse = new StringBuilder();
DifyChatStreamAdapter(StreamingChatResponseHandler handler) {
this.handler = handler;
}
@Override
public void onMessage(MessageEvent event) {
String answer = event.getAnswer();
if (answer != null) {
fullResponse.append(answer);
handler.onPartialResponse(answer);
}
// 保存 Dify conversation_id 以维持多轮对话
if (event.getConversationId() != null && chatRequest.getSessionId() != null) {
conversationService.saveMapping(chatRequest.getSessionId(), event.getConversationId());
}
}
@Override
public void onMessageEnd(MessageEndEvent event) {
// 保存 conversation_id
if (event.getConversationId() != null && chatRequest.getSessionId() != null) {
conversationService.saveMapping(chatRequest.getSessionId(), event.getConversationId());
}
// 构建完整的 ChatResponse 交给上层处理
AiMessage aiMessage = new AiMessage(fullResponse.toString());
ChatResponse response = ChatResponse.builder()
.aiMessage(aiMessage)
.id(event.getMessageId())
.build();
handler.onCompleteResponse(response);
}
@Override
public void onError(ErrorEvent event) {
handler.onError(new RuntimeException(event.getMessage()));
}
@Override
public void onException(Throwable throwable) {
handler.onError(throwable);
}
}
}

View File

@@ -1,70 +0,0 @@
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

@@ -1,115 +0,0 @@
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

@@ -1,163 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,135 +0,0 @@
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

@@ -1,126 +0,0 @@
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

@@ -1,73 +0,0 @@
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();
}
}
}