RAG 做不好?八成是用户问得太烂了——查询改写实战
前阵子帮一个客户优化他们的 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_versions3):promptf 用户查询{query}请从以下三个不同角度各生成一个改写后的查询 1. 最完整版补充所有隐含信息 2. 最简洁版保留核心关键词 3. 同义替换版使用不同的表达方式 输出格式 1. [完整版] 2. [简洁版] 3. [同义版] responsellm.invoke(prompt)returnparse_versions(response)融合策略多路检索结果先用 Reciprocal Rank FusionRRF合并排序再用一个轻量级 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}]responsellm.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 系统有做查询改写吗用的什么方案评论区聊聊。