树状查询分解与深度研究:递归多步检索
树状查询分解与深度研究:递归多步检索
学习目标
读完本章后,你将能够:
- 理解树状查询分解相比单步检索的优势
- 设计信息充分性检查与子查询生成策略
- 实现三源融合检索(知识库 + Web + 知识图谱)
- 在自己的 RAG 系统中引入 Deep Research 模式
前置知识
- RAG 全链路可视化管线 — 检索基础流程
- RAPTOR 层次化知识组织 — 层次化知识组织
1. 为什么需要树状查询分解
标准 RAG 管线对每个用户查询执行一次检索然后生成回答。但对于复杂查询,一次检索往往不够:
用户查询:"对比 GPT-4 和 Claude 3 在代码生成、数学推理、多模态三个方面的能力差异"
单步检索问题:- 向量数据库中没有一篇文章同时覆盖这三个方面的对比- 需要分别检索:GPT-4 代码生成、GPT-4 数学推理、GPT-4 多模态、 Claude 3 代码生成、Claude 3 数学推理、Claude 3 多模态树状查询分解的核心思想:
- 将复杂查询分解为多个子查询(树的分支)
- 对每个子查询递归执行检索
- 合并所有子查询的结果,生成综合回答
2. 核心流程
2.1 信息充分性检查
在执行检索后,用 LLM 判断当前获取的信息是否足以回答用户问题:
Prompt 示例:问题: {question}已检索到的信息: {retrieved_chunks}
请判断:上述信息是否足以完整回答问题?- 如果足够,返回 {"is_sufficient": true}- 如果不足,返回 {"is_sufficient": false, "missing_information": ["缺失信息1", "缺失信息2"]}关键设计:
- 信息充分性检查是动态的——如果一次检索的信息已足够,就提前终止,不再递归
- 这避免了无意义的深层检索,节省 API 成本和延迟
2.2 子查询生成
当信息不足时,LLM 根据缺失信息规划新的搜索查询:
Prompt 示例:问题: {question}已有信息: {retrieved_chunks}缺失信息: {missing_information}
请生成最多 3 个搜索查询,以获取上述缺失信息。每个查询应聚焦一个具体方面,避免过于宽泛。
返回: [{"question": "...", "query": "..."}, ...]关键设计:
- 子查询的
question是人类可读的自然语言问题(用于日志和回调) - 子查询的
query是优化后的搜索关键词(更适合搜索引擎)
2.3 递归深度控制
树状查询需要设置最大深度,防止无限递归:
research(chunk_info, question, query, depth=3): if depth == 0: return # 达到最大深度,终止 results = retrieve(query) if is_sufficient(results, question): return # 信息足够,提前终止 sub_queries = generate_sub_queries(question, results) for sq in sub_queries: research(chunk_info, sq.question, sq.query, depth-1) # 递归深度选择建议:
depth=1:单次检索 + 一次补充(适合中等复杂度查询)depth=2:最多 3 层(适合大部分场景)depth=3+:深度研究模式(API 成本高,适合关键场景)
3. 三源融合检索
树状查询的每个节点可以从多个数据源获取信息:
| 数据源 | 优势 | 局限 |
|---|---|---|
| 知识库 | 高质量、可控、可追溯 | 覆盖范围有限 |
| Web 检索 | 覆盖广、实时更新 | 质量参差不齐、需要 API 费用 |
| 知识图谱 | 结构化、关系清晰 | 需要先构建图谱、仅覆盖已有实体 |
融合策略:
- 知识库优先:优先从知识库检索(质量最高、成本最低)
- Web 补充:知识库不足时通过 Web 搜索补充(如 Tavily、SearXNG)
- 知识图谱增强:如果查询涉及实体关系,优先从图谱获取结构化信息
去重与合并:
- 按
chunk_id去重(同一文档的相同内容不重复添加) - 按
doc_id聚合来源文档统计 - 总结果数为各源结果的并集
4. 异步并发优化
子查询之间相互独立,可以并行执行:
# 伪代码async def research(chunk_info, question, query, depth=3): if depth == 0: return results = await retrieve_information(query) update_chunk_info(chunk_info, results)
if is_sufficient(chunk_info, question): return
sub_queries = await generate_sub_queries(question, query, results) # 并行执行所有子查询 tasks = [ asyncio.create_task(research(chunk_info, sq.question, sq.query, depth-1)) for sq in sub_queries["questions"] ] await asyncio.gather(*tasks, return_exceptions=True)优化效果:
- 如果某层生成 3 个子查询,串行需要 3 次 API 调用时间,并行只需 1 次(最慢的那个)
- 使用
return_exceptions=True确保单个子查询失败不影响其他分支
5. 问题与规避
| 陷阱 | 对策 |
|---|---|
| 递归爆炸(子查询指数增长) | 限制最大深度 + 每层最多 3 个子查询 |
| API 成本过高 | 信息充分性检查提前终止 + 设置总查询数上限 |
| 多源结果冲突 | 按数据源可信度加权,知识库 > 知识图谱 > Web |
| 子查询偏离原问题 | 子查询生成 prompt 中要求”紧密围绕原问题” |
| 结果合并后上下文超长 | 按 Token 预算截断,优先保留高相关度 chunk |
6. 设计取舍
树状分解 vs 线性多步
| 维度 | 树状分解 | 线性多步(ReAct) |
|---|---|---|
| 并行度 | 高(子查询可并行) | 低(每步依赖上一步) |
| 总延迟 | 低(并行执行) | 高(串行执行) |
| 复杂度 | 中(需要管理树结构) | 低(标准循环) |
| 适用场景 | 复杂多主题查询 | 单主题多步骤推理 |
推荐:当查询涉及多个独立子主题时,使用树状分解(如”对比 A 和 B 的 X、Y、Z 三方面”);当查询需要逐步推理时,使用线性多步。
信息充分性检查 vs 固定深度
| 维度 | 信息充分性检查 | 固定深度 |
|---|---|---|
| 效率 | 高(信息足够时提前终止) | 低(总是执行到最大深度) |
| 成本 | 低(减少不必要检索) | 高 |
| 可靠性 | 依赖 LLM 判断质量 | 确定性高 |
| 适用场景 | 通用场景 | 成本敏感场景 |
推荐:默认使用信息充分性检查。如果对成本控制要求极高,可以用固定深度替代。
参考来源
- IR-CoT 论文:https://arxiv.org/abs/2305.15153
- Self-Ask 论文:https://arxiv.org/abs/2210.03350