Home Projects Blog Resume Contact 中文
Back to list
2026年7月5日 1,619 words 4 min read

04-混合检索与 Query Rewrite:让正确证据进入候选池

前面几篇把证据链的前半段铺好了:请求有 trace,文档有身份,chunk 有结构。接下来进入真正的检索阶段。

#NexaRAG#Query Rewrite#混合检索#证据召回

04-混合检索和 Query Rewrite:让正确证据进入候选池

前面几篇把证据链的前半段铺好了:请求有 trace,文档有身份,chunk 有结构。接下来进入真正的检索阶段。

很多 RAG 项目一开始会把“检索”理解成“向量搜索”。实际做客服知识库时,单纯向量检索容易波动。产品型号、参数数字、版本号、售后条款、故障关键词,这些内容有时更适合关键词匹配;而用户表达含糊、上下文里有指代时,又需要 Query Rewrite 帮忙把问题补完整。

NexaRAG 这一阶段要解决的问题是:

让正确证据先进入候选池。 候选池质量稳定以后,后面的 rerank、上下文工程和答案校验才更有发挥空间。

Query Rewrite 解决指代和省略

查询改写的代码在 memory/_query/rewriter.pyagent/nodes/query_rewrite_node.py

QueryRewriter 的逻辑很克制。它会先判断当前问题是否需要改写:

if not chat_history or not chat_history.strip():
    return False
has_ref = any(w in query for w in ["它", "这个", "那个", "这款", "那款", "呢"])
return has_ref or len(query) <= 15

这说明 query rewrite 的目标放在检索场景里的指代消解省略补全

比如多轮对话里,用户先问“小米 15 Pro 的屏幕怎么样”,下一轮问“它电池多大”。如果直接拿“它电池多大”去检索,关键词和产品都不完整。Query Rewrite 要做的是把它改成“小米 15 Pro 电池容量是多少”这类独立查询。

QueryRewriteNode 还处理一种更安全的情况:当问题里有“它、这个、该产品”这类指代,同时缺少历史、图片识别结果和明确实体时,就先返回澄清问题。

if _needs_reference_clarification(state):
    return {
        "needs_clarification": True,
        "answer": "Which product are you asking about? ..."
    }

这一步和后面的拒答策略是一脉相承的:证据链含糊时,先澄清证据链。

混合检索的入口

混合检索核心在 rag/_retrieval/hybrid.pyHybridRetriever。它接收 query、top_k、filters 和 retrieval_mode,然后决定走 BM25、vector,还是 hybrid。

主流程大致是:

query
  -> BM25 search
  -> vector search
  -> RRF fusion
  -> rank_results
  -> apply_rerank
  -> trace

retrieval_mode 支持 autobm25vectorhybrid。这让调试很方便:同一个问题可以单独看 BM25 表现,也可以单独看 vector 表现,再看融合后的结果。

这也是 trace 的价值。它能展示每个检索通道各自召回了什么。

BM25 负责关键词和精确匹配

BM25 适合处理型号、数字、术语、故障关键词。比如:

小米 15 Pro
5000mAh
120W
保修
屏幕碎裂

这些信息未必需要复杂语义理解,反而需要精确匹配。向量检索可能觉得“续航”与“电池容量”相似,但如果用户问的是具体型号和具体参数,BM25 的关键词命中很有价值。

NexaRAG 在 HybridRetriever._search_payload() 里,除 vector 模式外都会走 BM25:

if mode != "vector":
    bm25_results = self.bm25.search(query, config.bm25_top_k, filters=filters)
    mark_channel(bm25_results, "bm25")

每个结果会被标记来源通道,后面进入 trace 时能看出它是 BM25 召回的,还是向量召回的。

向量检索负责语义召回

向量检索解决的是另一类问题:用户表达和文档表达存在差异。

比如文档写“续航表现”,用户问“电池续航怎么样”;文档写“售后服务政策”,用户问“坏了是否保修”。这种情况下,关键词可能对不上,向量检索更容易召回相关片段。

NexaRAG 的向量检索在 rag/_retrieval/vector.py,由 HybridRetriever 调用。值得注意的是,向量检索会受到环境开关影响:

vector_enabled = self._vector_enabled(mode)

