问题与目标
向量检索擅长语义近似,但可能忽略设备编号、错误码和精确术语;BM25 擅长字面匹配,却不理解同义表达。混合检索先从两条路径召回候选,再融合分数或名次,Rerank 最后对少量候选精排。
本篇重点不是堆组件,而是用固定查询集区分三类失败:没有召回、召回错误、排序不佳。只有知道失败在哪一层,优化才有方向。
核心概念
- BM25:根据词频、逆文档频率和文档长度计算相关性,适合编号与关键词。
- 稠密检索:使用 Embedding 相似度召回语义相关片段。
- 混合检索:合并两路候选,常用加权分数或 Reciprocal Rank Fusion(RRF)。
- Rerank:使用更强的交叉编码器或模型对查询—候选对重新评分,质量更高但延迟更大。
- Query Rewrite:改写检索表达,不应改变用户意图或偷偷添加事实。
不同检索器的原始分数尺度通常不同,直接相加没有意义。RRF 只依赖名次,是稳定的融合起点。
可运行实现
下面对两组已排序结果做 RRF,不依赖外部库:
python
from collections import defaultdict
def rrf(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
scores = defaultdict(float)
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] += 1 / (k + rank)
return sorted(scores.items(), key=lambda item: item[1], reverse=True)
bm25 = ["error-c17", "network-guide", "battery-guide"]
dense = ["network-guide", "recovery-runbook", "error-c17"]
print(rrf([bm25, dense]))
候选合并后先去重,再只把前 10 到 30 条交给 Reranker。离线评估数据可以写成:
python
cases = [
{"query": "C17 如何处理", "relevant": {"error-c17"}},
{"query": "终端断网后恢复步骤", "relevant": {"network-guide", "recovery-runbook"}},
]
def recall_at_k(ranking: list[str], relevant: set[str], k: int) -> float:
return len(set(ranking[:k]) & relevant) / len(relevant)
每次变更分词、Embedding、权重、候选数或 Reranker,都在同一查询集上记录 Recall@k、MRR、延迟和失败类型。示例中的排序只是数据流演示,生产结果必须来自真实检索器。
常见问题与排查
没有召回
检查文档是否入库、权限过滤是否过严、查询与文档是否使用同一 Embedding、关键词分词是否破坏编号,以及相关答案是否在同一 Chunk。
召回了错误文档
检查近义主题干扰、过大的 Chunk、缺少元数据过滤和查询改写漂移。增加候选数只能扩大集合,不能修复错误语义。
正确文档排在后面
尝试 RRF、字段权重和 Rerank,并查看正确文档在各路原始名次。若它从未进入候选,重排器无法创造它。
只看最终回答评价检索
模型可能凭参数知识答对,也可能在有正确证据时答错。检索指标与生成指标必须拆开测量。
小结
检索优化的顺序应是:先建立相关性标注,再定位召回或排序问题,最后选择关键词、稠密、融合或重排手段。混合检索不是必然更好,它必须用同一查询集和延迟成本证明价值。
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


