老周是市图书馆的一位金牌管理员。读者来还书时,常常只丢下一句模糊的话:“我想找本讲AI但别太深的书”“有没有讲机器学习和团队管理的?”这些话要是直接拿去书库的索引系统里查,多半碰壁——索引里写的是“人工智能”“计算机科学”“组织行为学”,和读者嘴里的"AI"“管理”对不上号。老周的经验是:先别急着进书架,先把读者的话在脑子里翻译、拆分、补充一遍。这套“在检索之前做的加工”,在RAG检索增强生成里有一个专门的名字:查询改写(Query Rewriting)。它让原本只听字面的检索系统,听得懂用户真正的潜台词——检索命中率上去了,最终回答质量自然也就提升了。
对AI从业者来说,掌握查询改写意味着你的RAG系统从“听字面”升级为“听懂人话”,在检索命中率和最终回答质量上往往有立竿见影的提升。
可视化演示
展示微服务架构中API网关转发请求、服务发现注册中心维护服务实例列表、负载均衡器分发流量到多个副本的完整流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
服务发现流程 网关从注册中心获取服务实例列表 动态路由请求到健康实例 GW→SD→Svc 服务注册与发现机制
改写必要性
在图书馆的老周看来,读者说的每一句话都有“潜台词”。“讲AI但别太深”翻译过来是“人工智能领域的入门读物”,“讲机器学习的书”可能是“机器学习实践案例”。如果老周不翻译,让读者直接去按字面找,多半空手而归。原因很简单:读者用的是口语,书库索引用的是书面语,两边说的不是同一种语言。
RAG系统也面临同样的困境。用户输入的自然语言查询经过嵌入模型变成向量,与文档向量做相似度比较。可当用户说“AI“而文档里写”人工智能“时,两个向量可能相隔很远,检索模块就会漏掉真正相关的文档。这种”用户语言“与”文档语言”之间的错位,专业上叫词汇鸿沟(lexical gap)。查询改写,就是在这道沟上搭一座桥。
更形式化地看,改写是找一个更可能命中目标文档的查询表达:
老周的翻译不是闲聊,而是让读者的话“对齐”到书库的语言体系。查询改写做的正是这件事:在把请求交给检索系统之前,先把话说成文档听得懂的样子。
- 查询表达$q'$ 表示改写后的候选查询,它来自对原始问题的同义替换、拆分或扩展。
- 目标文档$d^*$ 是用户真正想找的那篇文档,改写时并不知道它的存在,只能通过相似度去逼近。
- 最大化相似度改写就是要让查询表达尽可能靠近目标文档的语义空间。
同义替换扩写
如果读者说“我想找本电脑书”,老周不会只盯“电脑”两个字,而是在心里补出“计算机、PC、微机、计算器原理”这些近义词,再去书架上比对。这种“一个词变出一串词”的做法,就是查询改写里最基础的同义替换扩写。
技术实现上,同义替换可以靠人工维护的同义词表、词向量近邻,或者让语言模型生成多个同义表达。得到若干个候选词后,把它们加进原始查询中,形成一个“扩展查询”。扩展后的查询有时不止一个,而是多个候选查询并行去检索,最后把结果合并。为了让核心语义不跑偏,通常会给不同词分配权重,原词权重最高,扩展词次之。
1. 原查询权重:$\alpha$ 是原始查询在最终向量里的占比,取较大值以保留用户本意。
2. 同义扩展项:对每个同义词 $w_i$ 取嵌入向量后求和,用一个向量代表整组近义表达。
3. 归一化约束:$\alpha+\beta=1$ 保证扩展向量的尺度不会失控。
老周在做同义替换时,会保留读者原话里最核心的词,再往外扩展。同义替换扩写也一样:原词为主、近义为辅,既扩大了命中面,又不至于让查询变成一锅乱炖。
查询分解
有时候读者的问题不是“一个词”的问题,而是“好几个问题”打包在一起:“我想找讲机器学习怎么入门、还要能用在团队管理上的书。”老周一听就知道,这不是一本书能解决的,于是把它拆成两路:“机器学习入门”和“团队管理方法”,分头去找再汇总。
这就是查询分解(Query Decomposition)。当用户的问题包含多个独立子意图时,直接用一个完整问题去检索,向量会被拉扯到两个语义空间的中间地带,哪个都靠不近。更聪明的做法是让大模型把问题拆成几个子问题,每个子问题单独走一遍检索,最后把多路检索结果合并去重。
合并的常见策略是取并集:按各子查询的相关性分数保留Top-k,再对重叠文档取最高分或加权平均。这样既保证每个子意图都有覆盖,又不会因为某个子查询太强而淹没另一个子意图。
老周拆完问题后,还会在汇总时把重复推荐给读者的书合并成一条。查询分解的并集操作也一样——既要全,又要去重,让最终进入生成模块的文档清单干净且完整。
- 子问题集合$q_1, q_2, \dots, q_m$ 是从原始复合问题中拆分出的 $m$ 个独立子查询。
- 单路检索每个子查询 $q_k$ 独立执行检索,取相关性最高的前 $k$ 篇文档。
- 并集合并对多路结果取并集,文档重复出现时保留最高分。
HyDE假设文档
有一次,读者说:“我想找一本像《人类简史》那样、从一个宏大视角讲技术发展的书。”老周没有直接搜“宏大视角”,而是先在心里想象:“如果我给这么一本书写内容简介,它会是什么样?”然后拿着这段“想象出来的简介”去和书架上的书比对。这个做法,在查询改写里叫HyDE(Hypothetical Document Embedding,假设文档嵌入)。
HyDE的核心思路是:与其拿一句简短的查询去匹配长文档,不如让语言模型先“脑补”一篇针对查询的虚构文档——一段像模像样的段落。这段假设文档表达了用户期望在真实文档中看到的内容,把它做嵌入后,再去与库中真实文档的向量算相似度。短查询与长文档之间往往存在长度和抽象级别的错位,而“文档对文档”的匹配天然更对齐。
1. 假设文档:$h$ 是LLM根据查询生成的虚拟文档,包含用户可能想要的定义、概念和表述。
2. 真实文档:$d$ 是待检索库中的候选文档。
3. 余弦匹配:对 $h$ 和 $d$ 分别做嵌入,再计算余弦相似度作为相关性分数。
老周“想象出的简介”不是真的书,却帮他准确锁定了书架上的候选。HyDE的假设文档也不是答案,它只是探路的引子——真实答案永远来自库中检索出的真实文档。
策略对比选型
面对不同的读者,老周的应对也不同:能直接说出书名的,二话不说去取书;问题里藏了两个意图的,拆开找;只描述感觉、说不清书名的,才动用“想象法”。没有哪个方法能包打天下,查询改写也一样。
三种主流策略各自对应一类失败模式:同义替换扩写解决词汇不一致;查询分解解决复合意图;HyDE解决查询太简短、信息量不足。在实际RAG系统中,三种方法也可以组合使用,给不同策略分配权重。组合检索分数就是各路分数的加权和:
老周会根据读者的表达方式灵活切换策略,而不是每次都把三种方法都上一遍。工程上也一样:先分析检索失败的类型,再有针对性地选择改写策略,能让效果和成本都更可控。
- 三路分数$\operatorname{score}_{\text{syn}}$、$\operatorname{score}_{\text{sub}}$、$\operatorname{score}_{\text{hyde}}$ 分别代表三种改写策略对某篇文档的匹配分。
- 权重分配$\lambda_1,\lambda_2,\lambda_3$ 是三个策略的可调权重,体现业务对每种策略的信任度。
- 加权求和将三路分数归一化后相加,得到文档的最终排序依据。
效果评估方法
老周怎么知道自己翻译得好不好?他会悄悄记下读者有没有借到满意的书,借出去了还会不会再来问。如果“翻译后找到书的成功率”明显高于“直接带读者去书架”,说明翻译有效。
改写效果评估也分三个层次。第一层是无参考评估:不依赖人工标注,直接对比改写前后检索结果的命中率、MRR等指标有没有提升。第二层是有参考评估:准备一批标准查询-文档对,用改写后的查询去检索,看目标文档是否排进前几位。第三层是端到端评估:把改写放进完整RAG流程里,让LLM对最终生成答案打分。其中MRR是一个很常用的检索排序指标:
老周记录的是“读者最终满意而归”的比例,而不是“推荐了多少本书”。评估改写也一样,要看它是否真正提升了检索和回答质量,而不是只看改写本身花哨不花哨。
- 查询集合$\mathcal{Q}$ 是用于评估的查询集合,$|\mathcal{Q}|$ 表示查询总数。
- 首个正确答案的排位$\operatorname{rank}_q$ 是查询 $q$ 检索结果中第一个相关文档的排名。
- 平均倒数排名对每个查询取排名的倒数再求平均,排名第1得满分1。
RAG流程定位
老周的位置很有意思——他不在书库里,也不在借书台,他站在两者之间。读者说出需求后,他会先做翻译,再把人领进书架。这个“中间的过道”,就是查询改写在整个RAG流程里的位置:检索前的预处理阶段。
完整流程是:用户查询先进入改写模块,做同义替换、拆解或假设文档生成;改写结果交给嵌入模型转成向量;向量在文档库里检索出候选文档;候选文档再经过重排序精排;最后由大模型依据精排后的文档生成回答。改写不替代向量检索,也不替代重排序,它只负责让后续每一步都站在更牢靠的起点上。
老周从不亲自写书,他只是让“找书”这件事变得更顺。RAG里的改写模块也一样,它不产出答案,却决定了答案能站在多好的文档基础之上——位置在检索之前,价值贯穿整个链路。
- 改写函数$\operatorname{Rewrite}(q)$ 对原始查询做预处理,输出一个或一组改写后的查询。
- 检索与重排$\operatorname{Retrieve}$ 负责召回候选文档,$\operatorname{Rerank}$ 负责精排。
- 生成答案大模型基于精排后的文档生成最终回答。
工程落地实践
老周不是对每个读者都做全套翻译。有人直接报出书名,他就指路;有人描述含糊,他才追问几个问题。改写也要讲性价比:不是所有查询都值得改写,简单精确的查询直接检索更快。
工程上的关键是“按需触发”:先判断查询是否模糊、是否含领域别名、是否包含复合意图,只有命中这些条件才启动改写。改写好的结果可以放进缓存,相同的模糊问题再次出现时直接复用。改写模型也不必都上大模型,量小场景用同义词表,量大场景再考虑LLM。是否改写的判断可以写成一个延迟收益权衡:
老周的经验是:把精力花在最容易出错、最值得翻译的读者身上。查询改写落地也是这个道理——在正确时机做正确加工,用最小的额外延迟换最大的检索收益,才能让RAG系统真正“听懂用户”。
- 收益项$\Delta \text{hit}$ 是预期命中率提升,$p_{\text{improve}}$ 是本次查询落到可提升场景的概率。
- 成本项$t_{\text{rewrite}}$ 是改写消耗的时间,$c$ 是单位时间成本。
- 决策条件收益大于成本则改写,否则直通,以最小代价换取最大检索收益。
AI 应用场景
企业知识库客服助手
阶段:检索前预处理
解决的问题:员工在客服系统里提问口语化严重,比如“报销单老被打回”,而知识库文档里写的是“费用报销流程规范”“单据审核不通过原因”。直接向量检索命中率低,客服机器人只能答非所问。通过查询改写把口语问题翻译成知识库风格的语言,再进入向量检索,命中率大幅提升。
具体案例:某企业上线RAG智能客服后,将“报销单老被打回”改写为“报销单审核失败 原因 材料要求”,检索命中率从38%提升到81%,机器人一次解决率提高两倍。遇到“请假和报销流程”这种复合问题,查询分解模块自动拆成两个子查询并行检索。
法律政策文档问答系统
阶段:检索策略优化
解决的问题:法律条文和政策文件中充满规范术语,用户常用日常表达提问,例如“离职后社保怎么办”对应文档里的“社会保险关系转移接续”。词汇鸿沟导致检索召回不足,HyDE假设文档方法可以有效补全术语。
具体案例:某政务问答系统使用HyDE:大模型先根据“离职后社保怎么办”生成一段假设性的政策解答文案,再用这段文案的嵌入向量去匹配真实政策文件,准确找到《社会保险法》相关条款。相比直接查询,目标条款进入Top-5的概率提升了45%。
医疗科普问答助手
阶段:查询理解与检索前处理
解决的问题:患者描述症状时很不精确,如“心脏不舒服”可能对应“心悸”“胸闷”“心律失常”多个医学概念。单一查询无法覆盖多个潜在语义,需要同义替换扩写来扩大召回面。
具体案例:某医院科普问答小程序将“心脏不舒服”扩展为“心悸 胸闷 心律失常 心脏不适”,多路扩展查询并行检索,同时用查询分解区分“症状描述”和“就医建议”两类子问题,最终回答的相关性评分从2.1提升到4.3(满分5)。
Python 演示
# -*- coding: utf-8 -*-"""Query Rewriting demo: show how rewriting improves retrieval hit."""import mathimport re def tokenize(text): return re.findall(r"[\u4e00-\u9fa5a-zA-Z0-9]+", text.lower()) def vectorize(text, vocab): vec = [0] * len(vocab) for w in tokenize(text): if w in vocab: vec[vocab[w]] += 1 return vec def cosine(a, b): dot = sum(x * y for x, y in zip(a, b)) na = math.sqrt(sum(x * x for x in a)) nb = math.sqrt(sum(y * y for y in b)) if na == 0 or nb == 0: return 0.0 return dot / (na * nb) def search(query, docs, vocab): qv = vectorize(query, vocab) result = [] for doc in docs: result.append((cosine(qv, vectorize(doc, vocab)), doc)) return sorted(result, reverse=True) def rewrite_with_synonyms(query): table = { "AI": "人工智能 机器学习", "电脑": "计算机", "报销": "费用报销 单据审核" } words = query.split() new_words = [] for w in words: new_words.append(table.get(w, w)) return " ".join(new_words) def decompose_query(query): return [part.strip() for part in query.split(" ") if part] def hyde_expand(query): return query + " 我们关注其定义 原理 应用场景 以及清晰的入门解释" def build_vocab(docs): vocab = {} for doc in docs: for w in tokenize(doc): if w not in vocab: vocab[w] = len(vocab) return vocab def main(): docs = [ "人工智能是研究如何让机器具备人类智能的学科 包括机器学习与深度学习", "计算机视觉是让程序理解图像内容的技术 用于人脸识别和自动驾驶", "团队管理者需要绩效评估 目标拆解 以及沟通技巧等软技能" ] vocab = build_vocab(docs) q = "AI 入门" print("原始查询:", q) for score, doc in search(q, docs, vocab): print(" %.3f %s" % (score, doc)) rw = rewrite_with_synonyms(q) print("同义改写:", rw) for score, doc in search(rw, docs, vocab): print(" %.3f %s" % (score, doc)) subs = decompose_query(q) print("查询分解:", subs) merged = {} for sub in subs: for score, doc in search(sub, docs, vocab): merged[doc] = max(merged.get(doc, 0.0), score) for doc, score in sorted(merged.items(), key=lambda kv: kv[1], reverse=True): print(" %.3f %s" % (score, doc)) hy = hyde_expand(q) print("HyDE假设文档:", hy) for score, doc in search(hy, docs, vocab): print(" %.3f %s" % (score, doc)) if __name__ == "__main__": main()
def rewrite_with_synonyms(query):定义同义替换改写函数,通过同义词表把口语词翻译成文档常用词。table.get(w, w)查表替换:词在表中就换,不在表中保持原词,避免误改。def decompose_query(query):将复合查询按空格拆成多个子查询,模拟查询分解模块。def hyde_expand(query):用模板句式生成一段假设文档,模拟HyDE的“脑补段落”。def search(query, docs, vocab):查询入口:对查询和每篇文档做向量化并计算余弦相似度。result.append((cosine(qv, vectorize(doc, vocab)), doc))核心评分:计算查询与文档的相似度并保存。for score, doc in search(rw, docs, vocab):用改写后的查询重新检索,直观对比命中分数的变化。merged[doc] = max(merged.get(doc, 0.0), score)多路结果合并时取每篇文档的最高分,模拟并集去重。
常见误区
误区我看到HyDE生成的假设文档很长,同义替换也会增加很多词,所以检索时查询越长越好,反正向量能装下。
正解查改写的目标是消除语义错位,不是堆词。同义替换加的是“相关表达”,HyDE加的是“假设性内容”,这些都是带语义约束的扩展。盲目加长会引入无关词,把查询向量拖向语义杂糅的中间地带,反而降低命中率。老周不会在读者说“找本AI书”时,把整个图书馆的标签都念一遍。
为何常见:人们容易把“更多的信息”等同于“更好的信息”,忽略了查询扩展的质量比长度更重要。
误区用户问“机器学习怎么入门、团队怎么管理”,我用句号逗号一断,变成几个短句,就是查询分解了。
正解查询分解的本质是识别独立子意图,每个子问题都要语义自洽、可独立检索。简单按标点切分可能把“机器学习和团队管理的相似之处”这类真正复合的问题拦腰斩断,反而破坏完整语义。老周拆问题靠的是对读者意图的理解,不是数标点数量。
为何常见:“分解”的字面意思容易让人联想到语法层面的断句,忽略了它需要语义层面的意图识别。
误区HyDE用大模型生成了一段很像回答的段落,既然它已经写出了答案,直接把这段返回给用户不就行了吗?
正解HyDE生成的假设文档只是“探路地图”,不是“目的地”。它是一段用于做嵌入、检索真实文档的辅助文本,内容可能是大模型编造的、不准确的,绝不能当作事实答案返回。老周在脑子里想象的“简介”再好,也替代不了书架上真实存在的书。
为何常见:假设文档的文本形态太像“答案”了,初学者容易把检索工具误认为生成结果。
误区改写模块加上后,所有查询都先改写再检索,这样就万无一失了。
正解改写是为“模糊、口语化、术语错位”等问题准备的补救措施。如果用户查询本身精确清晰,改写会白白增加延迟,还可能稀释原意。工程上应当做触发判断:收益可能大于成本时才改写,否则直通。这和老周不会对直接报出书名的读者再做一遍翻译是一样的道理。
为何常见:看到案例里改写带来的命中率提升显著,就容易忽略案例里查询本来就模糊这一前提条件。
面试题
Q1. 查询改写的主要目的
- A.让用户问题变成更标准格式
- B.提高检索模块对目标文档命中率← 正确答案
- C.减少大模型生成答案的长度
- D.降低向量数据库存储成本
答案:B。查询改写的核心目标是让“用户口语”与“文档书面语”对齐,从而提升检索命中率。A是手段而非最终目的,C和D与改写无明显关联。就像老周翻译读者的话,是为了让读者找到书,而不是为了少走几步路。
Q2. 关于HyDE的表述正确的是
- A.HyDE生成假设文档直接返回给用户
- B.HyDE用假设文档向量匹配真实文档← 正确答案
- C.HyDE只能用于同义替换查询
- D.HyDE的假设文档必须和真文完全一致
答案:B。HyDE的做法是大模型先生成一段假设文档,再将其嵌入向量去与库中真实文档匹配。假设文档只是探路引子,不是答案,也不要求和真实文档完全一致,更不能直接返回给用户。B正确,A、C、D都是对HyDE的典型误解。
Q3. 查询分解最适用的问题类型是
- A.一个词有多个同义词导致漏检
- B.一个问题包含多个独立子意图← 正确答案
- C.文档格式混乱难以解析
- D.向量嵌入维度设置不合理
答案:B。查询分解针对的是“复合意图”问题:用户一句话里装了多个需要分别检索的子问题。单条查询向量会被拉到多个语义方向的中间地带,拆开后多路检索再合并效果更好。A适合同义替换,C和D属于工程问题,与分解无关。
Q4. 下列属于查询改写方法的有
- A.同义替换扩写原始查询← 正确答案
- B.将复合问题拆成多个子查询← 正确答案
- C.修改向量索引的存储结构
- D.生成假设文档后做向量匹配← 正确答案
答案:ABD。同义替换扩写、查询分解、HyDE假设文档是三类主流查询改写方法。修改向量索引结构属于检索工程优化,不是查询改写,所以C不选。
Q5. 同义替换扩写的主要风险是
- A.引入噪声稀释原始查询语义← 正确答案
- B.导致检索延迟不可控
- C.使向量维度数量翻倍
- D.让文档库无法正常索引
答案:A。同义替换把近义词加进查询,如果没有做好权重控制,扩展词可能把查询向量带偏,引入噪声。延迟和维度问题不是它的主要风险,维度由嵌入模型决定,文档索引更不会受影响。
Q6. 适合做无参考改写评估的信号是
- A.改写前后检索命中率提升幅度← 正确答案
- B.人工标注的答案F1分数
- C.GPU服务器资源利用率
- D.大模型推理温度参数
答案:A。无参考评估不依赖人工标注,直接对比改写前后检索命中率、MRR等指标。人工标注的F1属于有参考评估,GPU利用率和温度参数与改写效果无直接关系。
Q7. 关于改写与重排序的关系,正确的是
- A.改写是检索前的预处理模块← 正确答案
- B.重排序可以完全替代改写模块
- C.改写后的结果仍需经过重排序← 正确答案
- D.改写发生在答案生成完成之后
答案:AC。改写处于检索前预处理阶段,重排序处于检索后精排阶段,二者作用不同、串行互补。重排序只对已召回的文档排序,无法弥补漏检,因此不能替代改写。D把改写放到了生成之后,属于流程顺序理解错误。
Q8. 下列哪个指标用于衡量检索排序质量
- A.MRR平均倒数排名← 正确答案
- B.模型参数量大小
- C.训练迭代轮数
- D.学习率数值高低
答案:A。MRR(平均倒数排名)衡量第一个正确答案在检索结果中的排位,排位越靠前得分越高,是评价检索排序质量的常用指标。参数量、训练轮数、学习率都是模型训练配置,不是检索质量指标。
Q9. 关于改写触发时机的工程实践,正确的是
- A.简单精确的查询可以不改写← 正确答案
- B.模糊口语化查询应触发改写← 正确答案
- C.所有查询都必须强制改写
- D.改写结果可进入缓存复用← 正确答案
答案:ABD。工程落地强调按需触发:清晰查询直通检索省延迟,模糊查询才改写;改写结果可以缓存,重复问题直接复用。C“所有查询都改写”会引入无谓延迟并可能稀释精确意图,不正确。
Q10. 查询分解后的检索结果合并方式
- A.只保留第一个子查询的结果
- B.对多路子查询结果取并集去重← 正确答案
- C.随机抽样部分结果返回
- D.直接丢弃所有子查询结果
答案:B。查询分解后每个子查询独立检索,合并时通常取并集,重复文档保留最高分,确保每个子意图都有覆盖。只保留第一路会丢信息,随机抽样和丢弃更不可取。