如果向量库不可用,autohybrid 模式会 fallback 到 BM25。这对本地开发很重要:即使 Chroma 没开,检索链路也能继续跑,trace 里会记录 fallback_used 和 errors。

RRF 解决不同分数量纲问题

BM25 和向量检索的分数属于不同量纲,直接把 BM25 分数和 vector 分数相加会失真。NexaRAG 用 RRF 做融合,代码在 rag/_retrieval/fusion.py

rrf_score = 1.0 / (rrf_k + doc[rank_key])

RRF 基于排名给分。一个片段如果在 BM25 和 vector 里都排得靠前,融合后就更容易上来。

rrf_fusion() 还会记录结果来自哪些通道:

item["retrieval_source"] = "fused" if len(channels) > 1 else ...
item["source_channels"] = ",".join(channels)

这对诊断很有用。如果最终证据只来自 BM25,说明关键词通道贡献更大;如果来自 fused,说明多个通道都认为它重要。

metadata filter 提前收窄候选范围

检索除了文本相似度,还要考虑产品、版本、权限和生命周期。

NexaRAG 里 query 到 filter 的轻量推断在 rag/_filters/query.py

PRODUCT_ALIASES = [
    (("xiaomi", "小米", "15 pro", "xiaomi15"), "小米15 Pro"),
    ...
]

如果 query 里出现“小米 15 Pro”,就可以推断出 filters.product = "小米15 Pro"。后续检索时,BM25 和 vector 都可以带着 filters。

filter 的意义是提前收窄候选范围,避免证据池混入越界内容:

  • 用户问小米,要避免召回 iPhone 的参数。
  • 用户缺少权限,要避免召回内部文档。
  • 文档已经删除,要避免继续进入候选。
  • 旧版本要避免覆盖 active version。

这也是上一篇强调 metadata 的原因。有了 productversionaccess_scopeis_activeis_deleted 这些字段,检索阶段才有足够信息过滤。

trace 如何记录候选池

HybridRetriever._search_payload() 会同时返回 results 和 trace:

retrieval_mode
effective_mode
filters
fallback_used
vector_enabled
errors
retrieval.bm25
retrieval.vector
retrieval.fused
retrieval.reranked

这让候选池质量可以被观察。

如果用户问“小米 15 Pro 电池容量”,trace 可以展示几类检查结果:

  • BM25 对小米文档的召回情况。
  • vector 对同一批文档的召回情况。
  • fused 之后正确 chunk 的排序位置。
  • filters 对小米产品的限制情况。
  • vector 失败时的 BM25 fallback 情况。
  • rerank 前候选池本身的质量。

这一步的目标,是把正确证据送进后面的候选池。候选池越稳,rerank 和上下文工程越容易发挥作用。

这一步解决的内容和下一步

混合检索和 Query Rewrite 解决的是召回候选问题。

它让系统组合上下文指代消解、BM25 精确匹配、向量语义召回、RRF 融合和 metadata filter。它还通过 trace 把不同通道的结果展示出来,方便判断候选池质量。

下一步继续解决最终上下文问题。候选池里可能有 20 个片段,还要判断哪些片段更相关、哪些重复、哪些相邻 chunk 适合合并,以及证据编号怎么生成。这些要交给 rerank 和 ContextBuilder。

小结

NexaRAG 的混合检索是一条可调试的候选池生成链路

QueryRewriteNode 先让查询尽量独立完整;HybridRetriever 组合 BM25 和 vector;rrf_fusion() 用排名融合不同通道;filter 把产品、版本、权限和生命周期纳入检索;trace 则把各阶段候选暴露出来。

下一篇继续看候选池之后的部分:怎样用 rerank 和上下文工程,把候选片段整理成带编号、可引用、能交给模型的证据。

参考材料:

  • NexaRAG memory/_query/rewriter.py
  • NexaRAG agent/nodes/query_rewrite_node.py
  • NexaRAG rag/_retrieval/hybrid.py
  • NexaRAG rag/_retrieval/fusion.py
  • NexaRAG rag/_retrieval/filters.py
  • NexaRAG rag/_filters/query.py
  • NexaRAG tools/_retrieval/builder.py
  • 原 RAG 系列 06-检索增强复盘-QueryRewrite-HybridSearch和多路召回.md