167 构建多PDF代理:使用查询管道和HyDE
在本示例中,我们将展示如何构建一个多PDF代理,该代理可以跨多个工具进行推理,每个工具对应一个带有HyDE的RAG管道。
作者:https://github.com/DoganK01
安装依赖
%pip install llama-index-llms-openai
%pip install llama-index
%pip install pyvis
%pip install arize-phoenix[evals]
%pip install llama-index-callbacks-arize-phoenix
下载数据和导入库
!mkdir -p 'data/10k/'
!wget 'https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/10k/uber_2021.pdf' -O 'data/10k/uber_2021.pdf'
!wget 'https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/10k/lyft_2021.pdf' -O 'data/10k/lyft_2021.pdf'
import os
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.indices.query.query_transform import HyDEQueryTransform
from llama_index.core.query_engine import TransformQueryEngine
from IPython.display import Markdown, display
from llama_index.core import (
SimpleDirectoryReader,
VectorStoreIndex,
StorageContext,
load_index_from_storage,
)
from llama_index.core.tools import QueryEngineTool, ToolMetadata
# 定义全局回调设置
from llama_index.core.settings import Settings
from llama_index.core.callbacks import CallbackManager
设置可观测性
callback_manager = CallbackManager()
Settings.callback_manager = callback_manager
# 设置Arize Phoenix进行日志记录/可观测性
import phoenix as px
import llama_index.core
px.launch_app()
llama_index.core.set_global_handler("arize_phoenix")
os.environ["OPENAI_API_KEY"] = "sk-"
设置多文档HyDE查询引擎/工具
我们为多文档系统设置HyDE查询引擎及其工具。
HyDE,即假设文档嵌入,是一种创新的检索技术,旨在增强文档检索过程的效率。该方法通过为传入查询创建一个假设文档,然后嵌入该文档来操作。生成的嵌入用于高效检索与假设文档相似的真实文档。
try:
storage_context = StorageContext.from_defaults(
persist_dir="./storage/lyft"
)
lyft_index = load_index_from_storage(storage_context)
storage_context = StorageContext.from_defaults(
persist_dir="./storage/uber"
)
uber_index = load_index_from_storage(storage_context)
index_loaded = True
except:
index_loaded = False
if not index_loaded:
# 加载数据
lyft_docs = SimpleDirectoryReader(
input_files=["./data/10k/lyft_2021.pdf"]
).load_data()
uber_docs = SimpleDirectoryReader(
input_files=["./data/10k/uber_2021.pdf"]
).load_data()
# 构建索引
lyft_index = VectorStoreIndex.from_documents(lyft_docs)
uber_index = VectorStoreIndex.from_documents(uber_docs)
# 持久化索引
lyft_index.storage_context.persist(persist_dir="./storage/lyft")
uber_index.storage_context.persist(persist_dir="./storage/uber")
lyft_engine = lyft_index.as_query_engine(similarity_top_k=3)
uber_engine = uber_index.as_query_engine(similarity_top_k=3)
hyde = HyDEQueryTransform(include_original=True)
lyft_hyde_query_engine = TransformQueryEngine(lyft_engine, hyde)
uber_hyde_query_engine = TransformQueryEngine(uber_engine, hyde)
query_engine_tools = [
QueryEngineTool(
query_engine=lyft_hyde_query_engine,
metadata=ToolMetadata(
name="lyft_10k",
description=(
"提供关于Lyft 2021年财务信息。"
"使用详细的纯文本问题作为工具的输入。"
),
),
),
QueryEngineTool(
query_engine=uber_hyde_query_engine,
metadata=ToolMetadata(
name="uber_10k",
description=(
"提供关于Uber 2021年财务信息。"
"使用详细的纯文本问题作为工具的输入。"
),
),
),
]
设置ReAct代理管道
什么是ReAct代理
ReAct是一种使LLM能够推理和执行特定任务的技术。它结合了思维链推理和行动规划。它使LLM能够创建推理轨迹和特定任务的行动,通过记忆增强它们之间的协同作用。
ReAct代理模型是指一个框架,它将LLM的推理能力与执行行动步骤的能力相结合,创建一个更复杂的系统,能够理解并处理信息,评估情况,采取适当的行动,交流响应,并跟踪正在进行的情境。
推理循环
推理循环允许数据代理选择并根据输入任务与工具进行交互。
记忆
LLM,具有访问记忆的能力,可以存储和检索数据,非常适合跟踪状态或访问多个来源的应用程序。记忆保留过去的交互,使用户能够无缝引用早期的对话点。这种集成涉及为相关信息分配记忆槽,并在对话期间利用检索机制。通过回忆存储的数据,LLM增强了上下文响应并整合了外部来源,丰富了用户体验。
ReAct代理的步骤
- 接收代理输入
- 使用LLM调用ReAct提示生成下一个行动/工具(或返回响应)。
- 如果选择了工具/行动,调用工具管道执行工具并收集响应(在本例中,我们的工具是HyDE查询引擎工具,用于两个文档)。
- 如果生成了响应,获取响应。
代理输入组件
生成给定任务的输入。
from llama_index.core.agent.react.types import (
ActionReasoningStep,
ObservationReasoningStep,
ResponseReasoningStep,
)
from llama_index.core.agent import Task, AgentChatResponse
from llama_index.core.query_pipeline import (
AgentInputComponent,
AgentFnComponent,
CustomAgentComponent,
QueryComponent,
ToolRunnerComponent,
)
from llama_index.core.llms import MessageRole
from typing import Dict, Any, Optional, Tuple, List, cast
## 代理输入组件
## 这是生成代理输入到其他组件的组件
## 也可以在这里放置初始化逻辑。
def agent_input_fn(task: Task, state: Dict[str, Any]) -> Dict[str, Any]:
"""代理输入函数。
返回:
一个输出键和值的字典。如果在定义组件之间的链接时指定了src_key,请确保src_key与指定的output_key匹配。
"""
# 初始化current_reasoning
if "current_reasoning" not in state:
state["current_reasoning"] = []
reasoning_step = ObservationReasoningStep(observation=task.input)
state["current_reasoning"].append(reasoning_step)
return {"input": task.input}
agent_input_component = AgentInputComponent(fn=agent_input_fn)
定义代理提示
在这里,我们定义生成ReAct提示的代理组件,并在从LLM生成输出后解析为结构化对象。
在接收到输入后,使用ReAct代理提示调用LLM。
ReActChatFormatter基本上使用ReAct提示方法(思维链+行动)生成完全格式化的ReAct提示。
from llama_index.core.agent import ReActChatFormatter
from llama_index.core.query_pipeline import InputComponent, Link
from llama_index.core.llms import ChatMessage
from llama_index.core.tools import BaseTool
## 定义提示函数
def react_prompt_fn(
task: Task, state: Dict[str, Any], input: str, tools: List[BaseTool]
) -> List[ChatMessage]:
# 添加输入到推理
chat_formatter = ReActChatFormatter()
return chat_formatter.format(
tools,
chat_history=task.memory.get() + state["memory"].get_all(),
current_reasoning=state["current_reasoning"],
)
react_prompt_component = AgentFnComponent(
fn=react_prompt_fn, partial_dict={"tools": query_engine_tools}
)
你可以在这里看到ReAct提示:
https://github.com/run-llama/llama_index/blob/6cd92affa5835aa21f823ff985a81f006c496bbd/llama-index-core/llama_index/core/agent/react/prompts.py#L6
定义代理输出解析器 + 工具管道
一旦LLM给出输出,我们有一个决策树:
- 如果给出了答案,那么我们就完成了。处理输出。
- 如果给出了行动,我们需要使用指定的参数执行指定的工具,然后处理输出。
工具调用可以通过ToolRunnerComponent模块完成。这是一个简单的包装模块,它接收一个工具列表,并且可以使用指定的工具名称(每个工具都有一个名称)和工具动作“执行”。
我们实现这个整体模块OutputAgentComponent,它子类化CustomAgentComponent。
parse_react_output_fn
函数简单地将从react_prompt_fn
获得的ReAct提示解析为推理步骤。
在这种情况下,ReAct代理选择无论使用工具还是完成工具并简单地获取适合代理聊天响应的输出(AgentChatResponse)。
run_tool_fn
函数简单地运行一个工具(如果选择了)。
最后,通过应用process_agent_response_fn
函数,根据代理输出格式编辑传入的输出。
from typing import Set, Optional
from llama_index.core.agent.react.output_parser import ReActOutputParser
from llama_index.core.llms import ChatResponse
from llama_index.core.agent.types import Task
def parse_react_output_fn(
task: Task, state: Dict[str, Any], chat_response: ChatResponse
):
"""将ReAct输出解析为推理步骤。"""
output_parser = ReActOutputParser()
reasoning_step = output_parser.parse(chat_response.message.content)
return {"done": reasoning_step.is_done, "reasoning_step": reasoning_step}
parse_react_output = AgentFnComponent(fn=parse_react_output_fn)
def run_tool_fn(
task: Task, state: Dict[str, Any], reasoning_step: ActionReasoningStep
):
"""运行工具并处理工具输出。"""
tool_runner_component = ToolRunnerComponent(
query_engine_tools, callback_manager=task.callback_manager
)
tool_output = tool_runner_component.run_component(
tool_name=reasoning_step.action,
tool_input=reasoning_step.action_input,
)
observation_step = ObservationReasoningStep(
observation=str(tool_output["output"])
)
state["current_reasoning"].append(observation_step)
# TODO: 获取输出
return {"response_str": observation_step.get_content(), "is_done": False}
run_tool = AgentFnComponent(fn=run_tool_fn)
def process_response_fn(
task: Task, state: Dict[str, Any], response_step: ResponseReasoningStep
):
"""处理响应。"""
state["current_reasoning"].append(response_step)
response_str = response_step.response
# 现在我们完成了这一步,放入记忆中
state["memory"].put(ChatMessage(content=task.input, role=MessageRole.USER))
state["memory"].put(
ChatMessage(content=response_str, role=MessageRole.ASSISTANT)
)
return {"response_str": response_str, "is_done": True}
process_response = AgentFnComponent(fn=process_response_fn)
def process_agent_response_fn(
task: Task, state: Dict[str, Any], response_dict: dict
):
"""处理代理响应。"""
return (
AgentChatResponse(response_dict["response_str"]),
response_dict["is_done"],
)
process_agent_response = AgentFnComponent(fn=process_agent_response_fn)
缝合代理查询管道
我们现在可以将顶级代理管道缝合在一起:agent_input -> react_prompt -> llm -> react_output。
最后一个组件是调用子组件的if-else组件。
from llama_index.core.query_pipeline import QueryPipeline as QP
qp = QP(verbose=True)
from llama_index.core.query_pipeline import QueryPipeline as QP
from llama_index.llms.openai import OpenAI
qp.add_modules(
{
"agent_input": agent_input_component,
"react_prompt": react_prompt_component,
"llm": OpenAI(model="gpt-4-1106-preview"),
"react_output_parser": parse_react_output,
"run_tool": run_tool,
"process_response": process_response,
"process_agent_response": process_agent_response,
}
)
# 将输入链接到react提示到解析的响应(工具动作/输入或观察)
qp.add_chain(["agent_input", "react_prompt", "llm", "react_output_parser"])
# 从react输出到工具调用的条件链接(如果未完成)
qp.add_link(
"react_output_parser",
"run_tool",
condition_fn=lambda x: not x["done"],
input_fn=lambda x: x["reasoning_step"],
)
# 从react输出到最终响应处理的条件链接(如果完成)
qp.add_link(
"react_output_parser",
"process_response",
condition_fn=lambda x: x["done"],
input_fn=lambda x: x["reasoning_step"],
)
# 无论是响应处理还是工具输出处理,添加链接到最终代理响应
qp.add_link("process_response", "process_agent_response")
qp.add_link("run_tool", "process_agent_response")
可视化查询管道
from pyvis.network import Network
net = Network(notebook=True, cdn_resources="in_line", directed=True)
net.from_nx(qp.clean_dag)
print(net)
{
"Nodes": [
"agent_input",
"react_prompt",
"llm",
"react_output_parser",
"run_tool",
"process_response",
"process_agent_response"
],
"Edges": [
{
"src_key": null,
"dest_key": null,
"condition_fn": null,
"input_fn": null,
"width": 1,
"from": "agent_input",
"to": "react_prompt",
"arrows": "to"
},
{
"src_key": null,
"dest_key": null,
"condition_fn": null,
"input_fn": null,
"width": 1,
"from": "react_prompt",
"to": "llm",
"arrows": "to"
},
{
"src_key": null,
"dest_key": null,
"condition_fn": null,
"input_fn": null,
"width": 1,
"from": "llm",
"to": "react_output_parser",
"arrows": "to"
},
{
"src_key": null,
"dest_key": null,
"width": 1,
"from": "react_output_parser",
"to": "run_tool",
"arrows": "to"
},
{
"src_key": null,
"dest_key": null,
"width": 1,
"from": "react_output_parser",
"to": "process_response",
"arrows": "to"
},
{
"src_key": null,
"dest_key": null,
"condition_fn": null,
"input_fn": null,
"width": 1,
"from": "run_tool",
"to": "process_agent_response",
"arrows": "to"
},
{
"src_key": null,
"dest_key": null,
"condition_fn": null,
"input_fn": null,
"width": 1,
"from": "process_response",
"to": "process_agent_response",
"arrows": "to"
}
],
"Height": "600px",
"Width": "100%",
"Heading": ""
}
# 将网络保存为"agent_dag.html"
net.write_html("agent_dag.html")
from IPython.display import display, HTML
# 读取HTML文件的内容
with open("agent_dag.html", "r") as file:
html_content = file.read()
# 显示HTML内容
display(HTML(html_content))
围绕我们的查询引擎设置代理工作者
from llama_index.core.agent import QueryPipelineAgentWorker
from llama_index.core.callbacks import CallbackManager
agent_worker = QueryPipelineAgentWorker(qp)
agent = agent_worker.as_agent(
callback_manager=CallbackManager([]), verbose=True
)
运行代理
# 开始任务
task = agent.create_task(
"What was Uber's Management's Report on Internal Control over Financial Reporting?"
)
step_output = agent.run_step(task.task_id)
# 开始任务
task = agent.create_task("What was Lyft's revenue growth in 2021?")
step_output = agent.run_step(task.task_id)
step_output = agent.run_step(task.task_id)
step_output.is_last
True
print(step_output)
response = agent.finalize_response(task.task_id)
print(str(response))
结论
在本教程中,我们涵盖了大量信息,同时解决了一些常见的问题和问题:
- 使用不同的索引来处理不同的用例(列表索引 vs. 向量索引)
- 使用Streamlit的
session_state
概念存储全局状态值 - 使用Llama Index自定义内部提示
- 使用Llama Index从图像中读取文本