RAG 召回率不达标:查询改写的适用边界与量化判据

举报
上云 发表于 2026/11/12 09:40:04 2026/11/12
【摘要】 召回差的根因常是用户提问与文档措辞不匹配。本文先给出用 oracle 改写测收益上界的归因方法,再分别拆解查询分解、多路改写、HyDE、指代消解各自对应的失败模式与反例,最后给出一套可量化的净收益判据与上线阈值。

召回率上不去,多数团队的第一反应是换 embedding 模型或重调 chunk size,但真正卡住的是用户提问和文档措辞压根不是一套词。查询改写能补这个洞,它同时是召回链路里最容易引入噪声的一环——不加判断地上 HyDE 和多路改写,指标经常是 Recall@20 涨了两个点、nDCG@5 掉了三个点。下面把这件事拆成可测的失败模式,给每种手段划出适用边界和上线阈值。

先算上界,再决定要不要改写

动手前先做一件事:测出改写的收益天花板。取 200~500 条真实线上 query,用强模型做一次"oracle 改写"——把 query 改写成"如果我是文档作者,我会怎么表述这件事",然后跑同一套检索,算 ΔRecall@20。

这个数字决定后续投入:

  • ΔRecall@20 < 3 个点:瓶颈不在措辞。去查切分粒度、混合检索缺失、元数据过滤条件、向量库的 index 参数。改写做再多也是给一个不匹配的索引打补丁。
  • 3~8 个点:值得做,但只做低成本的那一两种,别上重型链路。
  • > 8 个点:词汇鸿沟是主因,多路改写和 HyDE 都要认真设计。

上界之外还要做归因。把失败的 query 逐条看 top-50 里有没有正确文档的近似版本,按现象分类:

现象 归因 优先手段
top-50 无正确文档,但用文档原词重搜能召回 词汇不匹配 多路改写 + BM25 混合检索
答案需要跨 2 段以上文档才能拼出 语义欠定 查询分解
query 只有 5~8 字、口语化、无领域词 表达鸿沟 HyDE
出现"它""这个方案""刚才说的" 上下文缺失 指代消解
正确文档在 top-50 但排序靠后 排序问题,不是改写问题 换 reranker

最后一行尤其常见。大量被归因为"召回差"的 case,实际是召回够、排序差,改写只会让候选池更乱。

查询分解:只对欠定问题有效

分解的前提是"一个 query 里塞了多个可独立检索的检索意图"。典型场景:多实体对比("A 和 B 在权限模型上的差异")、多条件聚合("支持私有化部署且能对接 LDAP 的网关有哪些")、多跳推理("这个报错的根因模块由哪个团队维护")。

不该分解的信号有三个,都很硬:

  1. 答案可以放在单个 512 token 的 chunk 内。分解后每路子查询召回的都是宽泛背景文档,融合后反而把精确命中的那篇挤下去。
  2. query 里没有连接词(和、以及、分别、对比、哪个更)。单跳事实问题分解,等于人为制造 vocabulary mismatch。
  3. 子查询之间高度重叠。两条子查询召回集重合度超过 70%,说明分解没产生新的检索意图,只是把同一路查询跑了两遍,白白抬高噪声。

分解的判据可以先做一个二分类,再走结构化输出:

{
  "need_decompose": true,
  "reason": "包含两个独立实体 A/B 的属性对比",
  "sub_queries": ["A 的权限模型设计", "B 的权限模型设计"],
  "answer_in_single_chunk": false
}

要求模型显式输出 answer_in_single_chunk 比只输出 need_decompose 更稳,因为这两个判断经常冲突,暴露冲突本身就是好的过滤信号。

度量分解是否有效,别只看整体 Recall,看子问题覆盖率:人工标注每个 query 需要几段证据(evidence set),分解后统计每段证据是否至少被一条子查询召回。如果覆盖率提升但最终 nDCG 不升,说明融合阶段出了问题——通常是各路子查询的 top-k 直接拼起来,导致某一路的高分文档占满了上下文窗口。

