LlamaIndex(八)——LlamaIndex Agents

一、Agents简介

数据Agents是 LlamaIndex 中由 LLM 支持的knowledge workers,可以通过readwrite功能智能地对数据执行各种任务。它们有能力做到以下几点:

  • 对不同类型的数据(非结构化、半结构化和结构化)执行自动搜索和检索。
  • 以结构化方式调用任何外部服务 API。 他们可以立即处理响应,也可以索引/缓存该数据以供将来使用。
  • 存储对话历史记录。
  • 使用上述所有内容来完成简单和复杂的数据任务。

从这个意义上说,Agents不仅仅是查询引擎,它们不仅可以从静态数据源read,还可以动态地提取和修改来自各种不同工具的数据。

构建数据Agents需要以下核心组件:

  • 一个推理循环
  • 工具抽象

数据Agent初始化时会设置一组 API 或工具,以进行交互;这些 API 可以被Agent调用以返回信息或修改状态。给定一个输入任务,数据Agent使用推理循环来决定使用哪些工具,以什么顺序,以及调用每个工具的参数。

1.1 推理循环(Reasoning Loop)

推理循环取决于Agent的类型。LlamaIndex支持以下代Agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
from llama_index.agent import OpenAIAgent, ReActAgent
from llama_index.llms import OpenAI

# import and define tools
...
# initialize llm
llm = OpenAI(model="gpt-3.5-turbo-0613")
# initialize openai agent
agent = OpenAIAgent.from_tools(tools, llm=llm, verbose=True)
# initialize ReAct agent
agent = ReActAgent.from_tools(tools, llm=llm, verbose=True)
# use agent
response = agent.chat("What is (121 * 3) + 42?")

每个Agent都有一套工具。每个Agent还支持两种接受输入任务的主要方法——聊天和查询。 请注意,这些分别是 ChatEngineQueryEngine 中使用的核心方法。 事实上,Agent基类(BaseAgent)只是继承自 BaseChatEngineBaseQueryEngine。 聊天允许Agent利用先前存储的对话历史记录,而查询是一个无状态调用——历史/状态不会随时间保留。

推理循环取决于Agent的类型。Function Calling Agents 在 while 循环中调用 function call API,因为工具决策逻辑已经内置在function call API 中。给定一个输入prompt和之前的聊天历史(包括之前的函数调用),function call API 将决定是否进行另一个函数调用(选择一个工具),或者返回一个assistant消息。如果 API 返回一个函数调用,那么我们就负责执行该函数并传递一个函数消息到聊天历史中。如果 API 返回一个assistant消息,那么循环就完成了。

ReAct Agent使用general text completion,因此它可以与任何 LLM 一起使用。general text completion有一个简单的 input str → output str格式,这意味着推理逻辑必须编码在提示中。ReAct Agent使用一个受到 ReAct 论文启发的输入提示(并适应到其他版本),以决定选择哪个工具。它看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
You have access to the following tools:
{tool_desc}

To answer the question, please use the following format.

```
Thought: I need to use a tool to help me answer the question.
Action: tool name (one of {tool_names})
Action Input: the input to the tool, in a JSON format representing the kwargs (e.g. {{"text": "hello world", "num_beams": 5}})
```
Please use a valid JSON format for the action input. Do NOT do this {{'text': 'hello world', 'num_beams': 5}}.

If this format is used, you will receive a response in the following format:

```
Observation: tool response
```
...

LlamaIndex在chat prompts上原生实现 ReAct;推理循环被实现为assistant消息和user消息交替的系列。Thought/Action/Action Input 部分被表示为assistant消息,而 Observation 部分被表示为user消息。ReAct prompt不仅期望选择工具的名称,还期望以 JSON 格式填写工具的参数。这使得输出与 OpenAI function call API 的输出类似——主要区别在于,在function call API的情况下,工具选择逻辑是内置在 API 本身中的(通过微调模型),而在这里则是通过明确的提示引出的。

二、Tool Abstractions

拥有适当的工具抽象是构建数据agents的核心。定义一组工具类似于定义任何API接口,不同之处在于这些工具是为agent而不是人类使用而设计的。LlamaIndex允许用户定义工具以及包含一系列底层函数的ToolSpec。

