Tensor NotesNotes on machine learning
Retrieval

RAG 不是搜索加拼接:分块、重排与上下文压缩的实践

RAG 不是搜索加拼接:分块、重排与上下文压缩的实践题图

过去半年我们把一套检索增强系统从原型推到线上,中间返工过三次。回头看,最贵的教训是:召回率的天花板由分块策略决定,而不是由向量模型决定。换更强的 embedding 模型带来的提升,往往不如把分块重做一遍。

一、朴素方案会在哪里失败

最常见的起手式是:按 512 字固定切分 → 全部灌进向量库 → 查询时取 top-5 → 拼进 prompt。这个方案在演示时表现良好,上线后会在几类问题上系统性翻车:

  • 答案跨块:一个定义在块尾,它的例外条款在下一块开头。取回其中一块,模型给出的答案是错的而不是"不知道"。
  • 表格被腰斩:表头留在上一块,数据行进了下一块。模型看到一堆无标签的数字,开始编造列名。
  • 查询与文档词汇不匹配:用户问「怎么退货」,文档写的是「逆向物流申请流程」。稠密向量在这种同义改写上不如想象中稳。
  • 实体名和编号:查「SKU-88213 的保修期」,向量检索对精确 ID 极不敏感,BM25 反而一击即中。

二、分块:语义边界优先于固定长度

我们最终的分块器是一个三层回退结构:

SEPARATORS = [
    "\n## ",      # 1. Markdown 二级标题
    "\n### ",     # 2. 三级标题
    "\n\n",       # 3. 段落
    "。", ";",   # 4. 句子
]

def split(text, target=700, hard_max=1100):
    """按分隔符优先级递归切分,只在超过 hard_max 时下沉一级。"""
    for sep in SEPARATORS:
        parts = text.split(sep)
        if max(len(p) for p in parts) <= hard_max:
            return merge_small(parts, sep, target)
    return hard_split(text, hard_max)

三条配套规则同样重要:

  1. 标题路径前置。每个块的开头强制拼上它所属的完整标题链,例如「售后政策 > 退换货 > 生鲜类目」。这一条把跨块指代问题解决了大半,实现成本却几乎为零。
  2. 表格不切。检测到 Markdown 表格或 HTML table 就整体保留,超长时按行切但每片都复制表头。
  3. 重叠 15%。相邻块共享约 100 字。重叠比例再高收益递减,但存储和检索噪声线性上升。

三、检索:混合召回是必需品,不是优化项

把 BM25 和向量检索的结果用倒数排名融合(RRF)合并:

RRF(d) = Σr ∈ retrievers 1 / (k + rankr(d)) ,k 取 60

RRF 的好处是不需要对两路分数做归一化——它们的量纲根本不可比。只用排名,鲁棒得多。

三种检索策略在内部评测集上的召回率对比
图 1 · 内部 3.2 万条问答评测集上的召回率。混合检索加重排相比单路向量,Recall@5 提升 15 个点

重排序器(cross-encoder)放在混合召回之后,把 top-50 精排到 top-8。它比双塔模型准得多,因为查询和文档在同一次前向里做完整交互;代价是不能预计算,延迟随候选数线性增长。50 个候选在 A10 上大约 40 ms,可以接受。

四、上下文压缩:给模型的不是越多越好

拿到 8 个块之后直接全塞进去是有代价的。长上下文里存在明确的位置效应:放在中间的信息被利用的概率显著低于开头和结尾。我们做过一个对照实验,把正确答案分别放在 8 个位置上:

正确块的位置12345678
回答正确率0.940.890.810.760.740.790.860.92

中间凹陷了 20 个点。据此我们做了两件事:

  • 按重排分数交替排布:最相关的放最前,次相关的放最后,最不相关的塞中间。这是纯粹的排布技巧,零成本,正确率提升约 3 个点。
  • 句级抽取压缩:用一个小模型判断每个句子与查询的相关性,剔除明显无关的句子。上下文缩短 40%,延迟降低 22%,正确率不降反升 1.5 个点。
关于 prompt最有效的一句指令是明确授权模型说不知道:「如果给定材料不足以回答,直接说明缺少哪部分信息,不要基于常识补充。」加上这句之后,我们的幻觉率从 8.1% 降到 2.4%。不是模型变聪明了,是它终于知道"承认不知道"是被允许的。

五、评测:不要只看端到端准确率

端到端指标掉了,你无法知道该修检索还是该修生成。我们把评测拆成三个独立的量:

  • 召回命中率:正确答案所在的块是否进了候选集。这是检索的上界,生成再强也救不回来。
  • 忠实度:回答里的每个事实性陈述能否在给定材料中找到支撑。用一个独立的判别模型逐句核对。
  • 可答性判断准确率:材料确实不足时,模型是否正确拒答。这一项最容易被忽略,也最影响用户信任。

建立这三个指标之后,每次回归都能立刻定位到是哪一环退化了。上线前的最后一次返工就是靠它发现的:某次向量库重建后维度顺序错位,召回命中率从 0.86 掉到 0.61,而端到端准确率只掉了 9 个点——因为模型用参数化知识蒙对了一部分,掩盖了真正的故障。

六、还没解决的问题

多跳问题依然是弱项。「A 公司 2024 年的营收,比它 2022 年收购的那家公司同期营收高多少」这类查询,需要先检索出被收购方是谁,再用这个名字发起第二次检索。目前的单轮召回结构处理不了,正在试的方案是让模型显式产出子查询后迭代检索,但延迟会翻倍,还在权衡。

上一篇:INT4/INT8 量化推理:从 GPTQ …下一篇:MoE 稀疏路由的工程真相:负载均衡、容量因…

继续阅读

Related