前阵子帮一个客户优化他们的 RAG 系统,折腾了两周,效果就是提不上去。
数据拆了又拆,chunk size 调了 6 种组合,embedding 模型换了 3 个,reranker 也加了——到头来提升不到 5%。我差点以为这项目要砸手里了。
后来一个偶然的机会,我翻了翻日志里的用户原始查询,发现了一个惊人的事实:
绝大多数用户问的问题,直接拿去向量检索,根本找不到正确的内容。
不是系统不行,是"人和机器之间的语言鸿沟"太宽了。
今天就来聊聊 RAG 系统里最容易被忽略、但性价比最高的一环——查询改写(Query Rewriting)。
问题到底出在哪
先给你看几个真实日志里的查询:
- “那个会画图的模型是什么来着”
- “前两天出的那篇讲 RAG 的文章”
- “怎么做”
- “之前说过的那个方案”
你看,凭这些问题,你让人来答都答不明白,何况是向量检索?
向量检索干的本质是语义匹配。它把你的查询向量化,跟库里的文档向量做相似度比较。问题越具体,匹配越准;问题越模糊,匹配越像在抽奖。
但用户不是故意的。他们来问问题的时候,脑子里已经有个上下文了。比如《那个会画图的模型是什么来着》——他可能上周刚看完一篇关于 DALL-E 3 的文章,所以觉得"会画图的模型"就够了。但对检索系统来说,这跟大海捞针差不多。
所以查询改写要做的事情就是:把用户模糊的、上下文依赖的、口语化的查询,翻译成检索系统能理解的具体、独立、明确的查询。
我试过的 3 种查询改写方案
花了大概一周时间,试了 3 种不同的方案,记录一下效果。
方案一:LLM 直接改写(最粗暴)
最简单的做法:把用户的原始查询扔给 LLM,让它"把问题写得更具体一些"。
Prompt 模板:
你是一个查询改写助手。用户提了一个问题,它可能很模糊或不完整。 请把它改写成适合搜索引擎使用的、具体的、独立的问题。 只输出改写后的问题,不要解释。 用户查询:{query} 改写结果:实际效果:
好处是快,一个 LLM 调用就搞定,延迟也就几百毫秒。
但问题很快就暴露了——
LLM 改写有个毛病:它会把问题改写得太"完美"了。
举个例子,用户查"怎么做",LLM 改写成"如何实现 XXX 功能"。看起来没什么问题对吧?但实际上,用户问"怎么做"的时候,他心里想的可能是"怎么安装"、“怎么配置”、“怎么调试”——不同用户心里的"做"是完全不同的概念。LLM 自作主张帮你扩写了,反而可能把检索方向带偏。
我管这个叫"好心办坏事型的过度扩写"。
测试 100 条查询后,有效提升:约 15%。
有提升,但不够。
方案二:多查询扩写 + 融合搜索(推荐)
这个方法是我在一个韩国的 RAG 论文里看到的,实测效果最好。
思路很简单:不要只改写一个版本,而是生成多个不同的改写版本,分别去检索,然后把结果融合。
具体做法:
defrewrite_queries(query,llm,num_versions=3):prompt=f""" 用户查询:{query}请从以下三个不同角度各生成一个改写后的查询: 1. 最完整版:补充所有隐含信息 2. 最简洁版:保留核心关键词 3. 同义替换版:使用不同的表达方式 输出格式: 1. [完整版] 2. [简洁版] 3. [同义版] """response=llm.invoke(prompt)returnparse_versions(response)融合策略:
多路检索结果先用 Reciprocal Rank Fusion(RRF)合并排序,再用一个轻量级 reranker 重排。
这么做的道理是什么?
用户的原始查询可能颗粒度不匹配,但三个不同的改写版本,理论上总有一个能"碰"到正确的文档。RRF 融合再把这些命中的文档提到前面来。
实测效果:
- 改写 3 个版本 + RRF 融合:召回率提升约 35%
- 加 reranker 重排后:首条命中率提升约 42%
这是我测试出来的性价比最高的方案。不需要换 embedding 模型,不需要调整索引策略,就加一个查询改写层,召回率直接拉上去。
代价:
- 多了一次 LLM 调用(生成本不高,3 个版本一次生成就好了)
- 检索次数变成了 3 倍(可以用异步并行,延迟基本持平)
- RRF 计算几乎零开销
这个方案后来我在线上跑了两个星期,效果稳定。
方案三:上下文感知改写(最精细)
这个方案更激进一些——如果 RAG 系统的对话里有历史消息,可以利用聊天历史来帮助改写。
举个例子,如果用户说:
历史:用户问"你们公司今年有什么新产品"
历史:助手答"我们推出了 GPT-Image 2,可以 AI 生图…"
当前:用户问"多少钱"
这时候,如果只看当前查询"多少钱",根本无法检索。但如果结合历史,改写结果应该是"GPT-Image 2 的 API 定价是多少钱"。
做法:
把最近 3-5 轮对话历史 + 当前问题一起发给 LLM,让它生成一个"自包含"的查询。
defcontext_aware_rewrite(query,history,llm):messages=[{"role":"system","content":"请根据对话历史,将用户最新问题改写为不依赖上下文的独立查询。"},*histo ry[-4:],# 最近 4 轮{"role":"user","content":query}]response=llm.invoke(messages)returnresponse.content效果:在多轮对话场景下,准确率比直接改写提升了约 20%。
代价:需要维护对话历史,prompt 更长,每个查询多消耗一些 token。
我最终推荐的方案
如果你在看这篇文章,想在自己的 RAG 系统里加上查询改写,我建议你这样做:
第一步:先做多查询扩写 + RRF 融合(方案二)
这个改造成本最低。你只需要在检索前加一层改写逻辑,然后把索引逻辑从"一次检索"改成"三次检索 + 一次 RRF 融合"。改动量大约 100 行代码。
第二步:如果有多轮对话场景,加上下文感知改写(方案三)
这个也不复杂,主要是改 prompt。
第三步:不要只依赖 LLM 改写
我发现很多人做查询改写,直接把用户问题扔给 GPT 然后取结果,以为就完事了。这样效果其实一般。好的查询改写应该和目标检索系统配合。比如你的检索系统是做稀疏检索(BM25)的,改写方向应该是关键词补充;做稠密检索(向量)的,改写方向应该是语义扩写。
踩坑记录
最后分享几个我亲身踩过的坑:
坑 1:改写后的查询太长。有一次 LLM 把一句 “怎么用” 改写成了 “如何在 Python 环境中使用 LangChain 框架的 Agent 模块来构建一个能够调用外部 API 的智能助手”。全长 50 个字。当这个查询喂给向量检索时,因为噪声太多,匹配结果反而更差了。
解决:在 prompt 里明确限制改写后的查询长度不超过 20 个字。
坑 2:过度依赖改写。有些查询本身已经很明确了(比如"LangChain 的 AgentExecutor 源码分析"),不需要改写。不改写反而更好。所以我加了一个简单的检测:如果原始查询已经包含 4 个以上的实体关键词,就不做改写,直接检索。
结果:这个"跳过改写"逻辑让整体准确率又提升了 8%。
坑 3:改写后丢失专有名词。有一次用户查的是"ChatGPT 的 System Prompt 长度限制",LLM 改写后变成了"大语言模型的提示长度限制"。虽然意思相近,但"System Prompt"这个专有名词被泛化了,导致检索不到相关文档。
解决:在 prompt 里强调"保留所有专有名词、产品名、技术术语不修改"。
写在最后
折腾了这么一圈,我最大的感受是:很多人花大钱买贵的 embedding 模型、搭复杂的索引架构,却在最基础的"用户问的问题本身就没写对"这个环节上翻了车。
查询改写是 RAG 系统里投入产出比最高的优化点之一。不夸张地说,加一层查询改写,比换一个更贵的 embedding 模型带来的提升大得多。
你现在的 RAG 系统有做查询改写吗?用的什么方案?评论区聊聊。