在使用agent或具有函数调用功能的LLM时,所选工具(以及为该工具编写的参数)强烈依赖于工具名称和工具目的及参数的描述。花时间调整这些参数可以显著改变LLM调用这些工具的方式。

工具实现了一个非常通用的接口——只需定义__call__,同时返回一些基本的元数据(名称、描述、函数模式)。

LlamaIndex提供了几种不同类型的工具:

  • FunctionTool:函数工具允许用户轻松地将任何用户定义的函数转换为工具。它还可以自动推断函数模式。
  • QueryEngineTool:一个包装现有查询引擎的工具。由于LlamaIndex的agent抽象继承自BaseQueryEngine,这些工具也可以包装其他agent。
  • 社区贡献的ToolSpec,定义了围绕单个服务(如Gmail)的一个或多个工具。
  • 用于包装其他工具的Utiltiy tools,以处理从工具返回的大量数据。

2.1 FunctionTool

FunctionTool是对任何现有函数(同步和异步都支持)的简单包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from llama_index.core.tools import FunctionTool


def get_weather(location: str) -> str:
"""Usfeful for getting the weather for a given location."""
...


tool = FunctionTool.from_defaults(
get_weather,
# async_fn=aget_weather, # optional!
)

agent = ReActAgent.from_tools(tools, llm=llm, verbose=True)

为了更好的函数定义,还可以利用pydantic来处理函数参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pydantic import Field


def get_weather(
location: str = Field(
description="A city name and state, formatted like '<name>, <state>'"
),
) -> str:
"""Usfeful for getting the weather for a given location."""
...


tool = FunctionTool.from_defaults(get_weather)

默认情况下,工具名称将是函数名称,文档字符串将是工具描述。但也可以覆盖此设置。

1
tool = FunctionTool.from_defaults(get_weather, name="...", description="...")

2.2 QueryEngineTool

任何查询引擎都可以使用QueryEngineTool转换为工具:

1
2
3
4
5
from llama_index.core.tools import QueryEngineTool

tool = QueryEngineTool.from_defaults(
query_engine, name="...", description="..."
)

2.3 Tool Specs

LlamaIndex还通过LlamaHub提供了丰富的工具和工具规范集。可以将工具规范视为旨在一起使用的工具包。通常,这些涵盖了单个接口/服务(如Gmail)中的有用工具。
要与Agent一起使用,可以安装特定的工具规范集成:

1
pip install llama-index-tools-google

然后使用它:

1
2
3
4
5
from llama_index.agent.openai import OpenAIAgent
from llama_index.tools.google import GmailToolSpec

tool_spec = GmailToolSpec()
agent = OpenAIAgent.from_tools(tool_spec.to_tool_list(), verbose=True)

可以查看LlamaHub以获取社区贡献的工具规范的完整列表

2.4 Utiltiy tools

通常,直接查询API可能会返回大量数据,这本身可能会超出LLM的上下文窗口(或者至少不必要地增加正在使用的Token数量)。为了解决这个问题,LlamaIndex在LlamaHub工具中提供了一组初始的“实用工具”——实用工具在概念上并不与特定服务(例如Gmail、Notion)相关联,而是可以增强现有工具的功能。在这种情况下,实用工具有助于抽象出需要缓存/索引和查询任何API请求返回的数据的常见模式。

2.4.1 OnDemandLoaderTool

此工具将任何现有的LlamaIndex数据加载器(BaseReader类)转换为Agent可以使用的工具。该工具可以调用数据加载器所需的所有参数,以及一个自然语言查询字符串。在执行期间,首先从数据加载器加载数据,将其索引(例如使用向量存储),然后“按需”查询。这三个步骤在单个工具调用中发生。

这通常比弄清楚如何自己加载和索引API数据更可取。虽然这可能允许数据重用,但用户通常只需要一个临时索引来抽象掉任何API调用的提示窗口限制。

1
2
3
4
5
6
7
8
from llama_index.readers.wikipedia import WikipediaReader
from llama_index.core.tools.ondemand_loader_tool import OnDemandLoaderTool

tool = OnDemandLoaderTool.from_defaults(
reader,
name="Wikipedia Tool",
description="A tool for loading data and querying articles from Wikipedia",
)

