03. 检索与重排:让制度问题先找到对的证据
学习目标
你将能为“差旅报销”类问题选择检索方法,解释 BM25 与向量检索的互补性,用 RRF 合并结果,并用日志定位召回失败。
检索链路可写成:
问题 → 规范化/改写 → 权限与时间过滤 → BM25 + dense → RRF → 重排 → 去重 → 上下文每一步都应保留候选 ID 和分数,最终回答才能解释“为什么选了这条制度”。
1. 先定义检索对象
索引对象是 02 章生成的 chunk,而不是任意字符串。至少存储:chunk_id、正文、标题路径、版本、生效时间、ACL、embedding、BM25 字段。
查询对象也要结构化:
{
"text":"北京出差住宿能报多少?",
"as_of":"2026-10-17",
"department":"研发",
"user_groups":["all-employees"]
}as_of 用于时间过滤;不要把日期只拼到自然语言里,否则索引层无法保证版本正确。
2. Dense 检索:找语义相近的说法
把问题和 chunk 转为同一 embedding 空间,用余弦相似度或点积取 Top-K。它能把“住店上限”与“住宿标准”联系起来,也可能把“国内出差”误召回到“海外出差”。
向量检索的基本检查:
- 查询和文档使用同一个 embedding 模型及版本。
- 记录向量维度、归一化方式和模型哈希。
- 过滤 ACL、版本后再计算 Top-K,或确认向量库支持预过滤。
- 对极短查询(“500 元?”)补充标题或实体,否则语义信号太少。
本地小规模实验可以用 Python 标准库计算余弦相似度;生产系统再换向量数据库。先验证召回质量,再讨论索引产品。
3. BM25:找精确词、编号与金额
BM25 根据词频、文档长度和逆文档频率评分。制度检索中,条款号“第十二条”、产品名、错误码、金额和城市名常常依赖精确匹配。
中文需要分词或字符 n-gram。分词器应把“住宿费”“一线城市”作为稳定词,同时保留数字和版本号。停用词表不能删除“不得”“无需”等否定词。
BM25 诊断样例:
查询:第十二条 一线城市 住宿上限
命中:travel-v3:p7:article-12 (BM25=11.2)
未命中:travel-v2:p7:article-12 (因 effective_to=2025-12-31 被过滤)4. 混合检索与 RRF
Dense 和 BM25 的分数通常不可直接相加:一个可能在 0 到 1,另一个没有固定上限。先分别取候选,再用 Reciprocal Rank Fusion(RRF)按名次合并:
RRF(d) = Σ 1 / (k + rank_i(d))如果某文档在 dense 排名第 2、BM25 排名第 1,k=60 时得分为 1/62 + 1/61。它不要求两个检索器的分数可比。
Elastic 官方实现说明 rank_constant 越大,低排名结果影响越大;rank_window_size 决定每个子检索器参与融合的窗口。团队可从 dense Top-50、BM25 Top-50、k=60 开始,用验证集调参。
伪代码:
def rrf(lists, k=60):
score = {}
for results in lists:
for rank, doc_id in enumerate(results, start=1):
score[doc_id] = score.get(doc_id, 0) + 1 / (k + rank)
return sorted(score, key=score.get, reverse=True)5. 元数据过滤先于排序
先过滤权限、产品线和有效期,再做排序。一个无权限文档即使语义最相似,也不能进入候选集。
过滤示例:
ACL ∩ 用户群组 ≠ ∅
且 effective_from <= as_of
且 (effective_to 为空 或 as_of < effective_to)
且 region ∈ 用户请求范围若用户问“2024 年标准”,把 as_of=2024-06-01 传入检索;若用户问“现在”,使用系统时钟并在日志记录具体日期。
6. 重排:把候选变成证据顺序
重排模型读取查询和候选正文,直接估计相关性。它适合处理“北京出差住几晚、超出部分谁承担”这类需要同时满足多个条件的问题。
典型流程是召回 50 条、重排 20 条、送入生成 5 至 8 条。重排输入要包含标题路径和版本信息,避免只看孤立正文。对表格 chunk,保留表头;对相邻条款,先按文档和章节去重。
没有重排模型时,可用规则基线:标题词命中 + 条款号命中 + 时间匹配 + 词汇覆盖率。把规则分数写入日志,方便以后替换模型时比较。
7. 去重与多样性
同一条款可能有重叠 chunk、HTML 复制页和 PDF 版本。按 doc_id + section_path 做近邻去重,每个文档设上限,避免 Top-8 全来自同一页。
但不要机械地去掉所有重复:一条规定和它的例外可能位于相邻条款。保留“主规则 + 例外 + 定义”三类证据,输出时按章节顺序排列,便于模型理解优先级。
8. 检索诊断表
为每个问题记录:查询文本、过滤条件、各路 Top-K、RRF 名次、重排分数、最终 chunk、未命中原因。
| 现象 | 常见原因 | 修复动作 |
|---|---|---|
| 精确条款号找不到 | 分词丢数字 | 保留数字、字符 n-gram |
| 同义问法找不到 | 只有 BM25 | 加 dense 或查询改写 |
| 旧版本排第一 | 未按日期过滤 | 把 as_of 变成索引过滤 |
| 结果全来自一页 | 无去重 | 按章节和文档设上限 |
| 召回正确但答案错 | 重排/上下文顺序差 | 加标题路径与例外条款 |
| 无权限内容出现 | 过滤太晚 | 召回前做 ACL 过滤 |
9. 评测与练习
建立 30 条带标注的问题集,标注“至少一个可回答 chunk”和“不可回答”。计算 Recall@5、MRR、首个正确版本排名、权限违规数和重复率。
练习一:为“上海出差住宿超过 450 元怎么办?”分别写 BM25 查询和 dense 查询,列出两路可能漏掉的证据。
练习二:给出三份候选的名次:dense=[A,B,C]、BM25=[C,A,D],用 k=1 手算 RRF 排序,并解释为什么 C 可能上升。
验收:在 30 条问题集上,团队设定 Recall@5 门槛;随机抽一条失败样本,能从日志判断是过滤、分词、embedding、RRF 还是重排导致。
参考资料
在生产环境还要记录索引构建时间、查询延迟和候选数量;性能回归不能用“感觉更快”判断。
最后,把一条线上失败问题加入回归集;下一次模型或分词器升级必须重跑它。
评论
正在加载评论…