04-混合检索与 Query Rewrite:让正确证据进入候选池
前面几篇把证据链的前半段铺好了:请求有 trace,文档有身份,chunk 有结构。接下来进入真正的检索阶段。
04-混合检索和 Query Rewrite:让正确证据进入候选池
前面几篇把证据链的前半段铺好了:请求有 trace,文档有身份,chunk 有结构。接下来进入真正的检索阶段。
很多 RAG 项目一开始会把“检索”理解成“向量搜索”。实际做客服知识库时,单纯向量检索容易波动。产品型号、参数数字、版本号、售后条款、故障关键词,这些内容有时更适合关键词匹配;而用户表达含糊、上下文里有指代时,又需要 Query Rewrite 帮忙把问题补完整。
NexaRAG 这一阶段要解决的问题是:
让正确证据先进入候选池。 候选池质量稳定以后,后面的 rerank、上下文工程和答案校验才更有发挥空间。
Query Rewrite 解决指代和省略
查询改写的代码在 memory/_query/rewriter.py 和 agent/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.py 的 HybridRetriever。它接收 query、top_k、filters 和 retrieval_mode,然后决定走 BM25、vector,还是 hybrid。
主流程大致是:
query
-> BM25 search
-> vector search
-> RRF fusion
-> rank_results
-> apply_rerank
-> trace
retrieval_mode 支持 auto、bm25、vector、hybrid。这让调试很方便:同一个问题可以单独看 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)
如果向量库不可用,auto 或 hybrid 模式会 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 的原因。有了 product、version、access_scope、is_active、is_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