跳到正文

第 7 章:Query 理解与改写

能讲清在线检索前为什么要先"读懂、改好"用户问题,并掌握意图识别、重写、扩写、HYDE

📊 学习时长:40-55 分钟 🎯 完成后能力:能讲清在线检索前为什么要先"读懂、改好"用户问题,并掌握意图识别、重写、扩写、HYDE 🔗 关联面试题:5 道(覆盖 5 个不同角度,见 Part 3)

这一章你会学到什么

  • ✓ 零基础也能跟着做:用一段零依赖代码,看到"重写"和"扩写"怎么把检索不到的问题救回来
  • ✓ 看懂意图识别的三条路线(规则 / BERT 分类 / LLM Prompt)及各自适用场景
  • ✓ 分清 Query 重写(求"准")和 Query 扩写(求"全")的区别
  • ✓ 理解 HYDE 这个反直觉的技巧:为什么"先让模型瞎答一个"反而能提升召回
  • ✓ 学会多轮对话里的指代消解:把"它/这个"补全成能独立检索的问题
  • ✓ 会写 LLM 意图识别的 prompt 模板,并知道 300-500 个意图怎么处理

本章按三段式组织,不同基础的读者各取所需:

段落 给谁看 内容 占比
🚀 Part 1 · 主线实战 零基础,想先跑起来 概念白话 + 重写/扩写 demo ~45%
🎯 Part 2 · 面试深度 想讲透、备战面试 意图识别 + 重写/扩写 + HYDE + 多轮 ~45%
🏆 Part 3 · 验收串题 检验学到位没 关联面试题 + 自检清单 ~10%

🚀 Part 1 · 主线实战 | ~45% 这部分零基础也能跟着做。先用一段不依赖任何库的代码,看到"改一改问题"对检索有多大影响。

在开始之前

你需要:

  • ✅ 装好 Python(3.9+),能看懂基础 Python(字典、函数)
  • ✅ 本章 demo 零依赖,直接跑
  • ✅ 最好先读过第 5、6 章,那两章讲怎么"检索 + 排序",这章讲检索之前先把问题改好

先看一个 Query 失败演练(这是本章的"为什么")

下面用合成问题演示 Query 理解的必要性,不是作者或学员的线上项目记录。即使切分、检索和重排已经可用,输入问题本身仍可能残缺或跑偏:

  • 有人问 "那它过了还能退吗?":"它"指上一轮聊的某款产品,"过了"指过了犹豫期。这句话单独拿去检索,知识库根本不知道"它"是谁,直接抓瞎。
  • 有人打字飞快:"请问下保险费用咋报销呀?":口语词加错别字,和知识库里"保险费用报销流程"这种规范标题字面对不上,一条都召回不到。

这些问题,检索和排序再强也可能救不了,因为送进去的 query 本身就是残缺或跑偏的。可以增加"先读懂、再改写"的处理:指代消解 + 重写 + 扩写,并用标注查询集分别测量改写前后的召回、延迟和误改率。

一句话:检索的上限,从用户按下回车那一刻就定了。先把问题理顺,后面才有的救。

这一章,我们就先用一段零依赖代码,亲手看"改一改问题"对召回有多大影响。

3 个核心概念(先白话,再术语)

概念 1:什么是"Query 理解"?

用户提问进来,别急着拿去检索。用户的话往往口语化("请问怎么……呢")、有错别字、还带"它/这个"这种指代。Query 理解就是在线流程的第一步:先读懂用户到底问什么、再把问题改写成"检索友好"的形式。它是系统的**"调度员 + 翻译官"**。

概念 2:什么是 Query 重写?

把用户那句口语化的问题,改写成一句更规范、更贴近知识库表达的问题。 比如把"请问怎么报销保险费用呢?"改成"保险费用报销流程"。目的是提高命中精度

概念 3:什么是 Query 扩写?

给问题加上同义词、相关词,变成好几个查询一起去检索。 比如"保险理赔"扩成"理赔 / 索赔 / 理赔流程"。目的是提高召回率:哪怕文档里写的是"索赔",也能召回。

一句话记住:重写求"准"(改成一句更好的),扩写求"全"(变成好几句一起查)。

🛠️ 跟着做:看"改问题"怎么救回检索

Step 1:写重写 + 扩写

新建 query_rewrite_expand.py(完整文件见 code-snippets/query_rewrite_expand.py):

docs = ["保险理赔流程", "保险索赔所需材料", "保险产品销售技巧"]  # 玩具知识库:三个文档标题
SYNONYMS = {"理赔": ["理赔", "索赔"]}        # 同义词表:命中"理赔"时,额外用"索赔"也查一遍
FILLER = ["请问", "怎么", "一下", "呢", "啊", "?", "?"]  # 口语/无意义词,重写时删掉(中英文问号都列上)

def rewrite(q):                      # 重写(求准):把口语词去掉,改成更贴近知识库的说法
    for f in FILLER:
        q = q.replace(f, "")         # 逐个把口语词替换成空
    return q.strip()                 # 去掉首尾空白后返回

