☰
RAG 召回差别只怪分块:先用查询改写把问题问对
2026/10/14 3:44:04 网站建设 项目流程

授权与合规声明
本文为原创技术分享,所有示例代码均运行于作者自建的隔离环境,仅供学习交流。文中涉及的第三方模型与服务以各自官方文档为准,请在服务条款范围内使用。转载请保留出处。

一、为什么召回差常怪错地方(先量 query-doc 表述 gap)

先把"分块"从嫌疑名单里拿掉

RAG 上线后最常见的抱怨是"检索不出来"。团队第一反应往往是调分块大小、换切分策略、加重叠窗口。但翻一下线上日志就会发现,真正卡住的多半是用户的提问方式和文档写法对不上:用户说"怎么让模型不瞎编",文档标题却是"生成式模型的事实性约束与引用策略"。两边说的是同一件事,向量却离得很远。

先量再改,是本文贯穿的主线。把一批真实 query 和候选文档摆在一起,人工或自动判一下"用户想找的,文档里到底有没有、用的什么词"。如果文档里明明有答案,只是措辞不同,那问题在 query 侧,而不在分块。分块改的是"文档怎么切",改不了"用户怎么问"。

三种典型的表述错位

  • 口语化:用户用大白话,文档用术语。比如"卡死了"对应文档里的"请求超时与降级处理"。
  • 省略指代:多轮对话里"它"“这个方法”"上面那个"没有主语,单句无法检索。
  • 用词不同:同一个概念,用户和文档各说各的词,比如"大模型乱说话"和"模型幻觉"。

这三类错位都不该靠改分块来解决,因为无论怎么切,文档里的词还是那个词,和用户问法的距离不会变。

一个低成本的量化起点

取最近一段时间的真实 query(或抽样标注一小批),对每条手动标出"理想答案在哪些文档里"。这就是后面第五章算命中率的基准集。在没做改写前先跑一遍基线召回,后面所有改写策略都和这条基线对比,才有判断依据。

# 基线召回统计示意:query 与已标注的相关文档集合求交集defhit_rate(baseline:dict,retrieved:dict)->float:hits=0forq,goldinbaseline.items():hits+=1ifgold&retrieved[q]else0returnhits/len(baseline)

注:以上为逻辑示意,gold 为人工标注的相关文档集合,retrieved[q] 为本次检索返回文档标识。

二、多查询展开(multi-query)

思路:一题拆多角

多查询展开让模型把一个问题改写成多个不同表述,分别检索后合并结果。比如"怎么防止模型胡说"可以展开成"生成式模型事实性控制方法"“如何降低大模型幻觉”"引用外部知识约束输出"三个角度。不同角度覆盖了不同措辞,漏掉的概率就小了。

它解决的是"用词不同"和"口语化"两类错位:原 query 只命中一种说法,而文档可能用了另外的说法。

实现:让模型生成多个改写 query

用一次轻量调用产出 N 个改写,再分别走检索。注意让模型只输出改写、不要额外解释。

fromtypingimportList PROMPT="把下面这个问题改写成 {n} 个不同表述,覆盖不同用词和角度,每行一个,不要编号:\n{q}"defmulti_query(q:str,n:int=3,client=None,model:str="your-model")->List[str]:resp=client.chat.completions.create(model=model,messages=[{"role":"user","content":PROMPT.format(n=n,q=q)}],)return[line.strip(" -")forlineinresp.choices[0].message.content.splitlines()ifline.strip()]

注:接口字段以你所用服务商的官方文档为准,示例仅展示调用结构。

适用与注意

多查询对"用词不同""口语化"收益明显。落地时要做三件事:合并去重、对每路返回做轻量重排避免某一角度刷屏、控制展开数量。一般 3 到 5 个即可,过多会拉高延迟且边际收益递减。如果某一路改写明显偏离原意,应直接丢弃该路结果。

三、HyDE 假设性文档嵌入

思路:先造一个答案再去查

HyDE 的做法是:先让模型针对 query 生成一段"假设答案",再用这段答案的向量去检索。理由是——答案的用词往往比问题更接近文档。问题"怎么降幻觉"很短,但模型生成的假设答案里会自然出现"事实性约束"“引用”"检索增强"等文档常用词,向量更贴。

它适合"问题短、答案结构相对固定"的场景,用假设文档把稀疏的 query 向量拉向文档密集区。

实现

生成假设文档,取嵌入,再检索。可以把 HyDE 向量与原始 query 向量做加权融合,而不是只信假设文档。

defhyde_retrieve(q:str,client=None,embed=None,model:str="your-model",top_k:int=5):hypo=client.chat.completions.create(model=model,messages=[{"role":"user","content":f"针对这个问题写一段可能出现在技术文档里的回答:\n{q}"}],).choices[0].message.content vec=embed(hypo)returnvector_search(vec,top_k=top_k)

注:vector_search 为你检索后端的占位函数,按实际接口替换。

局限

HyDE 在领域术语密集、答案结构固定的场景更稳;在需要严格事实、不能容忍模型"编"答案的领域要谨慎,因为假设答案可能把检索带偏。一个稳妥的折中是:用原始 query 向量和 HyDE 向量做加权融合,权重通过第五章的评测来确定,而不是拍脑袋给固定值。

四、指代消解与上下文补全

问题来源:多轮对话里的"它"

用户第二轮问"它支持批量吗",单看这句没法检索——"它"指第一轮提到的某个框架。把上下文里被省略的主语补回去,才能变成可检索的完整 query。这类错位在带对话历史的助手类应用里尤其常见。

