欢迎光临
我们一直在努力

RAG 为什么要混合检索:从关键词、向量到重排与引用溯源

本文定位:RAG 检索工程 / 搜索排序 / 企业知识库

示例环境:PostgreSQL + pgvector、Java 21、BM25 思路、Rerank 服务。指标阈值需要根据业务风险重新设定。

摘要

只使用向量相似度的 RAG,面对错误码、产品型号、合同编号和精确字段时往往不如关键词检索;只使用关键词,又无法很好理解同义表达和自然语言问题。混合检索的价值不是把两套结果简单拼起来,而是利用不同检索器的优势,再用统一的排序和证据治理把结果变成可解释的上下文。

本文从一个“设备故障知识库”出发,比较 BM25、向量检索、混合召回和 Rerank 的职责,给出候选集融合、引用元数据、Java 数据结构和评测方法,并说明为什么“召回更多”不一定代表回答更好。

一、不同检索器擅长什么

检索方式擅长不擅长
关键词/BM25 错误码、编号、专有名词、精确短语 同义改写、口语表达
向量检索 语义相近、自然语言描述 精确数字、罕见型号、否定条件
混合召回 兼顾精确与语义 参数更多,调试复杂
Rerank 在候选集中判断问答相关性 无法挽回第一阶段完全没召回的证据

例如用户问“告警 E102 连续出现三次后,先做什么”,关键词检索容易直接命中 E102;用户问“采集链路不稳定时怎样处理”,向量检索更容易找到“检查采集链路”的段落。高质量系统通常让两者并行产生候选。

二、检索链路

#mermaid-svg-TWKNRk3iF9ZSz6wk{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TWKNRk3iF9ZSz6wk .error-icon{fill:#552222;}#mermaid-svg-TWKNRk3iF9ZSz6wk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TWKNRk3iF9ZSz6wk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .marker.cross{stroke:#333333;}#mermaid-svg-TWKNRk3iF9ZSz6wk svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TWKNRk3iF9ZSz6wk p{margin:0;}#mermaid-svg-TWKNRk3iF9ZSz6wk .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .cluster-label text{fill:#333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .cluster-label span{color:#333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .cluster-label span p{background-color:transparent;}#mermaid-svg-TWKNRk3iF9ZSz6wk .label text,#mermaid-svg-TWKNRk3iF9ZSz6wk span{fill:#333;color:#333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .node rect,#mermaid-svg-TWKNRk3iF9ZSz6wk .node circle,#mermaid-svg-TWKNRk3iF9ZSz6wk .node ellipse,#mermaid-svg-TWKNRk3iF9ZSz6wk .node polygon,#mermaid-svg-TWKNRk3iF9ZSz6wk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TWKNRk3iF9ZSz6wk .rough-node .label text,#mermaid-svg-TWKNRk3iF9ZSz6wk .node .label text,#mermaid-svg-TWKNRk3iF9ZSz6wk .image-shape .label,#mermaid-svg-TWKNRk3iF9ZSz6wk .icon-shape .label{text-anchor:middle;}#mermaid-svg-TWKNRk3iF9ZSz6wk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TWKNRk3iF9ZSz6wk .rough-node .label,#mermaid-svg-TWKNRk3iF9ZSz6wk .node .label,#mermaid-svg-TWKNRk3iF9ZSz6wk .image-shape .label,#mermaid-svg-TWKNRk3iF9ZSz6wk .icon-shape .label{text-align:center;}#mermaid-svg-TWKNRk3iF9ZSz6wk .node.clickable{cursor:pointer;}#mermaid-svg-TWKNRk3iF9ZSz6wk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .arrowheadPath{fill:#333333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TWKNRk3iF9ZSz6wk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TWKNRk3iF9ZSz6wk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TWKNRk3iF9ZSz6wk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TWKNRk3iF9ZSz6wk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TWKNRk3iF9ZSz6wk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TWKNRk3iF9ZSz6wk .cluster text{fill:#333;}#mermaid-svg-TWKNRk3iF9ZSz6wk .cluster span{color:#333;}#mermaid-svg-TWKNRk3iF9ZSz6wk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TWKNRk3iF9ZSz6wk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TWKNRk3iF9ZSz6wk rect.text{fill:none;stroke-width:0;}#mermaid-svg-TWKNRk3iF9ZSz6wk .icon-shape,#mermaid-svg-TWKNRk3iF9ZSz6wk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TWKNRk3iF9ZSz6wk .icon-shape p,#mermaid-svg-TWKNRk3iF9ZSz6wk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TWKNRk3iF9ZSz6wk .icon-shape .label rect,#mermaid-svg-TWKNRk3iF9ZSz6wk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TWKNRk3iF9ZSz6wk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TWKNRk3iF9ZSz6wk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TWKNRk3iF9ZSz6wk :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