def expand(q):                       # 扩写(求全):把命中的词替换成同义词,生成多个查询
    out = {q}                        # 用集合装结果(自动去重),先放进原始 query
    for word, syns in SYNONYMS.items():
        if word in q:                # 这个词出现在 query 里 → 用它的每个同义词各造一个查询
            out.update(q.replace(word, s) for s in syns)
    return sorted(out)               # 排序只为输出稳定,好对照

def search(queries):                 # 朴素检索:只要 doc 里包含任意一个 query 字符串,就算命中
    return [d for d in docs if any(q in d for q in queries)]

Step 2:运行

python query_rewrite_expand.py

你应该看到(纯字符串包含判断,结果固定):

① 原始 query: '请问保险理赔呢?' → 命中: []
② 重写后(去口语): '保险理赔' → 命中: ['保险理赔流程']
③ 扩写后(加同义词): ['保险理赔', '保险索赔'] → 命中: ['保险理赔流程', '保险索赔所需材料']

🐛 跑不通看这里

  • 三行命中全是 []?检查 docs 里的标题有没有改动,这个 demo 靠"字符串包含"判命中,标题改了就对不上了。
  • 也命中了东西?多半是你改了 FILLER 或原始 query,让口语词恰好和某个标题字面对上了。
  • KeyError/缩进报错?确认 SYNONYMSFILLER、三个函数都按顺序粘进同一个文件,且函数体缩进一致。

Step 3:看懂这三行

  • 原始问题一条都没命中,因为"请问……呢?"这些口语词,知识库里的标题根本没有,字面对不上;
  • 重写去掉口语词后,"保险理赔"命中了「保险理赔流程」;
  • 扩写把"理赔"扩出"索赔",又把用"索赔"表述的「索赔材料」也召回了。

🎉 你亲眼看到了:同一个问题,改一改,召回结果完全不同。 检索做得再好,送进去的 query 不对,也是白搭,这就是 Query 理解的价值。

Step 4:🛠️ 动手实验,给同义词表加一条

SYNONYMS 改成两条,再跑一次:

SYNONYMS = {"理赔": ["理赔", "索赔"], "保险": ["保险", "保单"]}

你会发现扩写出来的查询更多了。这正是真实系统里同义词/术语词典在做的事:它越全,扩写召回越广。但别高兴太早:词典越大,查询数越多,延迟和噪音也跟着涨(Part 2 会讲这个取舍)。亲手加一条,你就对"扩写的代价"有了体感。

🧭 你刚才做的,等于真实系统的什么?

  • 你的 rewrite()(去口语词) = 生产里的 Query 重写,真实系统会用一个小模型/LLM 做纠错、口语转术语,比删词表强得多;
  • 你的 expand()(同义词表) = Query 扩写,真实系统的词典来自业务术语库,还会用模型动态扩;
  • 你这整段"先改 query 再 search" = 在线链路里检索之前Query 理解模块,坐在用户和第 5、6 章的检索/排序前面。 你手搓的是它最朴素的样子;Part 2 讲它在真实系统里还会加上意图识别、HYDE、多轮指代消解。

这一节你掌握了什么?

  • ✅ Query 理解是什么、为什么要在检索前先做
  • ✅ 重写(求准)和扩写(求全)的区别,以及它们各自怎么救回检索

🎯 Part 2 · 面试深度 | ~45% 这部分讲透意图识别的三条路线、重写/扩写的手段、HYDE,以及多轮里的指代消解。

2.1 意图识别:先搞清用户"想干啥"

同样一句话,意图不同,该走的检索/回答策略也不同(问"报销流程"该查制度文档,问"怎么提升业绩"该查培训资料)。判断意图有三条路线:

  • 规则 / 关键词:列好每类意图的关键词,命中就归类。简单可控,但换种说法就漏、要人工维护。
  • 机器学习(BERT 分类):用标注数据微调一个分类器,对同义表达更鲁棒,但要标注、要训练。
  • LLM Prompt:把意图选项写进 prompt 让大模型判断,零样本就能用,但有成本、延迟,需控偏差。

业内行话:实务里常组合用:冷启动期先用 LLM 粗分类(快、不用标数据),等线上攒够真实 query 和标注,再训一个 BERT 分类器(稳、便宜、低延迟)接管。没有最好,只有最配:按"意图数量、有没有标注数据、延迟/成本预算"来选。

用 LLM 做意图识别,prompt 模板怎么写才稳? 这是面试高频追问,关键是别让模型自由发挥:

  • 枚举候选:把所有意图类别明明白白列进 prompt,让模型"做选择题"而不是"做问答题";
  • 强制输出格式:要求"只输出编号"或输出 JSON,否则模型爱加一堆解释,后处理是噩梦;
  • few-shot 示例:给 1~2 个"问题 → 类别"的范例,准确率立竿见影地涨;
  • 留一个"其他/兜底"类:避免模型硬把不相关问题塞进某一类。

