RAG 查询改写笔记:不是资料找不到,而是问题没问对

这篇主要讲什么

这一份 PDF 对应 Part 5 ~ Part 9,集中讨论一件事:

用户原始问题,不一定是最适合检索的问题。

也就是说,很多 RAG 失败,并不是知识库里没有答案,而是检索时用的提问方式太单一、太窄、太贴表面。

这一组方法都可以归到一个大方向里:查询改写。

为什么要改写问题

因为向量检索虽然能理解一部分语义,但它也不是万能的。

用户一句话里可能会有这些问题:

  • 说法太口语
  • 问题太短
  • 问题太复杂,里面混了好几个子问题
  • 用了知识库里不常见的表达

这时如果只拿原问题去搜,很容易漏掉真正相关的内容。

我自己的感觉是,查询改写就像给搜索动作加了一个“思考前置步骤”。

不是急着搜,而是先想一想:这个问题换个说法,会不会更容易搜到东西?

1. Multi Query:同一个问题,多问几遍

这个方法的意思很简单:

让模型把原问题改写成多个不同版本,然后分别检索,最后把结果合并。

比如你问:

什么是 LLM agent 的任务分解?

模型可能会改写成:

  • 任务分解在智能体里起什么作用
  • LLM agent 如何把复杂任务拆成小步骤
  • autonomous agent 的 planning 与 task decomposition 有什么关系

这样做的好处是:

一个说法没搜到,另一个说法可能能搜到。

我觉得这是非常实用的一招,因为用户提问经常是不标准的,而知识库里的表达方式未必和用户完全一致。

2. RAG-Fusion:不只多搜,还要重新排序

它和 Multi Query 很像,也是先生成多个搜索问题。

但不同点在于:它不只是把结果简单拼起来,而是做一次融合排序。

原文里用了 Reciprocal Rank Fusion。不用记名字,理解它的目的就行:

如果某一段内容,在多个查询结果里都排得比较靠前,那它更可能是真的重要。

我对这个方法的评价是:

它比简单去重更靠谱,因为它考虑了“多次出现而且排名还不错”这个信号。

通俗一点说,就是把多次被不同问法都命中的内容,优先抬上来。

3. Decomposition:先把大问题拆小

这个方法特别适合复杂问题。

比如一个问题里同时在问:

  • 系统由哪些部分组成
  • 每个部分怎么协作
  • 为什么这样设计

如果一次性去搜,检索器很容易抓不住重点。

所以更好的做法是:

先让模型把大问题拆成几个子问题,再分别检索和回答,最后合并。

我个人很认同这种思路,因为复杂问题本来就不该“一把梭”。

这其实是在模仿人类处理难题的方式:先拆,再逐个解决,再汇总。

4. Step Back:往上退一步,先问更一般的问题

这个方法很有意思。

有时候用户的问题太具体,反而不好搜。于是先把问题“退一步”,改成一个更泛化、更抽象的问题,再拿它去找背景知识。

比如原问题很窄,但退一步后,能先找到更稳的基础概念。

我觉得这特别像学习时的一个动作:

先别急着抠细节,先问“这件事本质上属于哪一类问题”。

这样检索出来的资料,往往更适合补背景。

5. HyDE:先假设一篇答案,再拿这篇答案去搜

HyDE 的思路很“反直觉”,但很巧。

它不是直接拿用户问题去检索,而是先让模型根据问题写一段“假想答案”,再用这段假想答案去做检索。

为什么这样可能有效?

因为用户问题通常很短,但一段假想答案会自然带出更多相关术语和表达方式,更像目标文档的样子。

这样一来,检索更容易命中真正相关的内容。

我自己的看法是:

HyDE 很像“先脑补一版可能的标准答案,再拿这版答案去搜资料”。

这个方法挺聪明,但也有风险。如果模型先脑补错方向,后面的检索也可能被带偏。所以它适合做增强,不适合盲信。

这份内容的核心思想

我觉得这一整份最核心的想法是:

检索效果差,很多时候不是库不行,而是提问方式不行。

所以在“搜”之前,先对问题做加工,本身就是 RAG 的重要一环。

我自己的理解

我现在会把查询改写看成 RAG 里的“搜索策略层”。

基础 RAG 更像:

有问题就直接搜。

而这一组方法更像:

先判断怎么搜最合适,再去搜。

这说明 RAG 不只是“检索 + 生成”,中间其实还有一个很值得认真设计的环节:怎么理解用户的问题。

什么时候用哪一种

  • 问题表达可能有很多种说法:优先用 Multi Query
  • 想把多次检索结果整合得更稳:用 RAG-Fusion
  • 问题本身很复杂:用 Decomposition
  • 问题过于具体,缺背景:用 Step Back
  • 原问题太短,检索命中太弱:可以试 HyDE

一句总结

这一篇让我最有感触的一点是:RAG 不只是“去找文档”,而是“先把问题变成更适合找文档的样子”。很多效果提升,不在模型后面,而在检索前面。

核心代码实现

方法 1:Multi Query

1
2
3
4
5
6
7
8
9
10
11
def generate_multi_queries(question):
prompt = f"把这个问题改写成 5 个不同问法:{question}"
text = llm.invoke(prompt)
return text.split("\n")

def multi_query_retrieve(question):
queries = generate_multi_queries(question)
all_docs = []
for q in queries:
all_docs.extend(retriever.invoke(q))
return unique(all_docs)

方法 2:RAG-Fusion

1
2
3
4
5
6
7
8
9
10
11
12
def reciprocal_rank_fusion(results, k=60):
scores = {}
for docs in results:
for rank, doc in enumerate(docs):
key = doc.id
scores[key] = scores.get(key, 0) + 1 / (rank + k)
return sort_by_score(scores)

def rag_fusion_retrieve(question):
queries = generate_multi_queries(question)
results = [retriever.invoke(q) for q in queries]
return reciprocal_rank_fusion(results)

方法 3:Decomposition

1
2
3
4
5
6
7
8
9
10
11
12
13
def decompose_question(question):
prompt = f"把这个复杂问题拆成 3 个子问题:{question}"
text = llm.invoke(prompt)
return text.split("\n")

def decomposition_answer(question):
sub_questions = decompose_question(question)
qa_pairs = []
for sub_q in sub_questions:
docs = retriever.invoke(sub_q)
ans = llm.invoke(build_prompt(sub_q, docs))
qa_pairs.append((sub_q, ans))
return llm.invoke(build_final_summary_prompt(question, qa_pairs))

方法 4:Step Back

1
2
3
4
5
6
7
8
9
def step_back_question(question):
prompt = f"把这个问题改写成一个更通用、更容易检索的背景问题:{question}"
return llm.invoke(prompt)

def step_back_retrieve(question):
normal_docs = retriever.invoke(question)
generic_q = step_back_question(question)
step_back_docs = retriever.invoke(generic_q)
return normal_docs + step_back_docs

方法 5:HyDE

1
2
3
4
5
6
def hyde_retrieve(question):
fake_doc = llm.invoke(
f"请先写一段像参考答案一样的短文,用来回答这个问题:{question}"
)
docs = retriever.invoke(fake_doc)
return docs