Commit c31b66b6cc675c9edb26ea03af0dbecb27edf792

Authored by YunaiV
1 parent 45a9dfc4

feat:【ai 大模型】RAG 增加 rerank 模型

yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
@@ -18,6 +18,10 @@ import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; @@ -18,6 +18,10 @@ import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
18 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; 18 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
19 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; 19 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
20 import cn.iocoder.yudao.module.ai.service.model.AiModelService; 20 import cn.iocoder.yudao.module.ai.service.model.AiModelService;
  21 +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
  22 +import com.alibaba.cloud.ai.model.RerankModel;
  23 +import com.alibaba.cloud.ai.model.RerankRequest;
  24 +import com.alibaba.cloud.ai.model.RerankResponse;
21 import jakarta.annotation.Resource; 25 import jakarta.annotation.Resource;
22 import lombok.extern.slf4j.Slf4j; 26 import lombok.extern.slf4j.Slf4j;
23 import org.springframework.ai.document.Document; 27 import org.springframework.ai.document.Document;
@@ -27,6 +31,7 @@ import org.springframework.ai.transformer.splitter.TokenTextSplitter; @@ -27,6 +31,7 @@ import org.springframework.ai.transformer.splitter.TokenTextSplitter;
27 import org.springframework.ai.vectorstore.SearchRequest; 31 import org.springframework.ai.vectorstore.SearchRequest;
28 import org.springframework.ai.vectorstore.VectorStore; 32 import org.springframework.ai.vectorstore.VectorStore;
29 import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; 33 import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
  34 +import org.springframework.beans.factory.annotation.Autowired;
30 import org.springframework.context.annotation.Lazy; 35 import org.springframework.context.annotation.Lazy;
31 import org.springframework.stereotype.Service; 36 import org.springframework.stereotype.Service;
32 37
@@ -36,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU @@ -36,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
36 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 41 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
37 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG; 42 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG;
38 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; 43 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
  44 +import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
39 45
40 /** 46 /**
41 * AI 知识库分片 Service 实现类 47 * AI 知识库分片 Service 实现类
@@ -55,6 +61,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -55,6 +61,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
55 VECTOR_STORE_METADATA_DOCUMENT_ID, String.class, 61 VECTOR_STORE_METADATA_DOCUMENT_ID, String.class,
56 VECTOR_STORE_METADATA_SEGMENT_ID, String.class); 62 VECTOR_STORE_METADATA_SEGMENT_ID, String.class);
57 63
  64 + /**
  65 + * Rerank 在向量检索时,检索数量 * 该系数,目的是为了提升 Rerank 的效果
  66 + */
  67 + private static final Integer RERANK_RETRIEVAL_FACTOR = 4;
  68 +
58 @Resource 69 @Resource
59 private AiKnowledgeSegmentMapper segmentMapper; 70 private AiKnowledgeSegmentMapper segmentMapper;
60 71
@@ -69,6 +80,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -69,6 +80,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
69 @Resource 80 @Resource
70 private TokenCountEstimator tokenCountEstimator; 81 private TokenCountEstimator tokenCountEstimator;
71 82
  83 + @Autowired(required = false) // 由于 spring.ai.model.rerank 配置项,可以关闭 RerankModel 的功能,所以这里只能不强制注入
  84 + private RerankModel rerankModel;
  85 +
72 @Override 86 @Override
73 public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { 87 public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
74 return segmentMapper.selectPage(pageReqVO); 88 return segmentMapper.selectPage(pageReqVO);
@@ -211,28 +225,16 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -211,28 +225,16 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
211 // 1. 校验 225 // 1. 校验
212 AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId()); 226 AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId());
213 227
214 - // 2.1 向量检索  
215 - VectorStore vectorStore = getVectorStoreById(knowledge);  
216 - List<Document> documents = vectorStore.similaritySearch(SearchRequest.builder()  
217 - .query(reqBO.getContent())  
218 - .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK()))  
219 - .similarityThreshold(  
220 - ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold()))  
221 - .filterExpression(new FilterExpressionBuilder()  
222 - .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString())  
223 - .build())  
224 - .build());  
225 - if (CollUtil.isEmpty(documents)) {  
226 - return ListUtil.empty();  
227 - }  
228 - // 2.2 段落召回 228 + // 2. 检索
  229 + List<Document> documents = searchDocument(knowledge, reqBO);
  230 +
  231 + // 3.1 段落召回