业内行话:意图识别最坑的不是"分不准",而是输出不可控:模型今天回"理赔咨询",明天回"这应该属于理赔相关咨询~",下游根本没法用。枚举 + 强制格式 + few-shot 这三件套,先把输出钉死,再谈准确率。

2.2 Query 重写 vs Query 扩写

  • 重写(求准):去口头词、纠错别字、把口语转成术语,改成一句更贴近知识库的话。
  • 扩写(求全):加同义词("理赔/索赔")、相关词("理赔→流程/材料"),变成多个查询一起检索,再合并结果。

业内行话:扩写有成本:查询数翻几倍,延迟和成本也翻几倍。所以通常只对核心业务场景(如理赔咨询)做扩写,简单 FAQ 不扩。同理,重写也要克制:模型对改写没把握时,宁可保留原始 query,或者「原始 + 改写都查、结果合并」,避免把用户本意改歪。

2.3 HYDE:先让模型"瞎答一个"再检索

短查询(如"等待期")关键词太少,向量不够"丰满",检索容易飘。HYDE(Hypothetical Document Embeddings)的反直觉解法是:

先让 LLM 根据问题编一段"可能的答案"(假设文档),把这段假答案向量化,再用它去检索真实文档。 因为这段假答案自带一堆相关术语和上下文,它的向量更接近真正答案文档的向量,召回更准。

用向量空间的视角看,就很直观了:

业内行话:HYDE 的"假答案"哪怕事实是错的也没关系,我们只借它的"语义丰满度"来检索,最终回答仍然基于检索回来的真实文档。代价是多一次 LLM 调用(延迟 + 成本),所以一般只在"短查询召回不佳"时才上。

2.4 多轮对话:把"它/这个"补全

多轮场景里,用户爱用指代:"那过了还能退吗?" 直接拿这句去检索,知识库不知道"它"是啥。所以要先做指代消解:结合对话历史,把问题补全成一句能独立理解的话:

业内行话:指代消解本质也是一种 Query 重写,只是"补全信息"的来源是对话历史。实务里常用一个小模型或 LLM,输入"历史 + 当前问",输出一句"自包含的问题"再送检索。(更完整的多轮对话管理,如历史压缩、话题漂移,是第 8 章的主题。)

2.5 意图太多怎么办:300-500 个意图的两种方案

小项目意图就十来个,直接列进 prompt 就行。但大型客服系统动辄几百个细分意图,全塞进一个 prompt,又长又贵,模型还容易在几百个选项里选花眼。两种成熟解法:

  • 检索式意图识别(推荐):把每个意图的描述当成"文档"建一个向量库,用户问题来了先向量检索召回最相关的 top-10 候选意图,再让 LLM 只在这 10 个里选。本质就是把第 4、5 章的检索能力复用到"意图"上:意图再多,prompt 里也只出现 10 个。
  • 两级分类:先粗分到少数几个大类(如"理赔 / 承保 / 退保 / 其他",10 个以内),再在命中的大类里细分。每一级候选都很少,judge 得又快又准,代价是要维护一棵两层的意图树。

业内行话:这道题(几百个意图怎么做)是高阶面试题,很多人会卡在"prompt 塞不下"就答不下去。能说出"把意图当文档去检索、先召回再判断",把前面学的检索能力迁移过来:立刻显出体系化的工程思维,而不是只会堆 prompt。


🏆 Part 3 · 验收串题 | ~10% 学到这一步,做几道题验证一下,知道自己学到位没。

关联面试题(5 道,覆盖 5 个角度)

  1. 【意图识别作用】 RAG 系统中意图识别模块承担什么关键作用?
  2. 【意图识别实现】 RAG 系统的意图识别模块是如何实现的?
  3. 【Query 改写方法】 Query 改写技术有哪些常见的创新方法(同义扩展、语义重写等)?
  4. 【意图 Prompt 模板】 大模型意图识别任务里,如何设计有效的 prompt 模板?
  5. 【大规模意图】 需要处理 300–500 个意图时,如何克服上下文限制做有效召回?

学完这一章你应该能干嘛

跟着做(Part 1):

  • 能跑通重写 + 扩写 demo,讲清它们各自救回了什么
  • 能说清重写(求准)和扩写(求全)的区别

讲深度(Part 2):

  • 能讲清意图识别三条路线及怎么选,会写稳定的意图识别 prompt 模板
  • 能讲清 HYDE 为什么"假答案"反而有用
  • 能讲清多轮里为什么要做指代消解、怎么做
  • 能答出"300-500 个意图怎么做"(检索式 / 两级分类)

4 项以下 → 回去 Part 1 重做;4–5 项 → Part 2 再看一遍;6 项以上 → 进入下一章。

完整代码 + 数据集

本书每章的完整可运行代码 + 测试样本,都在配套 GitHub 仓库:

🔗 github.com/MisterBooo/rag-from-zero

  • 跟着教程 clone 下来就能跑
  • 本章的重写 + 扩写 demo(零依赖)也在里面
  • 欢迎 Star ⭐ / Issue 反馈

导航

← 上一章:重排与检索优化 | 下一章:多轮对话与记忆管理 → | 回到项目首页