用户问"如何创建工单并设置审批流程?"——这是一个包含两个意图的复合查询。直接用单个检索去匹配知识库,很可能只命中其中一部分,另一半被忽略。本文深入解析多意图检测、查询拆分、答案合成以及递归分解这四条关键链路,看它们如何协同工作以提升复杂问题的回答质量。

一、为什么需要查询分解?

在智能客服问答系统中,用户问题往往不是单一维度的。实际运营中我们发现:

  • 复合操作查询:用户在同一句话中询问多个操作步骤,如"如何创建工单并设置审批流程?"
  • 对比类查询:隐含多个实体间的比较,如"标准版和专业版的系统文档功能有什么区别?"
  • 条件嵌套查询:一个意图的执行前置条件是另一个意图的结果,如"我想开通供应商账号,需要先完成什么实名认证?"

传统的 RAG 流程面对这些问题时,通常只做一次检索,然后期望 LLM 自己从检索结果里找出所有答案。这存在两个致命缺陷:

  1. 检索召回偏移:向量检索或 BM25 检索倾向于匹配问题的"主体语义",第二个意图往往被第一个意图淹没,召回结果偏向一侧。
  2. 信息碎片化:即使 LLM 有能力回答,但检索结果本身就不包含所有需要的信息——LLM 的推理能力也无从发挥。

因此,我们在 Agentic RAG 系统中引入了查询分解(Query Decomposition) 子系统,核心思想是:将复杂查询拆分为多个简单子查询,分别检索,再将结果合成为一个完整答案。


二、系统架构概览

查询分解子系统在 Agent 主流程中位于Phase 1.5(初始检索质量评估之后)和Phase 1.7(最终回答生成之前)。其整体流程如下:

用户提问
    │
    ▼
Phase 1.0-1.4: 初始检索 + 记忆 + 路由
    │
    ▼
Phase 1.5: 检测是否需要分解
    ├─ 不需要 → 直接走 Phase 1.6 执行所需工具
    └─ 需要(多意图)→ 调用 decompose()
         │
         ▼
    Phase 1.7: 递归评估完整性
         ├─ 已完整 → 进入 Phase 2 生成最终回答
         └─ 不完整 → 生成追问 → 再次检索 → 递归合成

整个系统由三个核心模块组成,均在 decomposer.py 中实现:

模块 功能 核心方法
多意图检测 LLM 判断是否需要拆分并生成子问题 detect_multi_intent()
答案合成 将多个子问题的检索结果合并 synthesize_answers()
递归分解 基于完整性评估持续追问和补充 recursive_decompose()

三、多意图检测:LLM 驱动的第一道关卡

3.1 检测逻辑

detect_multi_intent() 是分解流程的入口。它的工作方式非常直接:将用户问题交给 LLM,让 LLM 判断该问题是否需要拆分为多个子查询。

def detect_multi_intent(user_query: str, ...) -> tuple[bool, list[str]]:
    """
    返回:
    - needs_decomposition: 是否需要拆分
    - sub_questions: 拆分后的子问题列表
    
    流程:
    1. 构造 prompt 给 LLM
    2. LLM 判断 needs_decomposition
    3. 如果需要,LLM 同时生成 sub_questions
    4. 返回 (True, ["子问题1", "子问题2", ...])
       或 (False, [])
    """

LLM 接收的 prompt 包含具体指导:

  • 当一个问题包含多个独立的子任务多个不同的查询主体时,需要拆分
  • 每个子问题应当是一个独立的、可自包含的检索问题
  • 不鼓励过度拆分——单一意图的问题应当直接返回 False

3.2 触发条件

并不是每个问题都需要经过分解检测。系统设置了阈值控制:只有当初始检索的综合质量评分低于 QUERY_DECOMPOSITION_THRESHOLD 时,才会触发多意图检测。这避免了不必要的 LLM 调用开销。

配置参数:

# 是否启用查询分解
QUERY_DECOMPOSITION_THRESHOLD = 0.6  # 检索质量低于此阈值时触发
QUERY_DECOMPOSITION_MAX_SUBS = 3     # 最多拆分为3个子问题

3.3 实际案例

用户问题:“如何创建工单,以及如果工单被驳回了怎么重新提交?”

LLM 检测结果

{
  "needs_decomposition": true,
  "sub_questions": [
    "如何创建工单",
    "工单被驳回后如何重新提交"
  ]
}

这两个子问题分别指向不同的知识库文档区域——前者是"工单创建"操作手册,后者是"工单撤销与驳回处理"流程。如果合在一起检索,第二个意图很可能因为语义权重偏低而被忽略。


四、答案合成:将碎片化为整体

当多个子问题的检索全部完成后,synthesize_answers() 负责将分散的结果合并为一个协调一致的最终答案。

4.1 合成流程

sub_questions = ["子问题1", "子问题2", "子问题3"]
       │
       ▼
对每个子问题分别执行 retrieve()
       │
       ▼