用户问题

规范化/提取实体

关键词召回

向量召回

候选去重

Rerank

权限/版本/时间过滤

上下文压缩与引用编号

模型生成

注意权限和版本过滤既可以在第一阶段做,也应该在最终上下文组装前再做一次。多层过滤是为了防止缓存、重排服务或异步流程引入过期、越权文档。

三、关键词和向量结果如何融合

最简单的方式是给两类结果设置权重:

score = alpha * normalized_vector_score
+ (1 – alpha) * normalized_keyword_score

但两个检索器的分数分布往往不同,不能直接相加。更稳妥的做法是 Reciprocal Rank Fusion:

RRF(d) = sum(1 / (k + rank_i(d)))

其中 rank_i(d) 是文档在第 i 个检索器中的排名,k 是平滑常数。RRF 不依赖原始分数尺度,适合快速建立混合检索基线。

public List<Candidate> rrfMerge(List<Candidate> keyword,
List<Candidate> vector,
int k, int limit) {
Map<Long, Double> score = new HashMap<>();
for (int i = 0; i < keyword.size(); i++) {
score.merge(keyword.get(i).chunkId(), 1.0 / (k + i + 1), Double::sum);
}
for (int i = 0; i < vector.size(); i++) {
score.merge(vector.get(i).chunkId(), 1.0 / (k + i + 1), Double::sum);
}
return score.entrySet().stream()
.sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
.limit(limit)
.map(e -> Candidate.withFusionScore(e.getKey(), e.getValue()))
.toList();
}

融合前要按 Chunk ID 去重,并保留每个候选来自哪些检索器、原始排名和分数。否则出了问题只能看到最终排序,无法判断是关键词错了、向量错了还是 Rerank 错了。

四、Rerank 应该放在哪里

Rerank 适合对 20~100 条候选做精细相关性判断,不适合直接扫全库。它可以理解问题和候选文本的交互关系,通常比单独的向量相似度更准确,但会增加网络调用、延迟和成本。

public List<Candidate> retrieve(QueryContext query) {
List<Candidate> candidates = merge(
keyword.search(query.text(), 30),
vector.search(query.embedding(), 30));
List<Candidate> permitted = permissionFilter.filter(candidates, query.auth());
List<RerankItem> items = permitted.stream()
.map(c -> new RerankItem(c.chunkId(), c.content()))
.toList();
return reranker.rank(query.text(), items, 8);
}

Rerank 不能代替权限过滤。先发送越权文本给外部 Rerank 服务,再在返回结果里过滤,已经失去安全意义。更安全的顺序是先过滤租户和权限,再重排。

五、查询规范化要克制

可以让模型把问题拆成错误码、设备型号、时间范围和意图,但查询改写不能改变原始条件。建议同时保留原问题和规范化结果:

{
"original": "E102 连续三次后先做什么?",
"keywords": ["E102", "连续三次"],
"semantic_query": "错误码E102重复出现后的首要处理步骤",
"must_keep": ["E102", "三次"]
}

如果改写模型把否定条件删掉,检索结果可能完全变质。例如“不是电源问题时如何排查”不能改成“电源问题如何排查”。对高风险检索,重要实体应通过规则或实体识别校验。

