老魏 · 折腾铺
民宿站监控台双色球点哪抠哪老照片修复Murdoku科学探险家
折腾铺/Java RAG 连载
Java RAG 连载2026-09-11老魏

Java RAG 合集⑧:先广撒网,再精挑细选,Rerank 与混合检索

向量检索会召回语义相近但业务无关的内容。本篇讲两阶段检索:粗召回 Top20 加 Rerank 精排取 5,BM25 加向量双通路 RRF 融合,以及这些提升要付多少延迟和钱的账。
Java RAG 合集⑧:先广撒网,再精挑细选,Rerank 与混合检索

Java RAG 合集⑧:先广撒网,再精挑细选——Rerank 与混合检索

上一篇把 RAG 拆成了四段积木,但地基还是那一条:检索质量。检索不行,后面三段全是白搭——垃圾进,垃圾出(第④篇的原话在这应验了)。

这一章讲检索质量的两次升级:两阶段精排和混合检索。这两招做完,RAG 答案的「准头」会有肉眼可见的跃迁——代价是延迟和账单,所以最后一节我们把账算清楚。

读完你应该能: - 说清向量检索为什么会召回「语义相近但业务无关」的内容 - 用 DocumentPostProcessor 插槽接 Rerank,实现粗召回 Top-20 → 精排 Top-5 - 知道混合检索为什么该上,以及 Spring AI 为什么没内置、怎么自己搭 - 给这些提升算延迟和成本的账


一、向量检索的先天短板

两阶段检索:先广撒网,再精挑细选

先看病根。向量检索用的是双塔模型:问题和文档分别编码成向量,比余弦距离。它的好处是文档可以离线编码、检索时只算一次问题向量——快,能从百万级文档里毫秒级捞人。

代价是:问题和文档在编码时从没见过面。「试用期年假怎么算」和《员工手册》总则里的「年假相关制度说明」语义确实相近,双塔模型分不清哪个才是真答案——它们只是「在向量空间里挨得近」。

所以你会经常遇到这种情况:召回的文档都沾点边,但真正能回答问题的那条,排在第 3 甚至第 5。前面全是高分噪声,全塞给模型,注意力被稀释,答案质量打折。

二、两阶段:粗召回 + 精排

解法是分工:召回阶段不追求排得准,只求别漏(Top-20);精排阶段用一个更重的模型,把 20 条排成 5 条。

精排用的交叉编码器和双塔完全不同:问题和文档拼在一起送进模型,逐对细算相关度。准得多,但也慢得多——所以只能伺候 20 条,伺候不起全库。

Spring AI 把这个环节做成了第⑦篇介绍过的 DocumentPostProcessor 插槽。Rerank 本身框架没内置(各家 API 形态不一),自己接一个很直接:

public class RerankPostProcessor implements DocumentPostProcessor {

    private final RerankClient rerankClient;   // 封装你的 Rerank API 调用

    @Override
    public List<Document> process(Query query, List<Document> documents) {
        if (documents.size() <= 5) {
            return documents;   // 本来就不多,不值得精排
        }
        // 1. 问题和 20 条文档逐对送进 Rerank API 打分
        List<ScoredDocument> scored =
            rerankClient.score(query.text(), documents);
        // 2. 按相关度重排,留前 5
        return scored.stream()
            .sorted(Comparator.comparing(ScoredDocument::score).reversed())
            .limit(5)
            .map(ScoredDocument::document)
            .toList();
    }
}

Cohere、Jina、智谱、硅基流动都有现成的 Rerank API,选一个延迟低的(国内服务通常 50~150ms 能跑完 20 条)。装配时挂上就行:

Advisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
    .documentRetriever(VectorStoreDocumentRetriever.builder()
        .vectorStore(vectorStore)
        .topK(20)                  // 粗召回放宽
        .similarityThreshold(0.3)  // 阈值相应调低,别在粗排就把人拒了
        .build())
    .documentPostProcessors(new RerankPostProcessor(rerankClient))
    .build();

注意粗召回的阈值要跟着调:Top-20 的使命是「别漏」,阈值设 0.6 会把正确答案挡在门外,精排再准也救不回来。

三、Rerank 前后,差在哪

Rerank 前后:同一个问题,两种命运

看这组对比:同一个问题「试用期年假怎么算」,粗召回把大而空的《员工手册》总则排在第一,真答案《年假实施细则》5.2 节排在第三。全塞给模型,模型大概率照着总则泛泛而谈。