实现:补全成完整 query

把历史轮次和当前句一起交给模型,输出一条自包含的 query。关键是"只补主语,不改意图"。

defrewrite_with_context(history:list,current:str,client=None,model:str="your-model")->str:ctx="\n".join(f"{m['role']}:{m['content']}"forminhistory)prompt=("以下是多轮对话,请把最后一句补全为不依赖上下文也能检索的完整问题,""只补全被省略的指代,不要改动原意:\n"f"{ctx}\n最后一句:{current}\n补全后:")returnclient.chat.completions.create(model=model,messages=[{"role":"user","content":prompt}]).choices[0].message.content.strip()

注意

补全要"只补主语,不改意图"。如果模型擅自把问题改成了另一个方向,召回就会跑偏。建议对补全结果做简短校验,必要时回退到原始 query。一个实用的兜底:当补全结果与原 query 的重叠词过少时,直接放弃补全、用原句检索。

五、改写前后对比怎么测(命中率)

标注 query 集

用第一章攒下的基准集:每条 query 对应一组人工标注的相关文档。这个集子宁可少而准,不要为了数量堆一批标注质量差的样本——噪声标注会让你分不清是改写没用还是标注错了。

计算命中率

对每个 query,取改写后合并检索返回的前 K 篇,看是否命中标注文档。改写前后各算一次 recall@K,差值就是改写带来的提升。

defrecall_at_k(baseline:dict,pred:dict,k:int)->float:hits=0forq,goldinbaseline.items():topk=pred[q][:k]hits+=1ifgold&set(topk)else0returnhits/len(baseline)

看哪个指标

别只看整体均值。按 query 类型拆开看:口语化、含指代、用词不同的三类,分别算提升。如果只有某一类明显受益,就只对那类开改写,其余走原 query。这样既能拿到收益,又避免给已经问得很清楚的用户引入噪声。

这份资料是什么:这一章讲怎么用标注集量化改写前后的命中率,评测模板已整理好。放在资料包里,扫码即可获取:

六、何时不用改写

query 已经很规范

如果你们的用户多是内部工程师,query 本身就带术语、表述清晰,改写几乎不带来收益,反而可能把清晰的问法搅乱。此时继续在 query 侧投入,性价比很低。

改写引入噪声

改写会放大错误:模型一旦把 query 理解偏,多个改写会一起跑偏,召回反而下降。当基线召回已经很高(比如接近满命中),再加改写往往是负优化,因为它在已经正确的检索上叠加了不确定性。

判据

一句话:改写前后召回@K 没有显著提升,就关掉它。判据来自实测差值,不是拍脑袋。给改写策略都配上"只在提升超过阈值时启用"的开关,是控制噪声最直接的方式。

七、落地清单

先量 gap 再决策

上线前先跑基线:真实 query 集加标注相关文档,算原始召回。gap 在 query 侧才上改写;gap 在分块或切分才去动文档侧。先量再改,避免盲目优化。

三策略怎么选

  • 用词不同、口语化:多查询展开优先。
  • 问题短、答案结构固定:试 HyDE,并把假设向量与原始向量做加权融合。
  • 多轮对话、含指代:上下文补全,且只补主语不改意图。

上线阈值

给改写设开关和灰度:只对"改写后召回提升超过阈值"的 query 类型启用,其余回退原 query。保留可关闭的总开关,避免某次模型更新后改写整体劣化却关不掉。结论回到开头那句话——先量 query 与文档的表述 gap,再决定改分块还是改 query。

这份资料是什么:第七张落地清单把三种策略的启用条件和灰度开关都列好了,配套清单已整理。放在资料包里,扫码即可获取:

附表 A:本文引用事实与出处对照表

本文结论依据 / 出处本文位置
RAG 召回不佳常源于 query 与文档表述不一致,而非分块策略待验证(行业实践共识,无一手出处)第一章
多查询展开通过多角度改写提升召回覆盖待验证(相关查询改写研究,具体论文与年份未核验)第二章
HyDE 用假设答案的向量去检索,使 query 向量更贴近文档Gao 等《Precise Zero-Shot Dense Retrieval without Relevance Labels》(发表年份与会议待验证)第三章
多轮对话中的指代省略会降低可检索性,需上下文补全待验证第四章
用标注 query 集计算 recall@K 可量化改写前后收益待验证(通用检索评测方法)第五章
query 已规范时改写可能引入噪声、降低精度待验证(基于本文实验判据的推论)第六、七章

附表 B:术语速查表

术语含义
RAG检索增强生成,先检索相关文档再让模型生成答案
查询改写把用户原始 query 改写成更易被检索命中的形式
multi-query多查询展开,把一题拆成多个角度分别检索再合并
HyDE假设性文档嵌入,先生成假设答案再用其向量去检索
指代消解把"它""这个方法"等省略指代补全为完整表述
召回率 / recall@K前 K 篇检索结果中命中相关文档的比例
基线召回未做改写时的原始召回水平,作为对比基准

写在最后:这篇用到的资料

写这篇文章时,我把 RAG 查询改写的三种策略伪代码、召回对比评测模板都整理了一遍,顺手也整理了几份配套的东西:

  • 查询改写策略集:multi-query、HyDE、指代补全的可改代码。
  • 召回对比评测模板:改写前后命中率怎么量化。
  • AI 大模型学习路线:从应用落地到工程化的系统路径。

资料是我自己整理的,放在下面这个码上,扫码即可获取:



资料较多,建议先看「全套 AGI 大模型学习路线」,再挑一个实战项目跟练。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询