2.4.2 LoadAndSearchToolSpec

LoadAndSearchToolSpec接受任何现有的工具作为输入。作为一个工具规范,它实现了to_tool_list,当调用该函数时,会返回两个工具:一个加载工具,然后是一个搜索工具。

加载工具的执行将调用底层工具,并使用向量索引(默认情况下)索引输出。搜索工具的执行将接受一个查询字符串作为输入,并调用底层索引。

这对于默认情况下返回大量数据的任何API都很有帮助——例如,WikipediaToolSpec默认将返回整个Wikipedia页面,这很容易溢出大多数LLM上下文窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
from llama_index.tools.wikipedia import WikipediaToolSpec
from llama_index.core.tools.tool_spec.load_and_search import (
LoadAndSearchToolSpec,
)

wiki_spec = WikipediaToolSpec()
# Get the search wikipedia tool
tool = wiki_spec.to_tool_list()[1]

# Create the Agent with load/search tools
agent = OpenAIAgent.from_tools(
LoadAndSearchToolSpec.from_defaults(tool).to_tool_list(), verbose=True
)

2.4.3 Return Direc

工具类构造函数中的return_direct选项。如果将其设置为True,则直接返回agent的响应,而不会被agent解释和重写。这对于减少运行时间或设计/指定将结束agent推理循环的工具非常有用。

1
2
3
4
5
6
7
8
9
10
tool = QueryEngineTool.from_defaults(
query_engine,
name="<name>",
description="<description>",
return_direct=True,
)

agent = OpenAIAgent.from_tools([tool])

response = agent.chat("<question that invokes tool>")

在上面的示例中,将调用查询引擎工具,并且该工具的响应将直接作为响应返回,并且执行循环将结束。
如果使用了return_direct=False,那么agent将使用聊天历史的上下文重写响应,甚至可能再次调用工具。

完整示例

2.4.4 Debugging Tools

通常,调试正在发送到API的工具定义的确切内容可能会很有用。可以通过使用底层函数来获取当前工具模式的一瞥,该模式在OpenAI和Anthropic等API中被使用。

1
2
schema = tool.metadata.get_parameters_dict()
print(schema)

三、Lower-Level Agent API

LlamaIndex提供了一个Lower-Level Agent API,它提供了一系列超越简单执行用户查询端到端的能力。这些能力允许以更细粒度的方式逐步控制 agent。最终目标是,可以在数据上创建可靠的agent软件系统。

灵感来源

3.1 High-Level Agent Architecture

LlamaIndex的Agents由与 AgentWorkers 交互的 AgentRunner 对象组成:

  • AgentRunners 是存储状态(包括对话记忆)的协调器,创建和维护任务,通过每个任务运行步骤,并向用户提供用于交互的面向High-Level的接口。
  • AgentWorkers 控制任务的逐步执行。给定一个输入步骤,agent worker 负责生成下一个步骤。它们可以初始化参数,并根据从 Task/TaskStep 对象传递下来的状态采取行动,但本身不存储状态。外部的 AgentRunner 负责调用 AgentWorker 并收集/聚合结果。

一些辅助类:

  • Task:High-Level任务,接收用户查询 + 传递其他信息,如记忆
  • TaskStep:代表单个步骤。将其作为输入输入到 AgentWorker,得到 TaskStepOutput。完成一个任务可能涉及多个 TaskStep
  • TaskStepOutput:给定步骤执行的输出。输出任务是否完成。

3.1.1 Benefits

以下是使用lower-level API 的一些好处:

  • 将任务创建与执行解耦 - 控制何时执行特定任务。
  • 获取每个步骤执行的更大调试性。
  • 获取更大的可见性:查看完成的步骤和下一步。
  • [即将推出] 可控性:通过注入人为反馈直接控制/修改中间步骤
  • 放弃任务:如果任务在执行过程中偏离轨道,可以放弃,而不影响核心Agent记忆。
  • [即将推出] 撤销步骤。
  • 更容易定制:通过实现 AgentWorker,很容易子类化/实现新的Agent算法(包括 ReAct、OpenAI,但也包括计划+解决,LLMCompiler)。

3.1.2 示例

