Spring AI RAG 检索重排序(Reranking)模型集成实战
Spring AI RAG 检索重排序Reranking模型集成实战在构建企业级检索增强生成RAG系统的过程中工程师们常常会发现一个痛点向量数据库检索出来的片段虽然在数学语义上相似度很高但真正包含关键答案的段落却往往排在第 5 名甚至第 10 名。如果直接把召回的前 10 个片段全部塞进 Prompt不仅会显著增加 Token 消耗还会触发大语言模型“迷失在中间”Lost in the Middle的注意力缺陷导致最终生成的回答出现幻觉或抓不住重点。为了解决召回精度与生成质量之间的矛盾工业界普遍采用了“两阶段检索”架构第一阶段利用向量数据库进行大规模粗筛第二阶段引入专门的重排序Reranking模型进行精准打分。本文将详细探讨重排序模型的核心原理并基于 Spring AI 框架给出一套生产可用的集成实践方案。双塔向量检索与交叉编码器的本质差异为什么仅靠向量相似度做不到精准排序这要从模型底层的编码机制谈起。目前主流的向量检索Embedding基于双塔架构Bi-Encoder。在建立索引时文档片段Document被独立送入模型转化为一个固定维度的稠密向量在检索时用户的查询Query也被独立转化为向量系统通过计算两个向量的余弦相似度Cosine Similarity来评估相关性。双塔模型的优势在于可以预先对海量文本建立索引查询时计算开销极低能在毫秒级内从数百万条数据中筛选出候选集。但其劣势同样明显Query 和 Document 之间没有发生任何 Token 级别的注意力交叉Cross-Attention细微的逻辑转折、否定句、专有名词边界极易被压缩损失。重排序模型如 BAAI 开源的 bge-reranker 系列或 Cohere Rerank则普遍采用交叉编码器架构Cross-Encoder。它将 Query 与每一个候选 Document 拼接在一起作为一个整体序列输入模型。在 Transformer 的每一层中Query 中的每一个词都可以与 Document 中的每一个词进行全方位的 Self-Attention 计算。这种深度交互使得模型能够敏锐捕捉到句子间的逻辑矛盾、指代关系和实体匹配度打分精度远超向量检索。然而Cross-Encoder 的计算复杂度与序列长度呈平方级关系不可能用来扫描整个知识库。因此合理的落地架构一定是组合拳用 Bi-Encoder 快速召回 Top 50 的候选集再用 Cross-Encoder 对这 50 个片段进行精细化评分最后取出得分最高的 Top 5 送入大模型。Spring AI 中的重排序组件设计在 Spring AI 体系中文档检索的标准契约通常抽象在DocumentRetriever接口中。为了在检索流中无缝引入重排序能力我们可以设计一个通用的 HTTP 客户端对接独立部署的 Reranker 推理服务例如基于 Text Embeddings Inference 或 vLLM 部署的 REST API并构建一个两阶段检索管理器。首先封装通用的 Rerank 请求与响应结构以及底层的调用客户端public class BgeRerankClient { private final RestClient restClient; public BgeRerankClient(String baseUrl) { this.restClient RestClient.builder() .baseUrl(baseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } public record RerankRequest(String query, ListString texts, int topN) {} public record RerankItem(int index, double score, String text) {} public record RerankResponse(ListRerankItem results) {} public ListRerankItem rerank(String query, ListString documents, int topN) { return restClient.post() .uri(/rerank) .body(new RerankRequest(query, documents, topN)) .retrieve() .body(RerankResponse.class) .results(); } }接下来实现两阶段检索流水线。该组件首先调用 Spring AI 的VectorStore完成初筛提取文档文本后调用 Reranker 服务最后将重排序的分数附加到文档元数据中返回重新排序后的结果Component public class TwoStageRAGRetriever { private final VectorStore vectorStore; private final BgeRerankClient rerankClient; public TwoStageRAGRetriever(VectorStore vectorStore, BgeRerankClient rerankClient) { this.vectorStore vectorStore; this.rerankClient rerankClient; } public ListDocument retrieve(String query, int candidateK, int finalTopN) { // 第一阶段向量检索粗筛候选集 SearchRequest searchRequest SearchRequest.query(query).withTopK(candidateK); ListDocument candidates vectorStore.similaritySearch(searchRequest); if (candidates.isEmpty()) { return Collections.emptyList(); } ListString rawTexts candidates.stream() .map(Document::getContent) .toList(); // 第二阶段Cross-Encoder 精准重排 ListBgeRerankClient.RerankItem rerankItems rerankClient.rerank(query, rawTexts, finalTopN); // 组装最终文档并注入重排序得分 ListDocument finalDocuments new ArrayList(); for (BgeRerankClient.RerankItem item : rerankItems) { Document originDoc candidates.get(item.index()); MapString, Object newMetadata new HashMap(originDoc.getMetadata()); newMetadata.put(rerank_score, item.score()); Document scoredDoc new Document(originDoc.getId(), originDoc.getContent(), newMetadata); finalDocuments.add(scoredDoc); } return finalDocuments; } }生产环境落地中的关键工程考量在实际业务系统上线重排序模型时不仅要关注召回准确率还需要综合考虑系统延迟、资源开销以及异常熔断等工程细节候选集大小Candidate K的权衡取舍粗筛返回的候选数量决定了精排的计算量。如果候选集设为 100Reranker 的推理延迟可能超过 300ms严重影响用户的首字等待时间TTFT如果候选集设为 10则初筛漏掉的文档无法被召回。通常建议将候选集控制在 20 到 40 之间精排最终保留 3 到 5 条。长文本分块与截断风险大部分开源 Reranker如 bge-reranker-large的最大输入序列长度为 512 个 Token。若单个 Document 文本过长模型会强行截断尾部内容导致关键信息遗漏。因此在文档切分阶段应将单块文本长度限制在 300~400 Token 以内确保 Query Document 整体不会超过限制。置信度阈值过滤与拒答机制Reranker 输出的得分经过 Sigmoid 归一化为 0~1代表相关性的绝对置信度。当最高分的片段依然低于预设阈值例如 0.35时说明知识库中根本不存在匹配的知识。此时系统应当主动触发拒答或引导用户补充问题而不是强行将无关上下文塞给大模型这能极大程度降低幻觉率。超时控制与降级兜底由于重排序模型依赖 GPU 推理服务当 GPU 负载过高或网络波动时可能出现抖动。在 Spring Boot 中必须配置严格的 HTTP 请求超时如 400ms。一旦 Reranker 发生超时或异常系统应平滑降级为直接使用向量检索的粗排结果确保问答主链路的高可用。