1. 案例目标
本案例展示了如何使用LlamaIndex的工作流(Workflow)系统实现一个子问题查询引擎(Sub Question Query Engine)。该系统能够将复杂问题分解为多个子问题,并行处理这些子问题,然后将所有子问题的答案合并成一个全面的回答。这种方法特别适用于需要从多个角度或数据源获取信息才能完整回答的复杂查询场景。
与LlamaIndex内置的子问题查询引擎相比,这个基于工作流的实现具有更强的灵活性,特别是能够使用多个工具来回答子问题,而内置版本只能使用单个工具。
2. 技术栈与核心依赖
llama-index-core
llama-index-llms-openai
llama-index-embeddings-openai
llama-index-readers-file
llama-index-utils-workflow
OpenAI GPT模型
ReAct智能体
向量存储索引
这个实现主要依赖LlamaIndex的核心工作流系统,结合OpenAI的语言模型和嵌入模型,以及ReAct智能体框架来处理子问题。向量存储索引用于高效检索和查询文档内容。
3. 环境配置
安装依赖
!pip install llama-index-core llama-index-llms-openai llama-index-embeddings-openai llama-index-readers-file llama-index-utils-workflow环境变量设置
import os from google.colab import userdata os.environ["OPENAI_API_KEY"] = userdata.get("openai-key")注意:需要设置有效的OpenAI API密钥才能运行此示例。在Colab环境中,可以使用userdata.get()安全地获取API密钥。
4. 案例实现
4.1 事件定义
class QueryEvent(Event): question: str class AnswerEvent(Event): question: str answer: str4.2 SubQuestionQueryEngine工作流类
工作流包含三个主要步骤:
query步骤
- 接收原始查询、LLM和工具列表
- 使用LLM将原始问题分解为子问题
- 为每个子问题创建QueryEvent
sub_question步骤
- 接收QueryEvent
- 创建ReAct智能体处理子问题
- 生成AnswerEvent
combine_answers步骤
- 收集所有AnswerEvent
- 使用LLM合并所有答案
- 生成最终响应
4.3 工作流可视化
draw_all_possible_flows( SubQuestionQueryEngine, filename="sub_question_query_engine.html" )4.4 数据准备与工具创建
folder = "./data/sf_budgets/" files = os.listdir(folder) query_engine_tools = [] for file in files: year = file.split(" - ")[0] # 为每个年份的预算文档创建向量索引和查询工具 index_persist_path = f"./storage/budget-{year}/" if os.path.exists(index_persist_path): storage_context = StorageContext.from_defaults( persist_dir=index_persist_path ) index = load_index_from_storage(storage_context) else: documents = SimpleDirectoryReader( input_files=[folder + file] ).load_data() index = VectorStoreIndex.from_documents(documents) index.storage_context.persist(index_persist_path) engine = index.as_query_engine() query_engine_tools.append( QueryEngineTool( query_engine=engine, metadata=ToolMetadata( name=f"budget_{year}", description=f"Information about San Francisco's budget in {year}", ), ) )4.5 工作流运行
engine = SubQuestionQueryEngine(timeout=120, verbose=True) llm = OpenAI(model="gpt-4o") result = await engine.run( llm=llm, tools=query_engine_tools, query="How has the total amount of San Francisco's budget changed from 2016 to 2023?", ) print(result)5. 案例效果
当用户询问"旧金山2016年至2023年的预算总额变化情况?"时,系统会:
- 将问题分解为8个子问题,每个年份一个
- 为每个子问题创建ReAct智能体,并行查询对应年份的预算数据
- 收集所有子问题的答案
- 使用LLM将所有答案合并成一个全面的回答,展示预算随时间的变化趋势
输出示例:
"从2016年到2023年,旧金山预算总额发生了显著变化。2016年,预算为96亿美元。2017年增加到101.069亿美元,2018年进一步增加到126.59306亿美元。2019年,预算为123亿美元。2020年预算大幅上升,达到153.73192亿美元(以千美元计),约合154亿美元。2021年,预算为141.66496亿美元,2022年为145.5006亿美元。到2023年,预算已增加到146亿美元。总体而言,从2016年到2023年,旧金山的预算从96亿美元增长到146亿美元。"
6. 案例实现思路
6.1 工作流设计模式
该案例采用了事件驱动的工作流设计模式,通过定义不同类型的事件(QueryEvent和AnswerEvent)来控制工作流的执行流程。这种设计使得系统能够灵活地处理并行任务和异步操作。
StartEvent→query步骤→QueryEvent(多个)→sub_question步骤→AnswerEvent(多个)→combine_answers步骤→StopEvent
6.2 问题分解策略
系统使用LLM将复杂问题分解为多个子问题,提示模板要求LLM以纯JSON格式返回子问题列表。这种设计确保了输出的一致性和可解析性。
6.3 并行处理机制
通过为每个子问题创建独立的QueryEvent,系统能够并行处理多个子问题。每个子问题由一个ReAct智能体处理,该智能体可以根据问题选择最合适的工具。
6.4 答案合并策略
使用ctx.collect_events()方法等待所有AnswerEvent完成后,系统将所有子问题的答案合并,并使用LLM生成一个连贯、全面的最终回答。
6.5 工具管理
每个年份的预算数据被转换为独立的查询工具,ReAct智能体可以根据子问题的内容选择使用哪个工具。这种设计使得系统能够处理跨多个数据源的复杂查询。
7. 扩展建议
7.1 智能问题分解
当前的问题分解完全依赖于LLM,可以考虑添加更智能的分解策略,例如:
- 基于问题类型的模板化分解
- 根据可用工具动态调整子问题
- 添加子问题优先级排序
7.2 结果验证与优化
- 添加子问题答案的一致性检查
- 实现子问题答案的置信度评分
- 对于低置信度的答案,可以触发重新查询或人工干预
7.3 性能优化
- 实现子问题处理的优先级队列
- 添加缓存机制,避免重复查询相同子问题
- 优化工具选择策略,减少不必要的工具调用
7.4 用户体验增强
- 添加问题分解过程的可视化展示
- 提供中间结果的实时反馈
- 允许用户调整问题分解策略或合并方式
7.5 应用场景扩展
- 多语言支持,处理非英语查询
- 多模态查询,结合文本、图像等多种数据源
- 领域特定优化,如医疗、法律等专业领域的查询处理
8. 总结
子问题查询引擎工作流是一个强大而灵活的复杂查询处理系统,它通过将复杂问题分解为多个子问题、并行处理这些子问题,然后合并答案的方式来提供全面的回答。与传统的单一查询方法相比,这种方法能够更好地处理需要多角度、多数据源信息的复杂查询。
该实现展示了LlamaIndex工作流系统的几个关键优势:
- 模块化设计:通过事件驱动的方式,将复杂的查询处理流程分解为独立的步骤
- 并行处理能力:能够同时处理多个子问题,提高查询效率
- 工具集成:可以灵活集成多种查询工具,增强系统的适应能力
- 可扩展性:工作流系统易于扩展和定制,可以根据特定需求进行调整
这个案例为构建复杂的查询处理系统提供了一个很好的基础框架,可以应用于各种需要处理复杂查询的场景,如知识库问答、数据分析、决策支持等。通过适当的扩展和优化,它可以成为一个强大的智能问答系统核心组件。