可以使用 OpenAIAgentReActAgent,或者通过 AgentRunnerAgentWorker 创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from llama_index.core.agent import AgentRunner
from llama_index.agent.openai import OpenAIAgentWorker

# construct OpenAIAgent from tools
openai_step_engine = OpenAIAgentWorker.from_tools(tools, llm=llm, verbose=True)
agent = AgentRunner(openai_step_engine)

# create task
task = agent.create_task("What is (121 * 3) + 42?")

# execute step
step_output = agent.run_step(task)

# if step_output is done, finalize response
if step_output.is_last:
response = agent.finalize_response(task.task_id)

# list tasks
task.list_tasks()

# get completed steps
task.get_completed_steps(task.task_id)

print(str(response))

四、使用示例

一个agent是从一组工具(Tools)初始化的。以下是从一组工具中实例化一个ReAct agent的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI
from llama_index.core.agent import ReActAgent


# define sample Tool
def multiply(a: int, b: int) -> int:
"""Multiple two integers and returns the result integer"""
return a * b


multiply_tool = FunctionTool.from_defaults(fn=multiply)

# initialize llm
llm = OpenAI(model="gpt-3.5-turbo-0613")

# initialize ReAct agent
agent = ReActAgent.from_tools([multiply_tool], llm=llm, verbose=True)

agent支持聊天和查询,分别继承自ChatEngineQueryEngine

1
agent.chat("What is 2123 * 215123")

要自动选择根据LLM的最佳Agent,可以使用from_llm方法来生成一个Agent。

1
2
3
from llama_index.core.agent import AgentRunner

agent = AgentRunner.from_llm([multiply_tool], llm=llm, verbose=True)

4.1 定义Tools

4.1.1 Query Engine Tools

将查询引擎作为工具包装到Agent中也很容易。只需执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import QueryEngineTool

# NOTE: lyft_index and uber_index are both SimpleVectorIndex instances
lyft_engine = lyft_index.as_query_engine(similarity_top_k=3)
uber_engine = uber_index.as_query_engine(similarity_top_k=3)

query_engine_tools = [
QueryEngineTool(
query_engine=lyft_engine,
metadata=ToolMetadata(
name="lyft_10k",
description="Provides information about Lyft financials for year 2021. "
"Use a detailed plain text question as input to the tool.",
),
return_direct=False,
),
QueryEngineTool(
query_engine=uber_engine,
metadata=ToolMetadata(
name="uber_10k",
description="Provides information about Uber financials for year 2021. "
"Use a detailed plain text question as input to the tool.",
),
return_direct=False,
),
]

# initialize ReAct agent
agent = ReActAgent.from_tools(query_engine_tools, llm=llm, verbose=True)

4.1.2 使用其他Agents作为Tools

Agent的一个巧妙特性是,由于它们继承自BaseQueryEngine,你以很容易地通过QueryEngineTool将其他Agent定义为工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from llama_index.core.tools import QueryEngineTool

query_engine_tools = [
QueryEngineTool(
query_engine=sql_agent,
metadata=ToolMetadata(
name="sql_agent", description="Agent that can execute SQL queries."
),
),
QueryEngineTool(
query_engine=gmail_agent,
metadata=ToolMetadata(
name="gmail_agent",
description="Tool that can send emails on Gmail.",
),
),
]

outer_agent = ReActAgent.from_tools(query_engine_tools, llm=llm, verbose=True)

4.2 Agent With Planning

将初始任务分解为更易于消化的子任务是一种强大的模式。LlamaIndex提供了一个Agent规划模块,正是为此而设计的:

1
2
3
4
5
6
7
8
from llama_index.agent.openai import OpenAIAgentWorker
from llama_index.core.agent import (
StructuredPlannerAgent,
FunctionCallingAgentWorker,
)

worker = FunctionCallingAgentWorker.from_tools(tools, llm=llm)
agent = StructuredPlannerAgent(worker)

通常,这个Agent可能比基本的AgentRunner类响应时间更长,但输出通常会更完整。另一个需要考虑的权衡是规划通常需要一个能力较强的LLM(就上下文而言,gpt-3.5-turbo在规划方面有时不稳定,而gpt-4-turbo表现得更好。)

完整例子

4.3 Lower-Level API