将检索结果格式化 → [{子问题, 检索结果}, ...]
       │
       ▼
构造合成 prompt → LLM 生成综合回答
       │
       ▼
合并来源信息(citation tracking)

4.2 Prompt 设计

合成阶段使用两段式 Prompt:

  • SYNTHESIZE_SYSTEM:定义 LLM 的角色——“你是一个知识库问答系统的答案合成助手”,以及合成的核心原则:必须基于所有子问题的检索结果,不能自行添加知识库中没有的信息。
  • SYNTHESIZE_PROMPT:包含用户原始问题、所有子问题及其对应的检索结果片段。LLM 需要从中提炼出完整答案,并标注每部分信息的来源。

4.3 来源合并

一个容易被忽视但极其重要的细节是来源信息的合并。每个子问题的检索结果都包含各自的文档来源元数据(文档名、章节、行号等)。合成阶段需要:

  1. 保留每个子结果的独立来源信息
  2. 在最终答案中清晰标注每段回答所引用的原始文档
  3. 避免来源冲突:同一段信息出现在多个子结果中时,合并去重

五、递归分解:持续追问直到答案完整

多意图拆分解决的是"水平拆分"问题——把并行的多个意图拆开分别检索。但还有一种更复杂的场景:一次检索的结果本身就不完整,需要基于已有答案生成追问,进行第二次、第三次检索。

这就是 recursive_decompose() 的职责。

5.1 为什么需要递归?

在实际运营中我们发现,某些问题存在信息依赖链

用户问:“供应商如何修改已经提交的报价单?”

第一次检索可能只找到了"报价单提交"和"报价单修改"的基本文档,但 LLM 在阅读后发现——“修改已提交的报价单"其实需要先"撤销提交”,再"修改草稿",最后"重新提交"。第一次检索的结果中缺少"撤销提交"的详细信息,于是需要针对这个缺失部分生成追问,进行第二轮检索。

5.2 递归流程

recursive_decompose(query, initial_answer):
    depth = 0
    while depth < RECURSIVE_MAX_DEPTH:
        # 第一步:评估当前答案的完整性
        evaluation = llm_call(RECURSIVE_EVAL_SYSTEM, 
                              current_answer)
        
        if evaluation.quality >= RECURSIVE_MIN_QUALITY:
            break  # 达到质量标准,结束递归
        
        # 第二步:生成追问
        followup = llm_call(RECURSIVE_FOLLOWUP_SYSTEM,
                           query, current_answer, evaluation)
        
        # 第三步:检索追问
        new_results = retrieve(followup)
        
        # 第四步:合成新答案
        current_answer = llm_call(RECURSIVE_SYNTHESIZE_SYSTEM,
                                 previous_answer, new_results)
        
        depth += 1
    
    return final_answer

5.3 三个专门的系统 Prompt

递归分解需要三个独立的 LLM 调用,每个有专门的 System Prompt:

Prompt 职责 核心逻辑
RECURSIVE_EVAL_SYSTEM 评估当前答案完整性 从信息覆盖度、细节深度、准确性三个维度打分,输出质量评分和缺失信息列表
RECURSIVE_FOLLOWUP_SYSTEM 生成追问 基于缺失信息列表,生成一个可直接用于检索的追问问题
RECURSIVE_SYNTHESIZE_SYSTEM 增量合成 将已有的答案与新检索结果融合,避免重复和矛盾

5.4 安全边界

递归分解不能无限进行——必须有严格的安全控制:

RECURSIVE_MAX_DEPTH = 3       # 最多递归3层
RECURSIVE_MIN_QUALITY = 0.8   # 质量评分达到0.8即停止
RECURSIVE_MAX_LLM_CALLS = 10  # 本轮总LLM调用数上限

这三个参数构成了三重保护

  • 深度保护:最多递归 3 层,防止深度遍历导致 Token 耗尽
  • 质量保护:达到 0.8 分即提前退出,避免不必要的追问
  • 总量保护:整个递归过程中 LLM 调用次数不超过 10 次,作为兜底

六、与 Agentic RAG 的集成

查询分解不是孤立的功能,而是深度嵌入 Agent 主流程的特定阶段。以下是在 agent.py 中的集成点:

6.1 集成位置

Phase 1: 检索与准备阶段
  ├─ Phase 1.0: 意图识别(多意图路由)
  ├─ Phase 1.1: 初始检索(五种检索策略)
  ├─ Phase 1.2: 记忆层检查(L1/L2/L3)
  ├─ Phase 1.3: Cross-encoder 精排
  ├─ Phase 1.4: 答案基础评估
  ├─ Phase 1.5: Detect Multi-Intent ← (多意图检测入口)
  ├─ Phase 1.6: 执行工具调用(含 decompose 检索)
  └─ Phase 1.7: Recursive Decompose ← (递归分解入口)
Phase 2: 最终回答生成

6.2 策略控制

通过 RETRIEVE_STRATEGY 配置可以控制分解行为:

RETRIEVE_STRATEGY = "decompose"    # 仅多意图拆分
RETRIEVE_STRATEGY = "recursive"    # 仅递归分解
RETRIEVE_STRATEGY = "combined"     # 先拆分后递归(推荐)

默认推荐 “combined”:先做多意图水平拆分,每个子问题单独检索;如果合成后的答案质量仍不足,再触发递归分解进行垂直追问。

6.3 与检索系统的交互

分解子系统与上层检索系统的交互通过标准接口完成:

# decompose 模式
sub_results = []
for q in sub_questions:
    result = retrievers.retrieve(q)  # 调用完整检索管线
    sub_results.append(result)

# recursive 模式
current_result = initial_result
while need_deeper:
    followup = generate_followup(current_result)
    new_result = retrievers.retrieve(followup)
    current_result = synthesize(current_result, new_result)

每一次子检索都走完整的检索管线(Navigator → Matcher → Fusion → Cross-encoder),而不是简单的关键词匹配。这保证了每个子问题都能获得最高质量的检索结果。


七、配置体系全览

查询分解相关的全部配置项集中在一个配置块中:

配置项 默认值 说明
RETRIEVE_STRATEGY "combined" 分解策略:decompose / recursive / combined
QUERY_DECOMPOSITION_THRESHOLD 0.6 检索质量低于此值触发多意图检测
QUERY_DECOMPOSITION_MAX_SUBS 3 最大子问题数
RECURSIVE_MAX_DEPTH 3 递归分解最大深度
RECURSIVE_MIN_QUALITY 0.8 递归分解目标质量
RECURSIVE_MAX_LLM_CALLS 10 递归过程中 LLM 调用总量上限

调优建议

  • 知识库密度高的场景(如系统C有 2000+ 操作文档):建议将 QUERY_DECOMPOSITION_THRESHOLD 降至 0.5,因为更多文档意味着更大概率需要拆分查询才能精准命中
  • 用户问题普遍简短的单产品线场景:可以考虑关闭 decompose("recursive" 模式),只保留递归分解来处理查询中的隐含信息链
  • Token 预算敏感场景:调低 RECURSIVE_MAX_DEPTHRECURSIVE_MAX_LLM_CALLS,但建议至少保留 2 层递归深度
  • 高精度场景:提升 RECURSIVE_MIN_QUALITY 至 0.9,配合 RECURSIVE_MAX_DEPTH=3 确保深度覆盖

八、实践要点与常见问题

8.1 子问题之间的信息重叠

多个子问题的检索结果可能存在内容重叠(例如"创建工单"和"工单字段说明"都可能命中工单创建文档)。合成阶段通过去重和优先保留更详细来源的策略处理这一问题。

8.2 递归终止的边界条件

递归分解最常见的陷阱是无限"追问-检索-追问"循环——LLM 可能因为追求完美而持续生成追问,即使答案已经足够好。RECURSIVE_MIN_QUALITYRECURSIVE_MAX_DEPTH 的双重限制正是为此设计。

8.3 对 Token 消耗的影响

需要坦诚的是,查询分解会显著增加 Token 消耗:

  • 多意图检测:1 次 LLM 调用(少量的输入 + 输出 Token)
  • 多意图拆分后:N 个子问题 × 各一次完整检索(但不会增加 LLM 调用)
  • 答案合成:1 次 LLM 调用
  • 递归分解:每轮递归 3 次 LLM 调用(评估 + 追问 + 合成)

以"combined"模式、2 个子问题、2 轮递归为例,大约额外增加:

  • 5 次 LLM 调用(1 检测 + 1 合成 + 3 × 2 递归)
  • 对应的输入输出 Token

这个开销是否值得?数据表明,在某个 FAQ 场景中,查询分解使综合回答准确率提升了约 22%,而 Token 消耗增加约 35%——在准确率优先的生产环境中,这个权衡是可以接受的。


九、总结

查询分解子系统为 Agentic RAG 系统提供了"把复杂问题拆碎、分别消化、再整合"的能力:

  1. 多意图检测detect_multi_intent)充当哨兵——只有当初始检索质量不足时才触发,避免无谓消耗
  2. 水平拆分decomposesynthesize_answers)解决"多个并列意图"问题——每个子问题独立检索,再通过 LLM 合成
  3. 垂直挖掘recursive_decompose)解决"信息链依赖"问题——基于已有答案评估完整性,生成追问进行深层检索
  4. 三重安全边界(深度、质量、调用次数)防止递归失控

这套设计将"一次检索搞定一切"的简单 RAG 模式,升级为"检测 → 拆分 → 检索 → 评估 → 追问 → 再检索 → 合成"的迭代式智能检索流程,有效应对了某个 FAQ 中大量存在的复合查询和信息依赖场景。

在下一篇文章中,我们将分析系统的多阶段评估与答案质量监控,看如何从多个维度量化回答质量,并在质量不达标时触发降级策略。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