Skip to content

03. 检索与重排:让制度问题先找到对的证据 ​

学习目标 ​

你将能为“差旅报销”类问题选择检索方法,解释 BM25 与向量检索的互补性,用 RRF 合并结果,并用日志定位召回失败。

检索链路可写成:

text
问题 → 规范化/改写 → 权限与时间过滤 → BM25 + dense → RRF → 重排 → 去重 → 上下文

每一步都应保留候选 ID 和分数,最终回答才能解释“为什么选了这条制度”。

1. 先定义检索对象 ​

索引对象是 02 章生成的 chunk,而不是任意字符串。至少存储:chunk_id、正文、标题路径、版本、生效时间、ACL、embedding、BM25 字段。

查询对象也要结构化:

json
{
  "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 诊断样例:

text
查询:第十二条 一线城市 住宿上限
命中: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)按名次合并:

text
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 开始,用验证集调参。

伪代码:

python
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. 元数据过滤先于排序 ​

先过滤权限、产品线和有效期,再做排序。一个无权限文档即使语义最相似,也不能进入候选集。

过滤示例:

text
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 还是重排导致。

参考资料 ​

在生产环境还要记录索引构建时间、查询延迟和候选数量;性能回归不能用“感觉更快”判断。

最后,把一条线上失败问题加入回归集;下一次模型或分词器升级必须重跑它。

评论

正在加载评论…