OpenAIAgent和ReActAgent是AgentRunnerAgentWorker交互的简单包装。
所有Agent都可以以这种方式定义。例如,对于OpenAIAgent:

1
2
3
4
5
6
from llama_index.core.agent import AgentRunner
from llama_index.agent.openai import OpenAIAgentWorker

# construct OpenAIAgent from tools
openai_step_engine = OpenAIAgentWorker.from_tools(tools, llm=llm, verbose=True)
agent = AgentRunner(openai_step_engine)

4.4 自定义Agent

如果你想自定义Agent,可以选择继承CustomSimpleAgentWorker,并将其插入到AgentRunner中。

1
2
3
4
5
6
7
8
9
10
11
12
from llama_index.core.agent import CustomSimpleAgentWorker


class MyAgentWorker(CustomSimpleAgentWorker):
"""Custom agent worker."""

# define class here
pass


# Wrap the worker into an AgentRunner
agent = MyAgentWorker(...).as_agent()

完整例子

4.5 高级概念(针对OpenAIAgent,测试版)

还可以在更高级的设置中使用Agent。例如,在查询时能够从一个索引中检索工具,并能够对一组现有的工具执行查询规划。
这些主要是通过OpenAIAgent类实现的(它们依赖于OpenAI Function API)。

4.5.1 Function Retrieval Agents

如果工具集非常大,可以创建一个ObjectIndex来索引这些工具,然后在查询时将一个ObjectRetriever传递给Agent,以便首先动态检索相关工具,然后让Agent从候选工具中选择。

首先构建一个现有的工具集上的ObjectIndex

1
2
3
4
5
6
7
8
# define an "object" index over these tools
from llama_index.core import VectorStoreIndex
from llama_index.core.objects import ObjectIndex

obj_index = ObjectIndex.from_objects(
all_tools,
index_cls=VectorStoreIndex,
)

定义OpenAIAgent

1
2
3
4
5
from llama_index.agent.openai import OpenAIAgent

agent = OpenAIAgent.from_tools(
tool_retriever=obj_index.as_retriever(similarity_top_k=2), verbose=True
)

完整例子

4.5.2 Context Retrieval Agents

上下文增强型OpenAI Agent在调用任何工具之前总是执行检索。
这有助于提供额外的上下文,可以帮助Agent更好地选择工具,而不仅仅是在没有任何上下文的情况下尝试做出决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from llama_index.core import Document
from llama_index.agent.openai_legacy import ContextRetrieverOpenAIAgent


# toy index - stores a list of Abbreviations
texts = [
"Abbreviation: X = Revenue",
"Abbreviation: YZ = Risk Factors",
"Abbreviation: Z = Costs",
]
docs = [Document(text=t) for t in texts]
context_index = VectorStoreIndex.from_documents(docs)

# add context agent
context_agent = ContextRetrieverOpenAIAgent.from_tools_and_retriever(
query_engine_tools,
context_index.as_retriever(similarity_top_k=1),
verbose=True,
)
response = context_agent.chat("What is the YZ of March 2022?")

4.5.3 Query Planning

OpenAI Function Agents能够进行高级查询规划。诀窍是为Agent提供一个QueryPlanTool - 如果Agent调用QueryPlanTool,它将被迫推断一个完整的Pydantic模式,代表对一组子工具的查询计划。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# define query plan tool
from llama_index.core.tools import QueryPlanTool
from llama_index.core import get_response_synthesizer

response_synthesizer = get_response_synthesizer(
service_context=service_context
)
query_plan_tool = QueryPlanTool.from_defaults(
query_engine_tools=[query_tool_sept, query_tool_june, query_tool_march],
response_synthesizer=response_synthesizer,
)

# initialize agent
agent = OpenAIAgent.from_tools(
[query_plan_tool],
max_function_calls=10,
llm=OpenAI(temperature=0, model="gpt-4-0613"),
verbose=True,
)

# should output a query plan to call march, june, and september tools
response = agent.query(
"Analyze Uber revenue growth in March, June, and September"
)

官方资源


LlamaIndex(八)——LlamaIndex Agents
https://mztchaoqun.com.cn/posts/D21_LlamaIndex_Agents/
作者
mztchaoqun
发布于
2024年5月22日
许可协议