第25回 高级 RAG(上)——查询变换与 HyDE
问得不清难得真,先把问题磨成刃。
一纸搜遍千卷书,关键还在提问人。
上回讲评估,看官应当明白:
很多 RAG 翻车,不是检索器太弱,而是“问法太糟”。
你去书库问一句“给我讲讲强化学习”,
那检索器只能回你一堆“看起来都相关”的段落。
可你若问“强化学习里探索与利用怎么权衡?给一个可计算例子”,
证据就好找得多。
所以高级 RAG 的第一刀,不砍检索器,先砍“查询”。
这一回讲两类常用招式:
- 查询改写(Rewrite):把口语问题改成更像文档的检索句
- HyDE:先“生成一段假答案/假文档”,再拿它去检索
一、查询变换为什么有效:让问题长得像证据
资料库里的句子往往像“陈述句”:
- “余弦相似度衡量向量夹角”
- “CRAG 讨论如何纠错检索”
但用户的问题往往像“口语句”:
- “余弦相似度是啥意思?”
- “检索错了怎么办?”
查询变换做的事就是:
把口语问题变成更像资料里的关键词与表达。
常见四种变换:
- 重写(Rewrite):同义改写,补齐关键词
- 分解(Decompose):把复合问题拆成子问题
- Step-back:先问一个更一般的“背景问题”,再回到细节
- 扩展(Expand):加上同义词、缩写、别名
看官若问“这算不算作弊”?
不算。
这是承认一个事实:
检索是匹配系统,匹配就得讲匹配的语言。
二、HyDE:先写一段“可能的答案”,再用它当检索钥匙
HyDE 的直觉更有趣:
你先让模型写一段“假答案/假文档”,
这段文字往往包含更丰富的关键词与语义线索,
再用它去检索,就更容易召回真正相关的文档。
这招像什么?
像你做题先写“草稿版解法”,
草稿里会出现你需要的定义、公式、关键术语,
然后你拿着这些术语去翻书,就更容易翻到对的页。
HyDE 最早作为一个检索增强技巧被提出并广泛引用;
在 2024 的 RAG 综述与“预检索阶段(pre-retrieval)”分类中,它也常作为典型方法被讨论。12
三、查询变换也会翻车:改写错了,比不改更糟
查询变换的风险主要有两类:
- 语义漂移:把用户意图改偏了
- 注入噪声:加了太多无关词,检索更发散
因此工程里常见一个守门策略:
改写不止一版,而是生成多条候选查询,然后检索合并或重排。
这就是把第19回“预算换能力”的思路搬到检索前:
多试几种问法,选最能召回证据的那种。
四、极简可跑代码:用“多查询 + 投票”提升召回
我们继续用第23回的玩具检索器(词袋+余弦),
这次加上查询变换:
- 原问题
q - 重写版
q_rewrite(我们用规则模拟) - HyDE 版
q_hypo(用模板生成“假答案”)
对每个查询检索 top-k,然后把证据按“出现次数”投票排序。
import math
import re
DOCS = [
"HyDE 的思路是先生成一个假文档或假答案,再用它去检索真实证据。",
"查询改写可以把口语问题变成更像文档的关键词表达,从而提升召回。",
"RAG 评估要拆开看:检索质量与生成质量是两本账。",
"长上下文并不必然取代 RAG:检索可以节省成本并提升可控性。",
"Corrective RAG 讨论检索错了如何纠错与再检索。",
]
def tokenize(s):
return [w for w in re.split(r"[^0-9A-Za-z\u4e00-\u9fff]+", s) if w]
def bow(tokens):
v = {}
for t in tokens:
v[t] = v.get(t, 0) + 1
return v
def dot(a, b):
return sum(a.get(k, 0) * b.get(k, 0) for k in a.keys())
def norm(a):
return math.sqrt(sum(x * x for x in a.values()))
def cosine(a, b):
na = norm(a)
nb = norm(b)
if na == 0 or nb == 0:
return 0.0
return dot(a, b) / (na * nb)
def retrieve(q, k=2):
qv = bow(tokenize(q))
scored = []
for i, d in enumerate(DOCS):
scored.append((cosine(qv, bow(tokenize(d))), i))
scored.sort(reverse=True)
return [i for _, i in scored[:k]]
def rewrite_query(q):
q = q.strip()
q = q.replace("啥", "是什么").replace("怎么办", "如何处理")
if "检索" in q and "错" in q:
return q + " 纠错 再检索"
return q
def hyde_query(q):
return "可能的回答:这问题涉及 " + q.strip() + " 的定义、动机、步骤与常见失败模式。"
def multi_query_vote(q, k=2):
qs = [q, rewrite_query(q), hyde_query(q)]
votes = {}
for qq in qs:
for idx in retrieve(qq, k=k):
votes[idx] = votes.get(idx, 0) + 1
ranked = sorted(votes.items(), key=lambda x: (-x[1], x[0]))
return [i for i, _ in ranked]
if __name__ == "__main__":
q = "HyDE 是啥?"
print("single:", [DOCS[i] for i in retrieve(q, k=2)])
print("multi :", [DOCS[i] for i in multi_query_vote(q, k=3)[:2]])
你会发现:
原查询太短时,单次检索容易飘;
多查询投票后,命中 HyDE 相关句子的概率更高。
这就是高级 RAG 的常识:
别把检索当成“一锤子买卖”,把它当成可搜索的过程。
五、小结:高级 RAG 先修“问法”,再修“武器”
这一回你要记住三点:
- 查询变换让问题更像证据,从而提升召回
- HyDE 用“假答案/假文档”当钥匙,补足语义线索
- 变换也会漂移,所以要多候选、多检索、再汇总
下一回(第26回)我们继续升级“找”这一环:
只靠向量检索会漏关键词,只靠 BM25 会漏语义。
那就两者都用,再加一层重排,让证据更准。
欲知后事如何,且听下回分解。
幻觉核查
- HyDE 的方法描述与定位:可核对其论文标题与摘要对“hypothetical document embeddings”的表述。2
- HyDE 作为预检索技巧的分类:可核对 RAG 文本生成综述对 pre-retrieval 方法的归纳。1
- 本回代码中的改写与 HyDE 为规则/模板模拟,用于说明流程,不代表真实 LLM 改写质量。
逻辑审计
- 与第24回衔接:评估揭示“问法”会影响召回,本回以查询变换直接提升检索栏指标。
- 与导读一致:导读强调“办案式检索循环”,本回把循环前置到“提问方式”,使检索更可控。
- 为后续铺路:第26回把检索信号做混合与重排;第27回把查询变换与重排纳入模块化架构,进入可训练与可路由的系统。
引用与溯源
Footnotes
-
Huang, Y., et al. A Survey on Retrieval-Augmented Text Generation for Large Language Models arXiv:2404.10981 (v2: 2024-08-23) https://arxiv.org/abs/2404.10981 ↩ ↩2
-
Gao, L., et al. Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE; 2022) https://arxiv.org/abs/2212.10496 ↩ ↩2