Rerank 之后:真答案登顶(相关度 0.94),两条无关文档直接出局。进提示词的不仅更准,还更少——token 账省下来了。

还有个容易踩的坑:向量分数和 Rerank 相关度是两套分数,不可直接比较。余弦 0.83 不代表比 Rerank 的 0.35 更相关——前者是几何距离,后者是逐对细算的相关性概率。别用一套阈值去卡另一套的输出。

四、混合检索:语义和关键词两条腿

混合检索:语义和关键词两条腿走路

Rerank 解决「排得准」,但「漏」的问题还得靠召回端——特别是这一类查询:

ERR_CODE_10086 报什么错?

错误码、型号、人名、专有名词——这些 token 的向量没有语义可循,向量检索经常捞不准;但 BM25 这类关键词检索对词面命中一击必杀。反过来,口语化的「电脑开不了机咋整」,关键词检索又干不过向量。两条腿各有一条盲区,且互补。

先泼冷水:Spring AI 没有内置混合检索。网上有些文章会给你一个 HybridRetriever.builder().weight(0.7, 0.3) 的写法——查遍官方文档也没有这个类,照抄必报错。正确姿势是自己实现 DocumentRetriever 接口,内部跑两条通路再融合:

public class HybridRetriever implements DocumentRetriever {

    private final VectorStore vectorStore;
    private final Bm25Searcher bm25;   // ES / OpenSearch / PG 全文检索

    @Override
    public List<Document> retrieve(Query query) {
        // 两路各自召回 Top-20
        List<Document> dense = vectorStore.similaritySearch(
            SearchRequest.builder().query(query.text()).topK(20).build());
        List<Document> sparse = bm25.search(query.text(), 20);

        return rrfFuse(dense, sparse);   // RRF 融合
    }

    /** Reciprocal Rank Fusion:按名次倒数融合,k 取 60 是惯例 */
    private List<Document> rrfFuse(List<Document> a, List<Document> b) {
        Map<Document, Double> score = new HashMap<>();
        for (List<Document> list : List.of(a, b)) {
            for (int i = 0; i < list.size(); i++) {
                score.merge(list.get(i), 1.0 / (60 + i + 1), Double::sum);
            }
        }
        return score.entrySet().stream()
            .sorted(Map.Entry.<Document, Double>comparingByValue().reversed())
            .limit(20)
            .map(Map.Entry::getKey)
            .toList();
    }
}

RRF(倒数排名融合)的好处是不需要把两路分数归一化——只看名次,谁在自家通路排得靠前谁得分高,简单稳健,是业界默认做法。

混合检索之后接 Rerank 精排,效果叠加:召回端「该来的都来了」,精排端「该排前的排前面」。公开评测里,混合检索相对纯向量召回常见两位数的指标提升,数据形态越偏「术语型」提升越大。

五、延迟与成本的账

好处说完,把账摆开:

结论:问答延迟预算够(比如客服场景 3 秒内),闭眼上;对延迟敏感的搜索联想类场景,先只上混合检索(便宜),Rerank 酌情。 Rerank 别配太大的粗召回池——20 条是延迟和效果的平衡点,上到 50 条延迟翻倍,收益递减。

六、坑位清单


七、完整装配

两阶段 + 混合检索全开:

Advisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
    // Retrieval:换成自研混合检索器
    .documentRetriever(new HybridRetriever(vectorStore, bm25Searcher))
    // Post-Retrieval:Rerank 精排 Top-20 → Top-5
    .documentPostProcessors(new RerankPostProcessor(rerankClient))
    // Generation:空结果兜底(第⑦篇)
    .queryAugmenter(ContextualQueryAugmenter.builder()
        .allowEmptyContext(false)
        .build())
    .build();

写在最后

这一章的优化都是「拍脑袋调不出对错」的——Top-20 还是 Top-30?阈值 0.3 还是 0.4?上不上 Rerank?每一刀都得靠数据说话。

下一篇《别再凭感觉调参:RAGAS 四维指标与 Java 实现》——给 RAG 装上仪表盘:Faithfulness、Answer Relevancy、Context Precision、Context Recall 四个指标,用 ChatClient 手写 LLM-as-Judge 评判器,让每次调参都有数可依。

点个「在看」——广撒网和精挑细选,从来都是两门手艺。


作者:老魏 · 一个把公众号和代码当副业的散户投资者 系列:Java RAG 从零到上线 · 第 8/10 篇 下期:RAGAS 四维指标与 Java 实现