229 List<AiKnowledgeSegmentDO> segments = segmentMapper 232 List<AiKnowledgeSegmentDO> segments = segmentMapper
230 .selectListByVectorIds(convertList(documents, Document::getId)); 233 .selectListByVectorIds(convertList(documents, Document::getId));
231 if (CollUtil.isEmpty(segments)) { 234 if (CollUtil.isEmpty(segments)) {
232 return ListUtil.empty(); 235 return ListUtil.empty();
233 } 236 }
234 -  
235 - // 3. 增加召回次数 237 + // 3.2 增加召回次数
236 segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); 238 segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId));
237 239
238 // 4. 构建结果 240 // 4. 构建结果
@@ -249,6 +251,42 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -249,6 +251,42 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
249 return result; 251 return result;
250 } 252 }
251 253
  254 + /**
  255 + * 基于 Embedding + Rerank Model,检索知识库中的文档
  256 + *
  257 + * @param knowledge 知识库
  258 + * @param reqBO 检索请求
  259 + * @return 文档列表
  260 + */
  261 + private List<Document> searchDocument(AiKnowledgeDO knowledge, AiKnowledgeSegmentSearchReqBO reqBO) {
  262 + VectorStore vectorStore = getVectorStoreById(knowledge);
  263 + Integer topK = ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK());
  264 + Double similarityThreshold = ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold());
  265 +
  266 + // 1. 向量检索
  267 + int searchTopK = rerankModel != null ? topK * RERANK_RETRIEVAL_FACTOR : topK;
  268 + double searchSimilarityThreshold = rerankModel != null ? SIMILARITY_THRESHOLD_ACCEPT_ALL : similarityThreshold;
  269 + SearchRequest.Builder searchRequestBuilder = SearchRequest.builder()
  270 + .query(reqBO.getContent())
  271 + .topK(searchTopK).similarityThreshold(searchSimilarityThreshold)
  272 + .filterExpression(new FilterExpressionBuilder()
  273 + .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()).build());
  274 + List<Document> documents = vectorStore.similaritySearch(searchRequestBuilder.build());
  275 + if (CollUtil.isEmpty(documents)) {
  276 + return documents;
  277 + }
  278 +
  279 + // 2. Rerank 重排序
  280 + if (rerankModel != null) {
  281 + RerankResponse rerankResponse = rerankModel.call(new RerankRequest(reqBO.getContent(), documents,
  282 + DashScopeRerankOptions.builder().withTopN(topK).build()));
  283 + documents = convertList(rerankResponse.getResults(),
  284 + documentWithScore -> documentWithScore.getScore() >= similarityThreshold
  285 + ? documentWithScore.getOutput() : null);
  286 + }
  287 + return documents;
  288 + }
  289 +
252 @Override 290 @Override
253 public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) { 291 public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) {
254 // 1. 读取 URL 内容 292 // 1. 读取 URL 内容
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; 1 package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
2 2
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
3 import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; 4 import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
4 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; 5 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
5 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; 6 import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
  7 +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel;
  8 +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
  9 +import com.alibaba.cloud.ai.model.RerankModel;
  10 +import com.alibaba.cloud.ai.model.RerankOptions;
  11 +import com.alibaba.cloud.ai.model.RerankRequest;
  12 +import com.alibaba.cloud.ai.model.RerankResponse;
6 import org.junit.jupiter.api.Disabled; 13 import org.junit.jupiter.api.Disabled;
7 import org.junit.jupiter.api.Test; 14 import org.junit.jupiter.api.Test;
8 import org.springframework.ai.chat.messages.Message; 15 import org.springframework.ai.chat.messages.Message;
@@ -10,11 +17,14 @@ import org.springframework.ai.chat.messages.SystemMessage; @@ -10,11 +17,14 @@ import org.springframework.ai.chat.messages.SystemMessage;
10 import org.springframework.ai.chat.messages.UserMessage; 17 import org.springframework.ai.chat.messages.UserMessage;
11 import org.springframework.ai.chat.model.ChatResponse; 18 import org.springframework.ai.chat.model.ChatResponse;
12 import org.springframework.ai.chat.prompt.Prompt; 19 import org.springframework.ai.chat.prompt.Prompt;
  20 +import org.springframework.ai.document.Document;
13 import reactor.core.publisher.Flux; 21 import reactor.core.publisher.Flux;
14 22
15 import java.util.ArrayList; 23 import java.util.ArrayList;
16 import java.util.List; 24 import java.util.List;
17 25
  26 +import static java.util.Arrays.asList;
  27 +
18 /** 28 /**
19 * {@link DashScopeChatModel} 集成测试类 29 * {@link DashScopeChatModel} 集成测试类
20 * 30 *
@@ -89,4 +99,31 @@ public class TongYiChatModelTests { @@ -89,4 +99,31 @@ public class TongYiChatModelTests {
89 }).then().block(); 99 }).then().block();
90 } 100 }
91 101
  102 + @Test
  103 + @Disabled
  104 + public void testRerank() {
  105 + // 准备环境
  106 + RerankModel rerankModel = new DashScopeRerankModel(
  107 + DashScopeApi.builder()
  108 + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
  109 + .build());
  110 + // 准备参数
  111 + String query = "spring";
  112 + Document document01 = new Document("abc");
  113 + Document document02 = new Document("sapring");
  114 + RerankOptions options = DashScopeRerankOptions.builder()
  115 + .withTopN(1)
  116 + .withModel("gte-rerank-v2")
  117 + .build();
  118 + RerankRequest rerankRequest = new RerankRequest(
  119 + query,
  120 + asList(document01, document02),
  121 + options);
  122 +
  123 + // 调用
  124 + RerankResponse call = rerankModel.call(rerankRequest);
  125 + // 打印结果
  126 + System.out.println(JsonUtils.toJsonPrettyString(call));
  127 + }
  128 +
92 } 129 }
yudao-server/src/main/resources/application.yaml
@@ -184,7 +184,7 @@ spring: @@ -184,7 +184,7 @@ spring:
184 stabilityai: 184 stabilityai:
185 api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx 185 api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
186 dashscope: # 通义千问 186 dashscope: # 通义千问
187 - api-key: sk-71800982914041848008480000000000 187 + api-key: sk-47aa124781be4bfb95244cc62f6xxxx
188 minimax: # Minimax:https://www.minimaxi.com/ 188 minimax: # Minimax:https://www.minimaxi.com/
189 api-key: xxxx 189 api-key: xxxx
190 moonshot: # 月之暗灭(KIMI) 190 moonshot: # 月之暗灭(KIMI)
@@ -194,6 +194,8 @@ spring: @@ -194,6 +194,8 @@ spring:
194 chat: 194 chat:
195 options: 195 options:
196 model: deepseek-chat 196 model: deepseek-chat
  197 + model:
  198 + rerank: dashscope # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
197 199
198 yudao: 200 yudao:
199 ai: 201 ai: