你有没有做过这样一件事——请客吃饭。客人说要来,你心想:“今晚得张罗一桌菜。”如果真把“做一桌菜”当成一个动作冲进厨房,大概率会手足无措地站在灶台前发呆:菜呢?肉呢?锅呢?火呢?真正会做饭的人会怎么做?先定菜单,再拆任务:凉菜一道、热菜两道、汤一锅、甜品一份。每一项再往下拆——买菜、洗菜、切菜、炒菜、摆盘。每一小步清清楚楚,各自执行,最后端菜上桌,一桌席面就齐了。 AI处理复杂问题也是同一个道理。当用户抛来一句“帮我比较三家云厂商在东南亚的计费规则,并推荐一家适合中小团队的主机方案”时,如果让AI直接拿这句话去检索文档库,往往会“站在灶台前发呆”——一次检索既覆盖不了所有厂商,又照顾不到“东南亚”“计费”“中小团队”等不同维度,最终答非所问。查询分解(Query Decomposition) 的思路就是:先把大问题像设计菜单一样拆成若干子问题,分别检索、分别回答,最后把子答案合并成一份完整的最终回答。这正是本文要讲的“子问题RAG”(Sub-Question RAG)策略。

岗位关联

对AI从业者来说,掌握查询分解等于掌握了复杂问答场景的“后厨管理术”——不必换更贵的模型,就能成倍提升RAG系统在对比、多条件、多属性类问题上的回答质量。

目录
  1. 可视化演示
  2. 问题分解策略
  3. 提示词设计
  4. 并行检索调度
  5. 答案聚合与冲突消解
  6. 递归与终止
  7. 适用场景
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示LR分析器对输入id+id*id的移进-归约解析过程:通过栈操作展示shift和reduce动作,最终接受输入并完成语法分析。

本节配有交互动效(进入视口后自动加载,约 3KB)。

LR分析器 Shift: 将输入符号移入栈 Reduce: 栈顶符号归约为非终结符 Accept: 输入完全匹配文法

问题分解策略

请客吃饭的第一步永远不是冲进厨房,而是坐下来写菜单。主人家会先想清楚客人的口味:有没有人忌辣?有没有小孩?荤素怎么搭配?然后才落笔把“一桌菜”拆成凉菜、热菜、汤、甜品。查询分解(Query Decomposition) 的第一节,做的就是这件事——拿到用户的大问题后,先不急着检索,而是先“写菜单”:拆成哪几个子问题。

从比喻落到技术上,这一步的输入是主查询(Main Query),输出是一组子查询(Sub-Query) 的列表。这组子查询必须满足两个关键性质:覆盖性(Coverage) 和独立性(Independence)。覆盖性指所有子问题合起来,必须完整覆盖主问题的全部信息需求,不能漏掉任何关键维度;独立性指子问题之间尽可能互不重叠,避免重复检索同一段知识。回到菜单的比喻:覆盖性保证“每道菜都有着落”,独立性保证“两道菜不会都跑去抢同一块五花肉”。

用集合语言可以精确描述这两个性质。设主问题为 $\text{Main}$,拆出的 $n$ 个子问题为 $\text{Sub}_1, \text{Sub}_2, \ldots, \text{Sub}_n$:

写菜单时有条经验:好菜单的每道菜都边界清晰、互不抢料。子问题拆分也是如此。拆得好,后续每一步都会顺畅;拆得烂,检索再快也弥补不了结构上的漏洞。

Main⊆⋃i=1nSubi,Subi∩Subj=∅ (i≠j)\text{Main} \subseteq \bigcup_{i=1}^{n}\text{Sub}_i,\qquad \text{Sub}_i \cap \text{Sub}_j = \varnothing\ (i \neq j)
第一个式子说“子问题的并集要罩得住主问题”,即覆盖性;第二个式子说“任意两个子问题交集为空”,即独立性。这两条是判断拆分质量的核心标准。
  1. 主问题集合$\text{Main}$ 表示用户原始查询包含的全部信息需求,可以想象成一个“信息篮子”,里面装着所有要找到的答案碎片。
    Main\text{Main}
  2. 子问题并集$\displaystyle\bigcup_{i=1}^{n}\text{Sub}_i$ 把 $n$ 个子问题各自的信息需求全部合并到一个大篮子里,检查这个大篮子是否完全装下了 $\text{Main}$ 里的每一片碎片。
    ⋃i=1nSubi\bigcup_{i=1}^{n}\text{Sub}_i
  3. 覆盖关系$\subseteq$ 表示“被包含于”,即主问题篮子里的东西,子问题合并篮子里全都有,一个都不能少。
    Main⊆⋃i=1nSubi\text{Main} \subseteq \bigcup_{i=1}^{n}\text{Sub}_i
  4. 互不相交$\text{Sub}_i \cap \text{Sub}_j = \varnothing$ 表示任意两个子问题的信息需求没有交集,就像菜单上“凉拌黄瓜”和“拍黄瓜”不能同时出现一样,避免重复劳动。
    Subi∩Subj=∅\text{Sub}_i \cap \text{Sub}_j = \varnothing

提示词设计

家宴主人决定了菜单后,不会自己闷头干,而是把任务分给帮厨。但真正会做事的主人,一定会写“任务卡”:菜名、要点、时限、交付物。含糊一句“你去弄一下”的结果往往是帮厨端出一盘完全跑偏的菜。提示词(Prompt)就是给大模型的任务卡。

在设计查询分解提示词时,有几样“任务卡必填项”。第一,角色与任务设定(Role Setting):明确告诉模型“你是一个问题拆解助手,负责将复杂问题拆成可独立检索的原子子问题”。第二,输出格式(Output Format):要求模型输出结构化的JSON列表,例如 {"sub_questions": ["子问题1", "子问题2"]},方便下游程序直接解析调度。第三,示例引导(Few-shot):给一两个“问题→拆解结果”的样例,模型会模仿样例的拆分粒度。第四,边界约束(Constraints):提醒模型“不得改变原问题语义”“按原子粒度拆分”“拆分数量3-5个为宜”。

拆解质量不是拍脑袋,可以量化打分量化为三个维度——覆盖度、独立度、可检索度。令覆盖度指标为 $\alpha$ 权重、独立度为 $\beta$ 权重、可检索度为 $\gamma$ 权重,那么一次拆分的整体质量分数可以写成:

任务卡写得越好,帮厨越不容易跑偏。提示词里把角色、格式、示例、边界四件事写清楚,模型的拆分质量就会像拿着任务卡做菜的帮厨一样稳定可靠。

Score=α⋅C(sub)+β⋅I(sub)+γ⋅R(sub),α+β+γ=1\text{Score} = \alpha \cdot C(\text{sub}) + \beta \cdot I(\text{sub}) + \gamma \cdot R(\text{sub}),\quad \alpha+\beta+\gamma=1
这是对拆分好坏打分的“配方”:覆盖度 $C(\text{sub})$ 衡量子问题是否不漏项,独立度 $I(\text{sub})$ 衡量子问题是否不重叠,可检索度 $R(\text{sub})$ 衡量每个子问题拿来检索能不能命中答案。权重加起来必须等于1,代表三者此消彼长的取舍关系。
  1. 覆盖度分量$C(\text{sub})$ 表示子问题对主问题信息需求的覆盖比例,满分1意味着每个需求都有对应子问题;如果漏了一个维度,这个值就小于1。
    C(sub)C(\text{sub})
  2. 独立度分量$I(\text{sub})$ 衡量子问题两两之间的重叠程度,重叠越多分值越低,理想状态接近1。
    I(sub)I(\text{sub})
  3. 可检索度分量$R(\text{sub})$ 衡量每个子问题是否足够具体、能直接命中检索库,太抽象的问题这项得分会很低。
    R(sub)R(\text{sub})
  4. 加权求和$\alpha \cdot C + \beta \cdot I + \gamma \cdot R$ 把三个维度乘上各自权重再相加,得到一个综合评分;$\alpha+\beta+\gamma=1$ 保证评分结果落在0到1之间,便于横向比较。
    α+β+γ=1\alpha+\beta+\gamma=1

并行检索调度

菜单定好、任务卡发出去,真正高效的后厨是什么样的?绝对不会是每个帮厨都排队等同一口锅。有经验的厨房一定是:凉菜师傅在自己案板上切,蒸锅师傅守着灶,炒锅师傅在另一边颠勺——多线并行,互不阻塞。子问题RAG的检索阶段,正是借鉴了这样的“后厨流水线”。

调度机制分三步:批量分发(Dispatch)、并发执行(Execute)、结果收集(Collect)。先把所有子问题打包成一批任务,同时发给多个检索执行单元(可以理解为多个“检索器”或“工人”),每个工人独立完成自己的检索任务后,把结果统一交回调度中心。因为子问题之间具有独立性,所以它们的检索过程互不依赖,天然适合并行。

并行带来的时间收益非常直观。假如串行执行,总耗时是每个子问题检索耗时的累加;并行执行时,总耗时约等于最慢的那个子问题耗时,加上最后的聚合开销。设 $n$ 个子问题的检索耗时分别为 $T_1, T_2, \ldots, T_n$:

后厨的高效来自并行,RAG的高效同样来自并行。拆出的子问题越多,并行检索的省时效果越显著,但也要注意一次拆得太多会引入调度和聚合的额外开销——这正是第五节“递归与终止”要讲的度。

Tparallel=max⁡(T1,T2,…,Tn)+Tagg,ΔT=∑i=1nTi−max⁡(T1,…,Tn)T_{\text{parallel}} = \max(T_1, T_2, \ldots, T_n) + T_{\text{agg}},\qquad \Delta T = \sum_{i=1}^{n}T_i - \max(T_1, \ldots, T_n)
并行总耗时是最慢的那个子任务耗时加汇总耗时;而省下来的时间,正是串行总耗时减去最慢耗时的那一大截。子问题拆得越均匀,并行的收益越明显——这就是“三个帮厨同时开工”的力量。
  1. 单子问题耗时$T_i$ 表示第 $i$ 个子问题从发起到检索完成所花的时间,每个子问题耗时可能不同。
    TiT_i
  2. 并行总耗时$\max(T_1, T_2, \ldots, T_n)$ 取所有子问题耗时中的最大值,因为并行是同时跑的,总时长由最慢的那个决定。
    max⁡(T1,T2,…,Tn)\max(T_1, T_2, \ldots, T_n)
  3. 聚合开销$T_{\text{agg}}$ 表示把各个子答案收集起来、合并处理的固定耗时,它和子问题个数无关,但必然存在。
    TaggT_{\text{agg}}
  4. 节省时间$\Delta T = \sum_{i=1}^{n}T_i - \max(T_i)$ 用串行总耗时减去并行耗时(忽略聚合项),差值就是并行带来的收益——子问题越多,差值越大。
    ΔT=∑i=1nTi−max⁡(Ti)\Delta T = \sum_{i=1}^{n}T_i - \max(T_i)

答案聚合与冲突消解

没有哪家家宴是菜炒好直接往桌上一扔就开吃的。主人会先尝一遍:凉菜咸了要调,热菜重复了要撤掉一道,口味相冲的菜调整上菜顺序。答案聚合(Answer Aggregation) 做的就是“尝菜调整”这一步:把各条子问题的检索答案收集起来,去重、消解、校验,最后端出一桌协调的席面。

聚合面对的首要问题是冗余。三个子问题可能都检索到同一篇文档的同一段话,直接拼接会让最终答案里出现三遍重复内容,所以第一步是去重。第二步是冲突消解(Conflict Resolution)。子问题A回答“这家云厂商东南亚节点延迟低”,子问题B回答“这家云厂商东南亚没有可用区”——两个答案矛盾时,需要一套仲裁机制。常用的方法包括多数投票(Majority Voting) 和置信度加权(Confidence Weighting),即看多个子答案中哪个答案的证据更充分、被更多子检索支持,就采信哪个。

设某个候选最终答案 $F$,$n$ 个子答案中有 $k$ 个支持 $F$,可以用投票分数来衡量这个候选答案的可信度:

上菜前的“尝菜”环节决定了这桌饭能不能拿得出手。答案聚合后的最终回答,也要经过去重、仲裁、校验这三道把关,才能交给用户。

Confidence(F)=1n∑i=1n1[Ansi=F]\text{Confidence}(F) = \frac{1}{n}\sum_{i=1}^{n} \mathbb{1}[\text{Ans}_i = F]
这个式子的意思是:每个子答案都投一票,如果某个子答案恰好等于候选答案 $F$,指示函数 $\mathbb{1}[\cdot]$ 记为1,否则记0;最后除以子答案总数,得到一个0到1之间的支持率。支持率最高的候选答案胜出。
  1. 指示函数$\mathbb{1}[\text{Ans}_i = F]$ 是一个“投票开关”——当第 $i$ 个子答案与候选答案 $F$ 完全一致时取1,否则取0。
    1[Ansi=F]\mathbb{1}[\text{Ans}_i = F]
  2. 逐项投票$\displaystyle\sum_{i=1}^{n}\mathbb{1}[\text{Ans}_i = F]$ 把所有子答案的投票相加,得到总支持票数,最高就是 $n$ 票(全员支持)。
    ∑i=1n1[Ansi=F]\sum_{i=1}^{n}\mathbb{1}[\text{Ans}_i = F]
  3. 归一化$\frac{1}{n}$ 把总票数除以子答案个数,把分数压到0到1区间,使不同问题之间可以比较。
    1n\frac{1}{n}
  4. 最高分胜出遍历所有候选答案,取 $\text{Confidence}(F)$ 最大的那个作为最终答案,等于说“得票最多的答案胜出”。
    Confidence(F)=1n∑i=1n1[Ansi=F]\text{Confidence}(F) = \frac{1}{n}\sum_{i=1}^{n}\mathbb{1}[\text{Ans}_i = F]

递归与终止

家宴菜单不是一成不变的。如果客人是广东人,主人想做一道“佛跳墙”当硬菜——这道菜里又有鲍鱼、又有花胶、又有鸽子蛋,每个食材的处理方式不同,于是任务卡上又拆出三张小任务卡。这就是递归分解(Recursive Decomposition):子问题如果仍然太复杂,可以继续往下拆,形成一棵“问题树”。

但递归不能无限进行下去。厨房里时间有限、灶台有限,过了火候菜反而做砸了。所以必须有终止条件(Termination Condition) 和深度上限(Depth Limit)。两个常用判据:一是看子问题的粒度是否已足够“原子化”——如果子问题已经具体到可以直接检索命中,就停止拆分;二是看拆分的深度是否超过预设上限——比如规定最多拆3层,更深就不再递归。此外,如果拆到最底层仍然无法检索到有效答案,还需要一个兜底策略(Fallback):直接返回现有检索结果或提示用户收窄问题。

把递归的收益和成本放在一起看,可以得到终止的量化依据。设分解深度为 $d$,每多拆一层的平均精度收益为 $\Delta A(d)$,而每层的代价为 $c$(时间与计算开销),最优深度出现在边际收益等于边际成本处:

好厨师懂得“见好就收”。递归分解同样讲究“恰到好处”——既要拆到子问题足够清晰可检索,又不能在收益递减后继续浪费算力。

G(d)=ΔA(d)−c,Stop if G(d+1)≤G(d)G(d) = \Delta A(d) - c,\qquad \text{Stop if } G(d+1) \leq G(d)
这个公式翻译成大白话:每多拆一层,能提升的回答精度是 $\Delta A(d)$,但要多花成本 $c$。当下一层的净收益不再比当前层高时,就停止递归。就像佛跳墙里的鲍鱼处理到“发泡到位”就已经足够,再泡下去既浪费时间也没更多提升。
  1. 边际精度收益$\Delta A(d)$ 表示从第 $d$ 层往下再拆一层,最终答案质量能提升多少——拆到后面,每层提升越来越小。
    ΔA(d)\Delta A(d)
  2. 边际代价$c$ 表示每多拆一层带来的固定开销,包括额外的提示词调用、额外的检索请求和延迟。
    cc
  3. 净收益$G(d) = \Delta A(d) - c$ 等于边际收益减去边际成本,就是“值不值得再拆一层”的判断值。
    G(d)G(d)
  4. 停止条件$G(d+1) \leq G(d)$ 表示再拆一层的净收益已经开始下滑,此时就是最佳停止点。
    G(d+1)≤G(d)G(d+1) \leq G(d)

适用场景

不是每次吃饭都得摆一桌家宴。平时一个人在家,下碗面条5分钟搞定就好;只有客人来访、值得庆贺的日子才值得大动干戈准备一桌。查询分解也是如此:先评估收益,再决定要不要拆。 不是所有问题都值得分解,拆得好是效率倍增器,拆得不好是画蛇添足。

最适合查询分解的是三类场景。第一,多条件约束查询(Multi-constraint Query):例如“找出上海静安区、月租3000以内、允许养猫的一室户”,每个条件是一个独立的检索维度,合在一起检索容易顾此失彼。第二,对比类问题(Comparative Question):例如“A云和B云谁的按量计费更便宜”——需要分别检索两家厂商的计费规则再对比,天然适合拆成两个子问题。第三,多属性检索(Multi-attribute Retrieval):例如“找一款屏幕好、续航长、价格低于5000的笔记本”,三个属性各自检索再聚合,比一次性检索更精准。

什么时候拆、什么时候不拆,可以量化成收益评估模型。设 $V$ 为答案的价值(比如用户决策的重要性),$P(\text{miss})$ 为一次直接检索失败的先验概率(问题越复杂这个概率越高),$C_{\text{decomp}}$ 为分解成本,$C_{\text{latency}}$ 为额外延迟成本:

一句话:“值得请客才开家宴,问题够复杂才拆解。”掌握这个判断标准,查询分解就不是一把无脑挥舞的锤子,而是一把只在合适时机出鞘的手术刀。

Gain=P(miss)×V−Cdecomp−Clatency\text{Gain} = P(\text{miss}) \times V - C_{\text{decomp}} - C_{\text{latency}}
只有当“失败的预期损失”(左侧第一项)大于“分解开销+延迟开销”时,拆解才有正收益。就像请客吃饭的价值足够高、翻车概率足够大时,才值得花时间备一桌菜。
  1. 失败概率$P(\text{miss})$ 是直接检索答非所问的概率——问题越复杂多条件,这个值越高。
    P(miss)P(\text{miss})
  2. 预期损失$P(\text{miss}) \times V$ 用失败概率乘以答案价值,得到“不拆解可能付出的期望损失”。
    P(miss)×VP(\text{miss}) \times V
  3. 分解成本$C_{\text{decomp}}$ 是让大模型做拆解提示词调用的开销,以及拆出来的子问题数量带来的检索费用。
    CdecompC_{\text{decomp}}
  4. 延迟成本$C_{\text{latency}}$ 是并行检索后额外等待的时间成本——虽然并行省了时间,但拆解本身仍会引入延迟。
    ClatencyC_{\text{latency}}
  5. 净收益判断$\text{Gain}$ 为正则值得拆,为负则不如直接检索。
    Gain=P(miss)×V−Cdecomp−Clatency\text{Gain} = P(\text{miss}) \times V - C_{\text{decomp}} - C_{\text{latency}}

AI 应用场景

场景一:电商平台的多条件商品对比搜索

阶段:查询分解 → 并行检索 → 答案聚合

解决的问题:用户在电商平台搜索“适合户外露营的轻薄本,重量1.3kg以内,续航10小时以上,价格7000以内”,传统搜索引擎按关键词匹配时,很难让同一篇商品详情同时满足四个条件,往往返回一堆顾此失彼的结果。AI把问题拆成“轻于1.3kg的轻薄本”“续航超10小时的笔记本”“7000元以内的便携本”三个子问题分别检索,再聚合出同时满足三个条件的机型。

具体案例:某电商导购助手接入了查询分解RAG后,用户输入上述多约束查询时,系统先拆出三个子问题并行检索规格库与评测库,聚合时用投票机制筛掉只满足部分条件的机型,最终给出一个对比表格,直接标注每款机型的命中维度。

场景二:企业知识库的对比分析答疑

阶段:子问题拆分 → 分库检索 → 冲突消解

解决的问题:企业内部员工问“今年Q3和去年Q3各产品线的营收变化如何”,直接检索会得到一堆混合历史年份的报表片段。AI把问题拆成“去年Q3各产品线营收”“今年Q3各产品线营收”“两期数据对比分析”三个子问题,分别从财务知识库检索,最后聚合成一张对比分析结论。

具体案例:某SaaS公司为其销售团队部署了知识库问答助手,销售在准备客户方案时询问“对比我们标准版和Pro版的计费差异”,助手自动拆成两个子问题分别检索定价文档,再聚合出差异对照表,销售直接复制进方案书。

场景三:多文档研究报告辅助撰写

阶段:递归分解 → 并行检索 → 证据综合

解决的问题:分析师需要回答“东南亚各国近三年的云计算政策有什么共同趋势”,这个问题的复杂度值得递归分解:先按国家拆成“新加坡”“印尼”“越南”三个子问题,每个子问题又按时间轴拆成“2021年政策”“2022年政策”“2023年政策”再检索。AI层叠聚合后给出跨国家、跨年份的趋势综述,并附上每一条结论对应的文档引用。

具体案例:某咨询公司的研究助手接入了递归查询分解,分析师输入一个泛主题后,系统自动展开成“国家×年份”的检索网格并行抓取,最终生成的综述引用了40多篇政策文档,与传统手工检索相比效率提升了5倍以上。

Python 演示

python101 行
# -*- coding: utf-8 -*-"""query_decomposition_demo.py一个极简的查询分解RAG演示:1. 用规则把大问题拆成子问题2. 每个子问题独立检索模拟文档库3. 聚合子答案并去重输出""" import reimport timefrom concurrent.futures import ThreadPoolExecutor  DOC_STORE = {    "aws": "AWS在东南亚有新加坡和雅加达两个region,按量计费每小时0.10美元起。",    "azure": "Azure东南亚region在新加坡,月度订阅套餐更优惠,需预付1年合约。",    "gcp": "GCP在东南亚有新加坡和雅加达region,网络流量费较低。",    "price": "按量计费适合短期弹性负载,长期稳定业务建议包年包月。",    "latency": "新加坡region对东南亚用户延迟最低,雅加达region覆盖印尼本地更优。",    "team": "中小团队通常选择包年包月以控制成本,起步配置约每月50美元。",}  class QueryDecomposer:    def __init__(self):        self.max_depth = 2        self.gc_count = 0     def decompose(self, question):        """按关键词规则将问题拆成子问题列表"""        sub_list = []        if "云厂商" in question and "东南亚" in question:            for vendor in ["AWS", "Azure", "GCP"]:                sub_list.append(f"{vendor}在东南亚的计费规则是什么")                sub_list.append(f"{vendor}在东南亚的region分布在哪里")        if "适合" in question and "团队" in question:            sub_list.append("中小团队应该选择按量计费还是包年包月")        if not sub_list:            sub_list = [question]        return sub_list[:6]  class SimpleRetriever:    def __init__(self):        self.doc_list = list(DOC_STORE.keys())        self.free_lists = {}     def search(self, sub_query):        """按关键词命中文档,模拟检索"""        self.free_lists["hit"] = []        for key, text in DOC_STORE.items():            for kw in sub_query.split("的"):                kw = kw.strip()                if kw and kw in text:                    self.free_lists["hit"].append(text)                    break        time.sleep(0.2)        return list(set(self.free_lists["hit"]))  class AnswerAggregator:    def __init__(self):        self.gc_count = 0     def aggregate(self, sub_answers):        """合并多个子答案,去重后拼接"""        seen_set = set()        merged = []        for ans in sub_answers:            for piece in ans:                if piece not in seen_set:                    seen_set.add(piece)                    merged.append(piece)        return " ".join(merged)  def main():    question = "对比AWS、Azure、GCP在东南亚的云服务,推荐一个适合中小团队的方案"    print("原始问题:", question)     decomposer = QueryDecomposer()    sub_questions = decomposer.decompose(question)    print("拆解出的子问题:")    for i, sq in enumerate(sub_questions, 1):        print(f"  {i}. {sq}")     retriever = SimpleRetriever()    start = time.time()    with ThreadPoolExecutor(max_workers=4) as pool:        results = list(pool.map(retriever.search, sub_questions))    print(f"并行检索完成,耗时 {time.time() - start:.2f} 秒")     aggregator = AnswerAggregator()    final_answer = aggregator.aggregate(results)    print("聚合后的最终回答:")    print(" ", final_answer)  if __name__ == "__main__":    main()
目的:演示查询分解RAG的完整链路——规则拆解子问题、线程池并行检索模拟文档库、去重聚合输出最终答案。
  1. sub_list.append(f"{vendor}在东南亚的计费规则是什么")按供应商维度生成独立子问题,保证覆盖性与独立性
  2. if not sub_list: sub_list = [question]兜底策略:如果拆不出子问题,就直接用原问题检索,不强行拆分
  3. ThreadPoolExecutor(max_workers=4)创建4个并行线程执行检索调度,对应正文的并行检索阶段
  4. pool.map(retriever.search, sub_questions)批量分发所有子问题,并发执行,自动收集结果
  5. for kw in sub_query.split("的")用简单关键词匹配模拟向量检索,教学演示用,生产环境替换为真实检索
  6. if piece not in seen_set答案聚合时按片段内容去重,避免多个子问题命中同一段文档被重复拼接
  7. aggregator.aggregate(results)统一收集所有子答案并合并为最终回答,对应答案聚合与冲突消解阶段

常见误区

误区是不是只要用了查询分解,效果一定比直接检索好?那我干脆把所有用户问题都先拆一遍再检索,反正拆总比不拆强。

正解拆解本身有成本。每次调用大模型做分解要花时间和token,拆出来的子问题逐个检索要增加检索次数和延迟。如果问的是“今天天气怎么样”这类简单问题,拆解带来的收益约等于零,反而白白增加了两三倍的延迟。就像一个人吃饭也要摆八道菜,菜做不完也吃不完,纯属浪费。

为何常见:因为看到案例里查询分解在复杂问题上效果显著,容易产生“技巧万能”的错觉,忽略了收益评估——判断标准是公式 $\text{Gain} = P(\text{miss}) \times V - C_{\text{decomp}} - C_{\text{latency}}$ 是否为正。

误区拆完问题、检索完每个子问题,把各段答案按顺序拼在一起不就是最终回答了吗?反正信息都有了,还要多做一步处理干嘛?

正解直接拼接会带来两个问题:一是多个子问题可能命中同一段文档,最终回答里同一句话出现两遍;二是子答案之间可能互相矛盾,直接拼接等于把矛盾原样交给用户。正确答案聚合需要经过冗余去重、冲突消解、一致性校验三步。就像把每道菜直接往桌上扔,盘子叠盘子、菜汁混菜汁,客人根本没法下筷。

为何常见:初学者最容易低估“聚合”这一步的技术含量,以为检索完就万事大吉,忽略了从“信息”到“答案”还需要一道处理工序。

误区既然拆得细能检索得更精准,那我尽量多拆——一个复杂问题拆成二十个子问题,覆盖得全面,答案一定更好。

正解子问题数量存在最优区间,并不是越多越好。拆得太多,每个子问题的检索收益递减,调度和聚合的开销却线性增长,并行检索时间反而可能超过串行——因为调度器本身要处理的任务排队也在耗时。正规做法是观察收益拐点,当边际收益小于边际成本时停止。后厨请二十个厨师炒一道菜,光协调指挥就比做菜还累。

为何常见:把这个策略和“检索粒度越细越准”混淆了,粒度的精细化有一个前提——每个检索操作本身有固定开销,数量膨胀会压垮收益。

误区拆分时就当每个子问题孤立处理,反正公式写了“独立性保证”,检索时互不干扰就行,不用考虑子问题之间的关联。

正解真实场景中,很多问题拆出来的子问题存在隐含依赖。比如“对比AWS和Azure的计费”,两个子问题各自检索价格,但“哪家更适合”这个结论依赖两边的价格对比,而不是各自孤立的价格信息。忽略依赖关系会导致聚合阶段出现逻辑矛盾。把“独立性”理解为“检索动作互相独立”,而不是“信息内容毫无关联”,才是正确的视角——就像夫妻俩分别买菜,买完还要商量这顿饭怎么做。

为何常见:公式里的 $\text{Sub}_i \cap \text{Sub}_j = \varnothing$ 容易让人误解为“信息完全不相关”,实际含义是“检索任务不重复”,而不是“答案内容不关联”。

误区子问题还复杂就继续拆,拆到不能再拆为止——层数深一点没事,拆到底总能把最原子的答案找出来。

正解递归必须设深度上限和终止条件。每拆一层都在增加延迟和成本,而且拆到第4层、第5层时,问题已经过于碎片化,检索回来的片段脱离上下文,反而拼不回完整的答案。终止条件应该是“子问题已可直接检索命中”或“达到预设深度”的或逻辑。佛跳墙里的鲍鱼要提前三天发泡,但不会有人把发泡后的鲍鱼再切成分子料理——过了头就失去了食材本味。

为何常见:看到“递归”两个字就联想到“深入到底”,忽略了工程实践里“深度换精度”的边际递减规律。

面试题

Q1. 查询分解的核心思想是什么?

单选题 · easy

  • A.用更复杂的模型重新生成答案
  • B.将大问题拆为多个子问题分别检索← 正确答案
  • C.一次性检索全部知识库文档
  • D.把答案拼接后直接返回用户

答案:B。查询分解的核心是把复杂大问题拆成多个可独立检索的子问题,再聚合子答案。选项B准确描述了这一定义;A是换模型思路;C是暴力检索;D忽略了聚合处理工序,直接拼接不是完整思想。

Q2. 子问题的“独立性”指什么?

单选题 · easy

  • A.子问题答案互相矛盾
  • B.子问题之间检索任务互不重复← 正确答案
  • C.子问题数量必须相等
  • D.子问题不能用中文表达

答案:B。独立性指子问题的信息需求互不重叠,检索任务不重复,对应公式 $\text{Sub}_i \cap \text{Sub}_j = \varnothing$。B正确;A是冲突不是独立;C数量无要求;D语言无关。

Q3. 以下哪个不是提示词设计中的关键要素?

单选题 · mid

  • A.角色与任务设定
  • B.规定JSON输出格式
  • C.给一两个拆解示例
  • D.指定检索时使用的向量维度← 正确答案

答案:D。提示词设计四要素是角色设定、输出格式、示例引导、边界约束。检索向量维度是检索器内部参数,与提示词无关,D不属于提示词设计范畴。

Q4. 适合使用查询分解的典型场景有哪些?

多选题 · mid

  • A.多条件约束查询← 正确答案
  • B.对比类分析问题← 正确答案
  • C.简单事实性问题
  • D.多属性联合检索← 正确答案

答案:ABD。查询分解适合多条件、对比类、多属性联合检索等复杂查询。C“简单事实性问题”直接用单次检索即可,分解反而浪费成本,不符合收益评估原则。

Q5. 答案聚合阶段的主要挑战是什么?

单选题 · mid

  • A.检索速度不够快
  • B.子答案冗余与冲突消解← 正确答案
  • C.提示词写得太短
  • D.文档库容量不足

答案:B。聚合阶段面对两个核心挑战——子答案冗余去重和冲突消解,对应正文的Confidence公式。A是检索阶段问题;C是分解阶段问题;D是数据层问题,均不属于聚合挑战。

Q6. 关于并行检索调度,以下说法正确的有?

多选题 · hard

  • A.并行总耗时等于所有子任务耗时之和
  • B.并行总耗时约等于最慢子任务的耗时← 正确答案
  • C.子问题拆得越多并行收益一定越高
  • D.调度与聚合会引入额外开销← 正确答案

答案:BD。并行耗时约等于$\max(T_i)$加聚合开销,B正确;A描述的是串行耗时;C错误,子问题过多时调度与聚合开销增长会抵消收益;D正确,调度器处理和聚合步骤都是固定额外成本。

Q7. 递归分解中“停止条件”的作用是什么?

单选题 · easy

  • A.让子问题永远拆下去
  • B.当子问题可检索命中时停止← 正确答案
  • C.强制所有问题只拆一层
  • D.跳过聚合步骤直接返回

答案:B。终止条件是在子问题已经足够原子、可直接检索命中时停止递归,或达到深度上限时停止。A是反面;C不灵活;D跳过聚合是错误的。

Q8. 查询分解中“覆盖率”的含义是?

单选题 · mid

  • A.检索文档占知识库的比例
  • B.子问题覆盖主问题全部需求← 正确答案
  • C.答案占最终输出的比例
  • D.提示词覆盖字符比例

答案:B。覆盖率指所有子问题合起来完整覆盖主问题的全部信息需求,对应公式 $\text{Main} \subseteq \bigcup \text{Sub}_i$。A是文档覆盖率概念;CD与查询分解的覆盖率无关。

Q9. 关于冲突消解的投票机制,正确的理解有?

多选题 · hard

  • A.每个子答案对候选答案投一票← 正确答案
  • B.得票率最高的候选答案胜出← 正确答案
  • C.所有冲突都必须投票解决
  • D.投票分数可用归一化公式计算← 正确答案

答案:ABD。公式 $\text{Confidence}(F) = \frac{1}{n}\sum \mathbb{1}[\text{Ans}_i=F]$ 表明每个子答案投一票、归一化后比较支持率,A、B、D正确。C太绝对,实际中证据权重充分时可直接采信,不必所有冲突都投票。

Q10. 判断“是否值得拆分”的核心标准是什么?

单选题 · mid

  • A.问题字数是否超过20字
  • B.拆解收益大于分解与延迟成本← 正确答案
  • C.用户是否要求详细回答
  • D.模型参数量是否足够大

答案:B。收益评估公式 $\text{Gain} = P(\text{miss}) \times V - C_{\text{decomp}} - C_{\text{latency}}$ 表明,只有当失败预期损失大于分解与延迟成本时才值得拆。A、C、D都不是判断依据,B符合收益评估的核心标准。