六、引用溯源:让每条结论都能回到原文

上下文组装时给每个 Chunk 生成稳定引用编号,编号和文档 ID、版本、页码、章节一一对应。模型只输出 [C1]、[C2],前端再把编号渲染成可点击来源。

public record Evidence(
String citationId,
long chunkId,
String title,
int version,
Integer page,
String content) {}

public String buildContext(List<Candidate> candidates) {
return IntStream.range(0, candidates.size())
.mapToObj(i -> {
Candidate c = candidates.get(i);
String id = "C" + (i + 1);
return "[" + id + "] 文档=" + c.title()
+ " 版本=" + c.version()
+ " 页码=" + c.page()
+ "\\n" + c.content();
})
.collect(Collectors.joining("\\n\\n"));
}

生成后校验引用编号是否存在,且引用内容是否在当前候选集合中。引用了不存在的 [C9],或者引用了一个没有支持该结论的 Chunk,都应视为回答质量问题。

七、评测必须拆分检索和生成

如果最终答案错了,不一定是模型生成能力差,也可能是正确证据根本没有被召回。评测分两层:

  • 检索评测:Recall@K、MRR、nDCG、过滤正确率。
  • 生成评测:引用支持率、答案覆盖率、拒答准确率、格式通过率。

可以把每条错误归因到以下类别:未召回、召回但排序靠后、证据冲突、上下文过长、模型推理错误、引用错误和权限错误。错误归因比只记录“答案不对”更有优化价值。

八、典型失败案例

错误码命中了,但处理步骤不对

可能是多个版本文档都包含同一错误码,旧版本排名更靠前。解决方案是把版本状态作为过滤条件,并让版本信息进入 Rerank 输入。

语义相似度很高,但没有关键数字

用户问“连续三次”,召回的内容只包含“重复告警”。可以对数字、错误码和型号做关键词增强,并要求证据覆盖这些实体。

候选变多,回答反而变差

召回了互相冲突的制度和历史记录。应按版本、发布日期和文档状态做治理,并在冲突时主动提示“存在多个版本,需要确认适用范围”。

引用很多,但不支持结论

模型为了显得可靠而堆引用。可以限制每个结论最多引用 2~3 条,并要求引用内容包含相关实体或条件。

九、性能和成本

建议先测四个阶段:关键词查询、向量查询、Rerank 调用、模型生成。Rerank 候选从 60 条增加到 200 条,准确率可能略有提升,但延迟和成本会显著增加。对高频固定问题可以缓存召回结果,但缓存键必须包含租户、权限、知识库版本和查询归一化结果。

可以采用分层策略:普通问题只做混合召回;高风险问题增加 Rerank 和引用校验;知识库外问题走拒答检测。不同场景使用同一套“最重链路”,通常会造成成本浪费。

十、上线检查清单

  • 关键词和向量结果是否都保存原始排名。
  • 融合时是否去重、归一化并保留来源。
  • 权限过滤是否发生在发送给 Rerank 之前。
  • 版本、状态和生效时间是否进入检索条件。
  • 生成答案中的每个引用是否可回溯。
  • 是否有知识库外问题和冲突文档测试。
  • Embedding 或 Rerank 模型更换后是否重新跑评测。
  • 缓存是否包含租户、权限和知识库版本。

十一、总结

混合检索不是“多调几个参数”,而是把精确匹配、语义理解、排序决策和证据治理组合起来。关键词擅长命中实体,向量擅长理解表达,Rerank 负责精排,引用溯源负责让答案可复核。

最有效的优化顺序通常是:先确认正确证据能被召回,再确认它排在前面,最后才调整模型回答。这样才能知道问题出在数据、检索还是生成,而不是在 Prompt 上反复试错。

读者讨论

如果你的知识库包含大量错误码、型号或版本号,建议先统计这些实体在查询中的占比,再决定关键词与向量的权重。

赞(0)
未经允许不得转载:171主机测评 » RAG 为什么要混合检索:从关键词、向量到重排与引用溯源
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址