多路改写:关键在多样性,不在数量

多路改写针对的是纯词汇不匹配:用户说"限流",文档写"流量控制";用户说"卡顿",文档写"响应延迟抖动"。做法是生成 3~5 个表述变体,各自检索后用 RRF 融合。

def rrf_fuse(rankings: list[list[str]], k: int = 60, top_n: int = 20):
    scores: dict[str, float] = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda x: -x[1])[:top_n]

两个参数值得较真:

  • 变体数取 3,不要取 8。 变体超过 5 个,新增召回的边际收益快速衰减,而噪声线性增长。
  • 必须做多样性约束。 生成完变体后算两两 embedding 余弦相似度,平均相似度 > 0.88 说明模型在堆同义词,等于没改写。这一步可以用一句 prompt 解决:"每个变体必须改变检索视角,不能只是换同义词"——但更可靠的做法是硬约束:相似度超阈值的变体直接丢弃。

多路改写的噪声机制很具体:变体越泛化,召回越偏向高频主题文档。库里关于"概述""入门"的文档向量位置居中,泛化变体最先命中它们,最终进 prompt 的 5 个位置被"大体相关"占满,精确文档被挤到第 8 位——Recall@20 变好看了,端到端回答质量反而下降。

所以多路改写的核心度量不是 Recall,是改写路径新增文档的进 prompt 率:

进prompt率 = 仅由改写召回的文档中,最终进入 LLM 上下文的比例

这个值低于 15%,说明改写召回的绝大多数是噪声,应该砍变体数或收紧多样性阈值;高于 40% 才说明改写真的在补漏。

HyDE:补表达鸿沟,但会污染精确查询

HyDE 的机制是让模型先生成一段假设答案,用假设答案的向量去检索。它真正的作用是把 query 的向量分布拉到 document 分布上——短 query 和长文档在向量空间里天然不同域,HyDE 是在做域对齐。这解释了它的适用条件:query 越短、越口语化、越缺领域术语,收益越大。

明确不该用的三类:

  1. 含精确标识符的 query。 型号、错误码、订单号、日期、金额。假设答案里模型会"编"一个看起来合理的数字,而这个数字的向量会主导检索,把真实匹配的那篇彻底压掉。
  2. 答案形态不唯一的 query。 "X 有什么优缺点"这类问题,HyDE 只会生成其中一个方向的答案(比如只写优点),检索随之偏向单一方向,另一半证据永远召不回。
  3. 延迟敏感的在线链路。 HyDE 多一次生成调用,P99 通常增加 300~800ms。如果首 token 时间卡在 1s 内,这个成本换不来对应的召回收益。

一个可操作的动态策略:对 query 做轻量特征判断(是否含数字/大写标识符/引号内容、token 长度、是否含疑问词),命中精确查询特征就关掉 HyDE;同时保留原 query 一路检索,HyDE 结果与原 query 结果各取 top-10 再融合,而不是让 HyDE 独占召回。双路融合能吃掉大部分 HyDE 的副作用——它不会让精确匹配消失,只会稀释。

验证 HyDE 是否值得,看两个数:有标注集上的 ΔRecall@20,以及假答案实体污染率——即 HyDE 路径召回的文档中,包含假设答案里虚构实体的比例。后者超过 20%,说明模型在编具体内容,这时候应该换更保守的 prompt("只生成可能的文档标题和关键词,不要生成完整答案"),而不是加更多检索路。

指代消解:顺序错了,后面全白做

多轮对话场景里,指代消解必须是所有改写的第一步。如果先做多路改写再消解,模型会围绕"它"生成一堆变体,全是废的。

判断是否需要进行指代消解很简单:query 里出现代词、省略主语、或与上一轮话题强相关但缺少实体。做法是把最近 3 轮对话和当前 query 一起给模型,要求输出独立可检索的 query,同时给出置信度和补全的实体列表:

{
  "standalone_query": "ModelArts 的推理服务如何配置自动扩缩容",
  "resolved_entities": ["ModelArts", "推理服务"],
  "confidence": 0.82
}

关键在 confidence。低于 0.6 时不要用改写结果替换原 query,而是双路检索:原 query 一路、改写后一路,RRF 融合。原因是指代消解最危险的失败模式是过度补全——用户说"它支持私有化吗",上一轮聊了三个产品,模型可能挑错一个,然后整条链路都在检索错误的产品文档,而且召回结果看起来还挺相关,人工抽检很难发现。

过度补全的检测手段:统计 resolved_entities 中未在原 query 出现的实体,其在最终召回文档中的出现率。如果某个被补全的实体在召回文档里出现率异常高(> 80%),说明检索被这个实体单方面主导,需要人工复核这批 case。

整合判据:一条可跑的决策链

把上面的判断串成顺序,避免互相干扰:

  1. 指代消解(仅多轮场景,低置信度走双路)
  2. 判断是否分解(仅当答案跨 chunk 且存在多意图)
  3. 多路改写(仅在归因为词汇不匹配时启用,变体数 3,多样性阈值 0.88)
  4. HyDE(仅在 query 短、无精确标识符、延迟可接受时启用,与原 query 双路融合)

每一步都要能被单独关掉做 A/B。离线 harness 至少算四个指标:

import math

def score(ranked: list[str], gold: set[str], k: int = 20):
    hit = len(set(ranked[:k]) & gold)
    recall = hit / len(gold) if gold else 0.0
    dcg = sum(1.0 / math.log2(i + 2) for i, d in enumerate(ranked[:k]) if d in gold)
    idcg = sum(1.0 / math.log2(i + 2) for i in range(min(len(gold), k)))
    return {"recall": recall, "ndcg": dcg / idcg if idcg else 0.0}

上线门槛建议用净收益而不是单一 Recall:

net = ΔRecall@20 − 0.6 × 噪声率 − 0.3 × ΔnDCG损失
噪声率 = 改写路径新增召回文档中未进 prompt 的比例

参考阈值:

手段 启用条件 上线门槛
指代消解 多轮 + 代词/省略 独立 query 命中率 > 75%
查询分解 多意图 + 跨 chunk 子问题覆盖率提升 > 20%,且 nDCG 不降
多路改写 ΔRecall 上界 > 5 点 + 词汇不匹配 进 prompt 率 > 25%,变体相似度 < 0.88
HyDE 短 query + 无精确标识符 + 延迟余量 污染率 < 15%,端到端提升可复现

一条经验:同一时间只上线一种改写手段,跑满一到两周再叠第二种。 改写手段之间的交互非常强,指代消解和多路改写同时上线导致指标变差的情况很常见,两者都会扩写 query,叠加后 query 严重漂移,而单独看每个都不明显。

落地建议

  1. 先跑 oracle 上界实验,用数字决定要不要做改写。 500 条真实 query、一次强模型改写、一次检索,半天能出结论。上界 < 3 个点就把人力挪去修切分和混合检索。
  2. 建立失败模式标注集,而不是只记一个召回率。 给每条失败 query 打上"词汇不匹配 / 语义欠定 / 表达鸿沟 / 上下文缺失 / 排序问题"标签,每个标签至少 30 条。改写手段的选择应该由标签分布决定,而不是由论文热度决定。
  3. 所有改写都保留原 query 一路召回。 无论是 HyDE 还是指代消解,都做双路 RRF 融合而不是替换。这一条能消掉大部分改写引入的噪声,代价只是检索次数翻倍。
  4. 把"进 prompt 率"和"污染率"纳入日常看板。 只看 Recall@20 会被改写手段骗过去,这两个指标才能反映改写召回的文档到底有没有被用上。
  5. 每种手段做成独立开关,灰度上线。 改写链路一旦耦合,出了问题无法定位是分解错了、变体跑偏了还是指代补错了,回滚也只能整体回滚。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。