前置文章:
Java 25 + Spring Boot 4.1.0 + Spring AI 2.0.1 调用 DeepSeek 模型
Java 25 + Spring Boot 4.1.0 + Spring AI 2.0.1 向量数据库 Milvus
前言
Reranker(重排序模型)是现代检索系统中的精排组件。它本身不负责从海量数据中“找”出候选,而是对第一阶段已召回的少量结果进行精细化重排,把真正能回答用户问题的内容排到最前面。
Spring AI 2.0.2 没有内置的、开箱即用的通用 Reranker 组件或专用设置项。
本文在前置文章的基础上进一步扩展,讲解如何在项目中使用 ONNX 加载本地 Reranker 模型 bge-reranker-v2-m3。
注意:请务必先完成前置文章 Java 25 + Spring Boot 4.1.0 + Spring AI 2.0.1 向量数据库 Milvus。
一、下载模型
bge-reranker-v2-m3-ONNX 模型下载
二、Maven 引入
<properties><!-- langchain4j-onnx-scoring 版本 --><langchain4j-onnx-scoring.version>1.17.2-beta27</langchain4j-onnx-scoring.version></properties><!-- 加载本地重排序需要用到 --><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-onnx-scoring</artifactId><version>${langchain4j-onnx-scoring.version}</version></dependency>三、添加 application.yml 配置
#重排序模型reranker:# 本地模型local:model:path:"E:\\ai-model\\onnx\\onnx-community\\bge-reranker-v2-m3-ONNX\\onnx\\model.onnx"tokenizer:path:"E:\\ai-model\\onnx\\onnx-community\\bge-reranker-v2-m3-ONNX\\tokenizer.json"# 重排序模型一组数据截断字数(根据模型设置,bge-reranker-v2-m3 上限:8192,官方建议:1024,超过 1024 token 后,排序效果可能会下降)reranker_doc_max_length:1024# 重排序结果分值是否归一is_sigmoid:true# 归一分值计算温度:可根据分数区间大小调整。1:就是标准 Sigmoid;> 1:输入被缩小,曲线变平缓,输出更靠近 0.5,区分度降低;< 1:输入被放大,曲线变陡峭,输出更靠近 0 或 1,区分度提高。sigmoid_temperature:1注意:该配置为业务自定义参数,并非 Spring AI 的配置参数。
四、创建 LocalRerankerModel 类
importdev.langchain4j.data.segment.TextSegment;importdev.langchain4j.model.scoring.onnx.OnnxScoringModel;importorg.apache.commons.collections4.CollectionUtils;importorg.springframework.ai.document.Document;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Configuration;importjava.util.*;/** * @description: 本地重排序模型 * @author: LIHAO * @createDate: 2026-11-03 10:02:06 */@ConfigurationpublicclassLocalRerankerModel{privatefinalOnnxScoringModelscoringModel;// 重排序结果分值是否归一privatebooleanisSigmoid;// Sigmoid 温度参数privateintsigmoidTemperature;publicLocalRerankerModel(@Value("${reranker.local.model.path}")StringmodelPath,@Value("${reranker.local.tokenizer.path}")StringtokenizerPath,@Value("${reranker.local.reranker_doc_max_length:1024}")intrerankerDocMaxLength,@Value("${reranker.is_sigmoid:true}")booleanisSigmoid,@Value("${reranker.sigmoid_temperature:1}")intsigmoidTemperature){this.isSigmoid=isSigmoid;this.sigmoidTemperature=sigmoidTemperature;// 创建 OnnxScoringModelthis.scoringModel=newOnnxScoringModel(modelPath,tokenizerPath,rerankerDocMaxLength);}/** * @description: 重排序 * @author: LIHAO * @createDate: 2026-11-03 10:59:32 * @param: [query 问题, documents 向量查询出来的内容] * @return: java.util.List<org.springframework.ai.document.Document> */publicList<Document>rerank(Stringquery,List<Document>documents){// 判断查询数据是否有值if(CollectionUtils.isEmpty(documents)){// 没有returndocuments;}// 1. 将 List<Document> 转换为 List<TextSegment>List<TextSegment>segments=documents.stream().map(doc->TextSegment.from(doc.getText())).toList();// 2. 批量重排序List<Double>scores=scoringModel.scoreAll(segments,query).content();// 判断数据是否一致if(scores.size()!=documents.size()){thrownewIllegalStateException("scoreAll 返回的分数数量("+scores.size()+")与文档数量("+documents.size()+")不一致");}// 3. 构造新的带分数 Document 列表(不修改入参)List<Document>scored=newArrayList<>(documents.size());for(inti=0;i<documents.size();i++){scored.add(documents.get(i).mutate().score(scores.get(i)).build());}// 4. 按原始分数降序排序List<Document>sorted=scored.stream().sorted(Comparator.comparing(Document::getScore,Comparator.nullsLast(Comparator.reverseOrder()))).toList();// 5. 根据配置决定是否做 Sigmoid 归一化if(isSigmoid){returnnormalizeScoresWithSigmoid(sorted,sigmoidTemperature);}returnsorted;}/** * 对重排序后的文档分数进行 Sigmoid 归一化,映射到 (0,1) 区间。 * * @param documentList 待归一化的文档列表(需包含原始 score) * @param temperature Sigmoid 温度参数,可根据分数区间调整。 * 用于缓解饱和:如果原始分数范围很大(比如 -20 到 +20), * 标准 Sigmoid 会让很多分数挤在 0 或 1 附近,丢失区分度, * 除以较大的 T 可以拉开它们。 * temperature > 1:输入被缩小,曲线变平缓,输出更靠近 0.5,区分度降低。 * temperature < 1:输入被放大,曲线变陡峭,输出更靠近 0 或 1,区分度提高。 * temperature = 1:就是标准 Sigmoid。 * @return 归一化后的文档列表(每个文档的 score 被替换为归一化值,原始分数存入 metadata['reranker_score']) */publicList<Document>normalizeScoresWithSigmoid(List<Document>documentList,doubletemperature){if(CollectionUtils.isEmpty(documentList)){returnList.of();// 或 Collections.emptyList()}if(temperature<=0){thrownewIllegalArgumentException("temperature 必须大于 0,当前值: "+temperature);}List<Document>normalizedDocs=newArrayList<>(documentList.size());for(Documentdoc:documentList){DoublerawScore=doc.getScore();if(rawScore==null){// 如果分数为 null,则跳过continue;}doublenormalized=1.0/(1.0+Math.exp(-rawScore/temperature));// 构建新 Document,保留原始分数到 metadataMap<String,Object>newMetadata=newHashMap<>(doc.getMetadata());newMetadata.put("reranker_score",rawScore);DocumentnormalizedDoc=doc.mutate().score(normalized).metadata(newMetadata).build();normalizedDocs.add(normalizedDoc);}returnnormalizedDocs;}}五、测试用例
importcom.lh.model.reranker.LocalRerankerModel;importcom.lh.vectorDatabases.milvus.dto.VectorQueryRequest;importcom.lh.vectorDatabases.milvus.dto.VectorUpsertRequest;importcom.lh.vectorDatabases.milvus.service.MilvusVectorStoreService;importlombok.extern.slf4j.Slf4j;importorg.apache.commons.collections4.CollectionUtils;importorg.junit.jupiter.api.Test;importorg.springframework.ai.document.Document;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importjava.util.List;importjava.util.Map;@Slf4j@SpringBootTestpublicclassOriginApplicationTest{@AutowiredprivateMilvusVectorStoreServicemilvusVectorStoreService;@AutowiredprivateLocalRerankerModelrerankerModel;@TestpublicvoidmilvusVectorStoreTest(){List<Document>documents=List.of(newDocument("今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!今天天气很好!",Map.of("meta1","meta1")),newDocument("世界很大,救赎就在拐角处"),newDocument("你面对过去向前走,然后转身面向未来。",Map.of("meta2","meta2")));//新增数据milvusVectorStoreService.upsert(VectorUpsertRequest.builder().documentList(documents).build());Stringquery="今天";//查询数据List<Document>results=milvusVectorStoreService.searchByVector(VectorQueryRequest.builder().text(query).topK(20).build());System.out.println(results);//重排序List<Document>rerankerResults=rerankerModel.rerank(query,results);System.out.println(rerankerResults);if(CollectionUtils.isNotEmpty(rerankerResults)){List<String>idList=rerankerResults.stream().map(Document::getId).toList();//根据id删除数据milvusVectorStoreService.deleteById(VectorQueryRequest.builder().idList(idList).build());}}}六、总结
本文在 Spring AI 2.0.2 + Milvus 的向量检索基础上,借助langchain4j-onnx-scoring引入本地 Reranker 模型bge-reranker-v2-m3,补齐了检索链路中「精排」这一环。整体实现可以归纳为三点:
- 依赖与配置解耦:重排序相关参数(模型路径、分词器路径、截断长度、是否归一、温度)全部放在
reranker自定义配置下,与 Spring AI 自身的配置互不干扰,便于按环境调整。 - 模型封装成 Bean:
LocalRerankerModel在构造时一次性加载 ONNX 模型与分词器,对外只暴露rerank(query, documents)一个方法,内部完成「Document → TextSegment 转换 → 批量打分 → 按分数降序 → 可选 Sigmoid 归一化」的完整流程,调用方无需关心底层细节。 - 分数可控可解释:原始分数会保留在
metadata['reranker_score']中,归一化后的分数写入score,既方便业务侧按阈值过滤,也便于排查排序效果。
几点实践建议:
- 截断长度要权衡:
reranker_doc_max_length默认取 1024,超过该长度后排序效果可能下降;若文档普遍较长,建议先做分块再送入重排序。 - 温度参数按需调整:原始分数区间较大时,适当调大
sigmoid_temperature可缓解 Sigmoid 饱和导致的区分度丢失;反之调小可让结果更「两极分化」。 - 注意性能开销:重排序模型内部是逐条打分,耗时与候选数量线性相关,建议只对向量召回后的 TopK(如 20~50 条)做精排,而不是对全量数据重排。
- 模型文件本地化:ONNX 模型与分词器文件建议随镜像或部署包一起分发,避免运行时依赖外部网络。
至此,从向量召回、重排序到结果输出的完整链路已经跑通。后续可以在此基础上继续扩展,例如结合业务阈值做低分过滤、将重排序结果接入大模型做最终回答,或替换为其他 Reranker 模型进行效果对比。