新增千问3重排序模型,并附带新增sql文件

This commit is contained in:
yangzhen
2026-04-20 16:07:02 +08:00
parent 80ca76ea37
commit e1b8a5f011
9 changed files with 502 additions and 109 deletions

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

@@ -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,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

@@ -1,5 +1,6 @@
package org.ruoyi.domain.dto.response; package org.ruoyi.domain.dto.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.ruoyi.domain.bo.rerank.RerankResult; import org.ruoyi.domain.bo.rerank.RerankResult;
@@ -12,6 +13,7 @@ import java.util.stream.Collectors;
* @author yang * @author yang
* @date 2026-04-19 * @date 2026-04-19
*/ */
@JsonIgnoreProperties(ignoreUnknown = true)
public record ZhipuRerankResponse( public record ZhipuRerankResponse(
String model, String model,
String object, String object,
@@ -31,6 +33,7 @@ public record ZhipuRerankResponse(
/** /**
* Token使用信息 * Token使用信息
*/ */
@JsonIgnoreProperties(ignoreUnknown = true)
public record UsageInfo( public record UsageInfo(
@JsonProperty("total_tokens") @JsonProperty("total_tokens")
Integer totalTokens, Integer totalTokens,

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

@@ -1,107 +0,0 @@
package org.ruoyi.service.rerank.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.http.client.*;
import lombok.extern.slf4j.Slf4j;
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.service.rerank.RerankModelService;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Jina AI 重排序模型实现
* 参考设计模式OpenAiEmbeddingProvider
* 使用 Jina 官方重排序API
*
* @author yang
* @date 2026-04-19
*/
@Slf4j
@Component("jina")
public class JinaRerankModelService implements RerankModelService {
protected ChatModelVo chatModelVo;
protected HttpClient httpClient;
protected final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void configure(ChatModelVo config) {
this.chatModelVo = config;
HttpClientBuilder httpClientBuilder = HttpClientBuilderLoader.loadHttpClientBuilder();
this.httpClient = httpClientBuilder.build();
}
@Override
public RerankResult rerank(RerankRequest rerankRequest) {
long startTime = System.currentTimeMillis();
try {
// 构建Jina重排序请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", chatModelVo.getModelName());
requestBody.put("query", rerankRequest.getQuery());
requestBody.put("documents", rerankRequest.getDocuments());
requestBody.put("top_n", rerankRequest.getTopN() != null ?
rerankRequest.getTopN() : rerankRequest.getDocuments().size());
requestBody.put("return_documents", rerankRequest.getReturnDocuments());
// 构建HTTP请求
HttpRequest httpRequest = HttpRequest.builder()
.url(chatModelVo.getApiHost())
.method(HttpMethod.POST)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer " + chatModelVo.getApiKey())
.body(objectMapper.writeValueAsString(requestBody))
.build();
// 发送请求
SuccessfulHttpResponse httpResponse = httpClient.execute(httpRequest);
// 解析响应
@SuppressWarnings("unchecked")
Map<String, Object> response = objectMapper.readValue(httpResponse.body(), Map.class);
return parseResponse(response, rerankRequest.getDocuments().size(),
System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("Jina重排序失败: {}", e.getMessage(), e);
throw new RuntimeException("重排序服务调用失败: " + e.getMessage(), e);
}
}
/**
* 解析Jina API响应
*/
@SuppressWarnings("unchecked")
private RerankResult parseResponse(Map<String, Object> response, int totalDocs, long durationMs) {
List<Map<String, Object>> results = (List<Map<String, Object>>) response.get("results");
if (results == null || results.isEmpty()) {
return RerankResult.empty();
}
List<RerankResult.RerankDocument> documents = new ArrayList<>();
for (Map<String, Object> result : results) {
Integer index = (Integer) result.get("index");
Double score = ((Number) result.get("relevance_score")).doubleValue();
String document = (String) result.get("document");
documents.add(RerankResult.RerankDocument.builder()
.index(index)
.relevanceScore(score)
.document(document)
.build());
}
return RerankResult.builder()
.documents(documents)
.totalDocuments(totalDocs)
.durationMs(durationMs)
.build();
}
}

View File

@@ -90,14 +90,15 @@ public class ZhiPuRerankModelService implements RerankModelService {
// 生成智谱认证Token // 生成智谱认证Token
String token = generateToken(chatModelVo.getApiKey()); String token = generateToken(chatModelVo.getApiKey());
String url = chatModelVo.getApiHost() + "/" + chatModelVo.getModelName(); // 智谱重排序固定端点路径
String url = chatModelVo.getApiHost() + "/api/paas/v4/rerank";
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.addHeader("Authorization", token) .addHeader("Authorization", token)
.post(body) .post(body)
.build(); .build();
try (okhttp3.Response response = okHttpClient.newCall(httpRequest).execute()) { try (Response response = okHttpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
String err = response.body() != null ? response.body().string() : "无错误信息"; String err = response.body() != null ? response.body().string() : "无错误信息";
throw new IllegalArgumentException("智谱API调用失败: " + response.code() + " - " + err); throw new IllegalArgumentException("智谱API调用失败: " + response.code() + " - " + err);

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();
}
}
}