前面一篇文章里,我们讨论了 RAG 的 Prompt 应该怎么写。
Prompt 的核心作用,是约束模型基于检索资料回答,并在资料不足时不要自由发挥。
但在真实系统里,还有一个问题经常被低估:
用户的问题本身并不总是适合直接拿去检索。
很多用户不会按照知识库里的标题、术语、字段名和文档结构来提问。
他们会用口语表达,会省略上下文,会把多个问题揉在一句话里,也会提出需要跨文档、跨步骤才能回答的问题。
比如:
这个政策现在还能不能用,跟去年有什么区别? 如果我已经申请过一次,还能不能再申请? 这个接口线上报错,是不是和权限配置有关? 新版本里这个字段还需要传吗?这些问题看起来只是普通提问。
但对 RAG 来说,它们并不简单。
如果系统只是把原始问题直接丢给向量检索,很可能召回不稳定,甚至完全找不到真正需要的资料。
这篇文章就讨论一个更接近真实场景的问题:
RAG 如何处理复杂问题,为什么需要查询改写、多查询召回和多步检索。
为什么复杂问题不能只靠一次检索?
最小 RAG 系统通常是这样工作的:
用户问题 ↓ Embedding ↓ 向量检索 ↓ 返回 TopK ↓ 模型回答这个流程适合简单问题。
比如:
试用期员工能不能休年假?如果文档里有对应制度,向量检索很可能能召回相关片段。
但真实用户经常会问更复杂的问题。
第一类是省略上下文的问题。
比如:
这个还能用吗?如果系统不知道“这个”指的是哪份制度、哪个接口、哪个产品功能,就无法检索。
第二类是多条件问题。
比如:
上海员工试用期内申请年假,需要提前几天提交?这里同时包含地域、员工状态、假期类型和流程要求。
如果只用一句原始问题检索,可能只召回“试用期年假”,漏掉“上海地区规则”或“申请提前时间”。
第三类是对比问题。
比如:
新旧版本的报销审批流程有什么区别?这个问题至少需要找到旧版本和新版本两类资料,再做对比。
一次检索可能只召回其中一边。
第四类是因果或排障问题。
比如:
订单创建接口返回 403,是不是因为 token 过期?这个问题可能需要查错误码、权限规则、认证机制、接口文档和日志说明。
它不是一个单纯语义相似问题。
第五类是多跳问题。
比如:
负责这个系统的部门有哪些审批权限?系统可能需要先查“这个系统”归属哪个部门,再查该部门的审批权限。
这不是一次检索能稳定完成的。
这些场景说明:
复杂问题的难点不只是“召回质量”,还包括“问题理解”。
RAG 不能只问:
我该检索哪些文档?还要先问:
用户这个问题到底想查什么?Query Rewrite:先把问题改写成适合检索的表达
Query Rewrite 就是查询改写。
它的目标不是改变用户问题的意思,而是把用户原始表达改写成更适合检索的查询。
很多用户问题是口语化的。
比如:
还没转正能不能休年假?知识库里可能写的是:
试用期员工年休假按入职时间折算。如果直接检索“还没转正”,可能不如检索“试用期员工 年休假”稳定。
所以可以改写成:
试用期员工 是否可以休年假 年休假 折算规则再比如用户问:
接口下单的时候数量还要传吗?知识库里可能写的是:
POST /orders quantity 参数 是否必填可以改写成:
订单创建接口 POST /orders quantity 参数 必填规则这类改写能提高检索命中率。
Query Rewrite 常见目标包括:
- 把口语表达改成正式术语。
- 补充可能的业务关键词。
- 消除代词和省略信息。
- 把模糊问题改成更明确的检索表达。
- 将上下文依赖问题改写成独立问题。
比如多轮对话中:
用户上一轮:试用期员工能不能休年假? 用户这一轮:那需要提前几天申请?第二轮问题“那需要提前几天申请?”单独看是不完整的。
它应该被改写成:
试用期员工申请年假需要提前几天提交?这就是上下文补全型 Query Rewrite。
Query Rewrite 不是让模型随便扩写
查询改写很有用,但也有风险。
它不是让模型自由发挥,把用户问题扩写成一大堆可能相关的内容。
改写必须保持原意。
比如用户问:
试用期员工能不能休年假?合理改写是:
试用期员工 年休假 是否可以申请 折算规则不合理改写是:
试用期员工 年假 病假 事假 调休 加班 离职结算后者加入了用户没有问的内容,会导致检索范围变宽,噪声变多。
再比如用户问:
quantity 参数是不是必填?如果上下文里已经知道用户在问POST /orders,可以改写成:
POST /orders quantity 参数 是否必填但不能改写成:
订单接口 所有参数 是否必填这会偏离用户意图。
所以 Query Rewrite 要遵守几个原则。
第一,不改变问题含义。
第二,不凭空加入未经确认的实体。
第三,不扩大到用户没有问的范围。
第四,尽量保留关键约束。
第五,必要时输出多个候选查询,而不是把所有意图混成一个长查询。
在工程上,可以给改写模型一个明确 Prompt:
请将用户问题改写为适合知识库检索的查询。 要求: 1. 保持用户原意,不要扩展到未提及的问题。 2. 保留实体、时间、版本、地域、接口名、参数名等约束。 3. 如果问题依赖上下文,请补全必要上下文。 4. 输出 1 到 3 条检索查询。这样比直接让模型“优化问题”更可控。
Multi-query Retrieval:一个问题生成多个检索角度
复杂问题经常不是一个查询就能覆盖的。
这时候可以使用 Multi-query Retrieval,也就是多查询召回。
它的思路是:
针对同一个用户问题,生成多个不同角度的检索查询,然后分别召回资料,再合并结果。
比如用户问:
新员工入职后多久可以申请电脑,审批是谁负责?这个问题其实包含两个点:
新员工什么时候可以申请电脑? 电脑申请审批负责人是谁?可以生成多个查询:
新员工 入职 电脑申请 时间 电脑设备申请 审批负责人 IT 设备申请 流程 审批分别检索后,系统可能召回:
资料 A:新员工入职设备申请流程 资料 B:IT 设备审批角色说明 资料 C:办公设备发放时限再经过合并、去重和 Rerank,选择真正支撑答案的内容。
多查询召回适合几类场景。
第一,用户问题包含多个子问题。
比如:
申请条件是什么,审批流程要多久?第二,用户问题包含多个关键词表达。
比如“电脑”“设备”“办公终端”可能指向同一类文档。
第三,用户问题需要同时查概念和流程。
比如先查“什么是专项审批”,再查“专项审批怎么提交”。
第四,用户表达很模糊,需要从多个角度召回。
比如:
这个报错怎么处理?可以分别查错误码、接口文档、排障手册。
但多查询召回也不能滥用。
每多一个查询,就会增加检索成本和后续排序压力。
如果一个简单问题生成十几个查询,系统会变慢,也会召回更多噪声。
所以比较合理的做法是:
简单问题:1 条查询 中等复杂问题:2 到 3 条查询 复杂问题:拆成子问题后分别检索查询数量应该由问题复杂度决定,而不是固定越多越好。
问题拆分:把复合问题拆成子问题
有些复杂问题不是改写一下就够了。
它们需要拆分。
比如用户问:
新版本报销制度和旧版本相比,申请条件、审批流程和额度有什么变化?这个问题至少包含三个对比维度:
申请条件变化 审批流程变化 报销额度变化还包含两个版本:
新版本 旧版本如果直接用原问题检索,系统可能召回一堆相关制度,但很难保证每个维度都覆盖。
更好的方式是拆成子问题:
1. 新版本报销制度的申请条件是什么? 2. 旧版本报销制度的申请条件是什么? 3. 新旧版本报销制度的审批流程有什么区别? 4. 新旧版本报销额度有什么变化?然后分别检索,再汇总答案。
问题拆分适合:
- 对比类问题。
- 多条件类问题。
- 多实体类问题。
- 多步骤流程类问题。
- 需要分别查不同资料来源的问题。
再举一个技术文档场景。
用户问:
订单创建接口报 403,可能是什么原因,应该怎么排查?可以拆成:
1. 订单创建接口 POST /orders 的鉴权要求是什么? 2. 403 错误码在订单系统中表示什么? 3. token 过期和权限不足分别如何排查? 4. 订单创建接口常见错误处理流程是什么?这样比一次检索“订单创建接口报 403 怎么排查”更稳定。
因为系统会主动覆盖不同信息维度。
但问题拆分也有风险。
如果拆得太细,后续合成答案会更复杂。
如果拆错方向,会导致检索偏离。
所以拆分前最好先判断问题类型。
不是所有问题都需要拆分。
Multi-hop Retrieval:一步检索不够,就分步查
Multi-hop Retrieval 可以理解为多步检索。
它和多查询召回不完全一样。
多查询召回通常是并行的:
生成多个查询 ↓ 同时检索 ↓ 合并结果多步检索是串行的:
第一步检索 ↓ 根据第一步结果生成第二步查询 ↓ 继续检索 ↓ 汇总答案举个例子。
用户问:
负责 Alpha 系统的部门有哪些审批权限?如果知识库里没有一篇文档同时写“Alpha 系统”和“审批权限”,一次检索可能找不到答案。
多步检索可以这样做:
第一步:
查询:Alpha 系统 归属部门 负责人 结果:Alpha 系统归属数据平台部。第二步:
查询:数据平台部 审批权限 结果:数据平台部可审批数据导出、权限开通和报表发布。最后回答:
Alpha 系统归属数据平台部,因此可参考数据平台部审批权限。再看一个接口问题。
用户问:
v2.3.1 版本里订单创建接口的 quantity 还需要传吗?多步检索可以是:
第一步查接口:
POST /orders quantity 参数第二步查版本变更:
v2.3.1 订单创建接口 quantity 参数变更第三步合并判断:
当前版本参数说明 + 版本变更记录这类问题不是一个 Chunk 就能回答的。
它需要系统先找到一个中间事实,再根据中间事实继续查。
这就是 Multi-hop Retrieval 的价值。
Query 处理应该先做问题分类
查询改写、多查询、多步检索都很有用。
但它们不应该对所有问题都默认开启。
一个简单问题如果被复杂处理,反而会增加成本和错误率。
比如用户问:
试用期员工能不能休年假?这个问题可能只需要一次改写或一次检索。
没必要拆成五个子问题。
所以更合理的做法是先做问题分类。
可以把问题分成几类:
简单事实查询 流程查询 对比查询 多条件查询 排障查询 上下文依赖查询 多跳查询 资料不足或不明确查询不同类型使用不同策略。
比如:
简单事实查询:直接检索或轻量改写 流程查询:保留步骤、角色、时间等关键词 对比查询:拆成多个版本或对象分别检索 多条件查询:保留所有约束,必要时多查询 排障查询:拆成错误码、接口、配置、处理流程 上下文依赖查询:先补全上下文 多跳查询:分步检索 不明确查询:先追问,而不是强行检索这能让系统更稳。
因为不同问题需要不同处理方式。
Query 处理不是越复杂越好,而是要和问题类型匹配。
什么时候应该追问用户?
复杂问题处理里,有一种情况很重要:
问题不完整时,不要硬检索。
比如用户问:
这个还能不能申请?如果没有上下文,系统不知道“这个”是什么。
这时强行检索很可能乱召回。
更好的做法是追问:
请问你指的是哪类申请?例如年假申请、报销申请、设备申请或权限申请。再比如用户问:
新版本有什么变化?如果不知道是什么系统、什么文档、哪个版本,也应该追问:
请补充你想对比的系统或文档名称,以及新旧版本范围。RAG 系统不一定每次都要给答案。
当问题缺少关键实体、时间、版本、对象时,追问往往比盲目检索更可靠。
可以设定追问条件:
- 缺少核心对象。
- 缺少版本范围。
- 缺少比较对象。
- 缺少用户上下文。
- 多个解释都可能成立。
- 检索结果分散且没有明显高置信来源。
追问不是失败。
追问是为了获得更准确的问题。
这对于企业知识库尤其重要。
因为错误答案的成本通常比多问一句更高。
一个可落地的复杂问题处理流程
把上面的内容合在一起,一个可落地的流程可以这样设计:
原始用户问题 ↓ 问题分类 ↓ 判断是否需要追问 ↓ Query Rewrite ↓ 必要时问题拆分 ↓ Multi-query Retrieval ↓ 必要时 Multi-hop Retrieval ↓ 候选结果合并 ↓ 去重和 Metadata Filter ↓ Rerank ↓ 上下文压缩 ↓ Prompt 生成答案这里有几个关键点。
第一,不是所有问题都进入完整流程。
简单问题可以走短链路。
复杂问题才走长链路。
第二,Query Rewrite 要保留原意。
不能为了召回更多内容而改变用户问题。
第三,问题拆分要服务答案结构。
拆出来的子问题,最后应该能组合成清晰答案。
第四,多步检索要记录中间结果。
比如第一步查到了哪个实体,第二步基于哪个实体继续查。
第五,最终答案仍然要回到 Prompt 约束。
即使做了多步检索,资料不足时也不能编。
日志很重要
复杂问题处理比普通检索更难排查。
如果没有日志,系统答错时很难知道问题出在哪里。
至少应该记录:
原始问题 问题分类结果 是否触发追问 改写后的查询 拆分后的子问题 每个子问题的检索结果 多步检索的中间事实 候选合并结果 Rerank 排序结果 最终上下文 Prompt 版本 最终答案这样才能定位问题。
如果答案错了,可能是问题分类错了。
也可能是 Query Rewrite 改偏了。
也可能是子问题拆错了。
也可能是某一步检索没有召回关键资料。
还可能是最终 Prompt 没有处理资料冲突。
复杂链路越长,日志越重要。
不要只记录最终答案。
要记录中间过程。
这也是 RAG 工程化和简单 Demo 的区别。
常见误区
第一个误区是所有问题都做 Query Rewrite。
有些问题本身已经很清楚。
比如:
ERR_1024 表示什么?这类问题可能直接关键词检索更好。
过度改写反而可能丢掉精确符号。
第二个误区是多查询越多越好。
查询越多,召回越多,噪声也越多。
后续 Rerank 压力会变大,系统延迟也会上升。
第三个误区是拆分后忘了合并。
问题拆分只是中间步骤。
最终还是要回到用户的原始问题,给出一个完整回答。
第四个误区是不处理上下文依赖。
多轮对话中,用户经常会说“这个”“那它呢”“和上一个一样吗”。
如果系统不补全上下文,检索会很不稳定。
第五个误区是不追问。
问题缺关键信息时,强行回答很容易错。
第六个误区是把多步检索当成 Agent 万能推理。
多步检索应该有清晰边界。
它是在文档中逐步查证,不是让模型随意规划一长串不受控操作。
第七个误区是没有评估。
复杂问题处理上线后,要用真实问题集评估。
否则很难知道 Query Rewrite 和多步检索到底提升了质量,还是只是增加了复杂度。
怎么评估复杂问题处理是否有效?
评估不能只看模型答案是否流畅。
应该关注几个指标。
第一,改写是否保持原意。
如果 Query Rewrite 改偏了,后面检索再好也没用。
第二,关键资料是否被召回。
复杂问题通常需要多个资料片段支撑。
要看这些片段是否都被召回。
第三,子问题是否合理。
拆分后的子问题应该覆盖原始问题,不应该遗漏关键条件。
第四,多步检索中间事实是否正确。
如果第一步中间事实错了,第二步会沿着错误方向继续查。
第五,最终答案是否完整回答原问题。
不能只回答其中一个子问题。
第六,资料不足时是否会追问或拒答。
复杂问题里,承认信息不足很重要。
第七,延迟和成本是否可接受。
复杂链路会增加调用次数。
如果质量提升不明显,但成本明显上升,就需要简化策略。
可以准备一组复杂问题测试集:
多条件问题 对比问题 多轮上下文问题 排障问题 版本问题 跨文档问题 资料不足问题然后分别比较:
直接检索 Query Rewrite Multi-query Retrieval 问题拆分 Multi-hop Retrieval这样才能判断哪种策略真正有效。
落地建议
如果你正在做 RAG 系统,可以按下面顺序推进。
第一步,先收集真实复杂问题。
不要凭空设计复杂链路。
先看用户到底怎么问。
第二步,做轻量问题分类。
至少区分简单事实、流程、对比、排障、多轮上下文和资料不足。
第三步,只对需要的场景启用 Query Rewrite。
尤其是口语化、上下文依赖和术语不匹配问题。
第四步,为多条件和对比问题设计拆分策略。
拆分结果要能回到原问题。
第五步,对跨实体、跨文档问题引入多步检索。
但要控制步数,避免链路失控。
第六步,保留所有中间过程日志。
包括改写、子问题、检索结果、Rerank 结果和最终上下文。
第七步,用测试集评估。
不要只看单次演示效果。
第八步,设置追问机制。
当问题缺少关键实体或版本时,系统应该先追问。
总结
真实 RAG 系统面对的用户问题,往往不是简单的一句话事实查询。
用户会使用口语、省略上下文、混合多个条件,也会提出需要跨文档和多步骤才能回答的问题。
如果系统只是把原始问题直接送进向量检索,就很容易漏召回、错召回或只回答问题的一部分。
Query Rewrite 可以把用户表达改写成更适合检索的查询,但必须保持原意,不能随意扩写。
Multi-query Retrieval 可以从多个角度召回资料,适合多条件、多表达和模糊问题。
问题拆分可以把复合问题拆成子问题,适合对比、流程和排障场景。
Multi-hop Retrieval 可以先查中间事实,再基于中间事实继续查,适合跨实体和跨文档问题。
但复杂处理不是越多越好。
系统应该先做问题分类,再决定是否改写、拆分、多查询或多步检索。
当问题缺少关键信息时,追问比盲目检索更可靠。
从工程角度看,复杂问题处理的关键不是堆更多模型调用,而是让问题理解、检索策略、Rerank、Prompt 和日志形成闭环。
下一篇文章,可以继续讨论 GraphRAG。
因为当问题涉及全局关系、实体网络和跨文档结构时,普通基于 Chunk 的 RAG 会遇到新的边界。