ARTICLE / 7 MIN READ
RAG 混合检索与重排序实战
用意图识别、三路召回、RRF 融合和 cross-encoder 重排,破解制造业知识库里"手册说正常但实际过热"这类矛盾问题。
制造业知识库有个天然的撕裂:维修手册是非结构化长文本,ERP 物料表是结构化行数据。用户问“A860-T301 电机过热,但手册说正常,怎么排查?”,纯向量检索会因为语义相似召回一堆“电机运行正常”“温度在额定范围内”的废话——它们和 query 的余弦距离很近,但对排查毫无帮助;纯关键词检索又抓不住“过热排查流程”这种没有精确词面重叠的意图。单路召回在这里必然失败,必须上 Hybrid Search 加 Rerank。
先做意图识别和 query 改写
召回之前必须有一个前置层,否则后面所有权重都是拍脑袋。这一层做三件事:
- 实体抽取:物料号
A860-T301是强实体,正则[A-Z]\d{3}-[A-Z]\d{3}先兜底,再叠一层 NER 兜漏网的型号。抽出来的实体决定了后面 BM25 权重要拉高——实体是“必须精确命中”的锚点,向量对型号里一个字符的差别根本不敏感(A860 和 A861 语义几乎一样,但可能是完全不同的电机)。 - 意图分类:故障排查 / 参数查询 / 操作指导。这个 query 是“故障排查”,它天然需要“流程类”文档,而不是“状态描述类”文档。
- query 改写:把口语化的“过热”扩写成“温度过高、温升超标、over-temperature、报警代码”,做术语归一。矛盾点“手册说正常”要拆成两个子检索:一个查手册的额定温度范围,一个查过热排查流程。
三路并行召回,各取 TopN
用户 query: "A860-T301 电机过热,但手册说正常,怎么排查?"
│
▼
┌─────────────────────────────────────────────┐
│ 意图识别 + query 改写 │
│ 实体=[A860-T301] 意图=故障排查 │
│ 扩写=[温升超标, over-temp, 报警] │
└─────────────────────────────────────────────┘
│
├──────────────┬───────────────────────┐
▼ ▼ ▼
① BM25 精确层 ② 向量语义层 ③ ERP 结构化过滤
keyword/词面 bge-m3 embedding metadata filter
Top 50 Top 50 material_no='A860-T301'
│ │ │
└──────────────┴───────────────────────┘
▼
④ RRF / 加权融合 (权重按意图动态) → Top 100
▼
⑤ Rerank cross-encoder → Top 8
▼
⑥ 多文档聚合 + 矛盾消解 → 拼 context 给 LLM
① BM25 精确层负责实体和词面。BM25 打分公式决定了它对稀有词(高 IDF)敏感,正好适合物料号这种低频高信息量的 token:
score(D,Q) = Σ IDF(qi) · f(qi,D)·(k1+1) / (f(qi,D) + k1·(1 - b + b·|D|/avgdl))
参数: k1=1.2, b=0.75
A860-T301 在整个语料里 IDF 极高,只要文档里出现就会被强力顶上来,这是向量做不到的。
② 向量语义层负责“过热排查流程”这种没有词面重叠的意图。中文场景我用 bge-m3 做 embedding,它同时输出 dense 和 sparse 向量,dense 走 HNSW 近似最近邻取 Top 50。
③ ERP 结构化过滤不是排序,是硬过滤。物料表用 metadata filter:WHERE material_no = 'A860-T301' 直接锁定这台电机的额定参数(额定温度、温升限值、报警阈值)。这一路给的是“权威事实”,用来判定“手册说的正常”到底是不是真的正常。
融合:权重必须按意图动态
两种融合各有场景。RRF(倒数排名融合) 不依赖分数量纲,最稳:
RRF(d) = Σ_i 1 / (k + rank_i(d)) k=60
它只看每一路里的排名,天然规避了 BM25 分数和余弦相似度不可比的问题。但 RRF 对两路一视同仁,无法体现“这个 query 更偏实体还是更偏语义”。
所以我在 RRF 之上再叠一层意图驱动的加权。归一化后线性融合:
score = α · norm(bm25) + (1-α) · norm(vector)
α 不是常数,由意图层决定:
| query 特征 | α (BM25 权重) | 理由 |
|---|---|---|
| 含强实体(物料号/报警码) | 0.7 | 精确命中优先,型号错一位就是另一台设备 |
| 纯故障描述、无型号 | 0.3 | 语义主导,靠向量找相似故障 |
| 参数查询(额定值多少) | 0.6 | 词面 + 结构化过滤兜底 |
这个 query 两者都占,实测 α≈0.55 效果最好——既保证 A860-T301 精确命中,又让“排查流程”的语义召回不被压没。
Rerank:单靠融合分不够
融合后的 Top 100 还是“粗排”,BM25 和向量都是双塔式的(query 和 doc 分开编码),无法建模两者的深层交互。Rerank 用 cross-encoder,把 (query, doc) 拼成一条输入过一次 Transformer,直接输出相关性分——精度高一个量级,代价是不能预计算、每对都要现算,所以只能对 Top 100 做,不能对全库做。
选型是延迟、成本、中文效果、私有化四个维度权衡:
| 方案 | 延迟(100 doc) | 成本 | 中文 | 私有化 |
|---|---|---|---|---|
| Cohere Rerank v3 | ~200ms(API) | 按 1000 次搜索计费 | 好 | 不支持 |
| BGE-reranker-v2-m3 | ~150ms(单卡 A10) | 一次性 GPU | 强 | 支持 |
| 自研微调 | 视规模 | 训练+标注贵 | 可定制 | 支持 |
制造业知识库是内网、数据敏感、不能出网,Cohere 的 API 直接出局。最终选 BGE-reranker-v2-m3:开源、可私有化部署、中文效果在维修语料上和 Cohere 打平,单卡 A10 重排 100 条约 150ms 可接受。数据积累够了再用线上点击/人工标注的 pair 微调它,逐步逼近自研效果。
矛盾消解:靠多文档聚合,不靠 top-1
“手册说正常但实际过热”这种矛盾,是这套 pipeline 的价值所在。关键动作是 rerank 后取 Top 8 而不是 Top 1,并强制多文档聚合:
- 手册召回的“额定温度 ≤ 75℃ 属正常”进 context;
- 过热排查流程文档(“温升超标先查散热风道、再查绕组绝缘、最后查负载电流”)进 context;
- ERP 结构化层给出这台 A860-T301 的权威额定阈值。
三份放一起,LLM 就能推理出:手册说的“正常”是有前提的额定工况,用户观测到的温度若已超过 ERP 里的报警阈值,那“手册正常”和“实际过热”就不矛盾了——是工况偏离,应走排查流程。如果只返回 top-1,大概率只给出“手册说正常”就把用户顶回去了。矛盾消解本质上不是检索算法问题,是“别过早收敛到单一文档”的工程约束。
边界
这套 pipeline 不是银弹。BM25 对未登录词(手册里的生僻缩写)仍会漏,需要维护同义词/别名词典;意图分类错了会把 α 调反,得有兜底(分类置信度低时退回纯 RRF);rerank 增加一次网络/GPU 往返,端到端 P99 会涨 200ms 左右,对延迟极敏感的场景要做召回缓存或降级开关。三路召回的 Top50、Top100、Top8 都是超参,必须在业务金标集上调,别照抄。
参考资料:Cohere Rerank 文档、BGE-reranker (FlagEmbedding)、Reciprocal Rank Fusion 原始论文。
GITHUB DISCUSSION
评论与回复
评论保存在 GitHub Discussions,登录 GitHub 后即可参与,发布和回复都在本页完成。
评论区进入视口后自动加载。