宣布我们的 LlamaCloud 正式发布(以及我们的 1900 万美元 A 轮融资)!
LlamaIndex

Jerry Liu 2023-07-12

数据代理

今天,我们非常激动地宣布 LlamaIndex 内一项重要的新功能发布:数据代理

数据代理是由大型语言模型驱动的知识工作者,能够智能地对您的数据执行各种任务,包括“读取”和“写入”功能。它们具有以下能力:

  • 对不同类型的数据——非结构化、半结构化和结构化数据——执行自动化搜索和检索。
  • 以结构化的方式调用任何外部服务 API。它们可以立即处理响应,或将此数据索引/缓存供将来使用。
  • 存储对话历史。
  • 利用以上所有功能来完成简单和复杂的数据任务。

我们在代理侧和工具侧都努力提供了抽象、服务和指南,以便构建数据代理。今天发布的版本包含以下关键组件:

详细信息请见下文。我们将向您展示如何构建一个 Gmail 代理,它能够在不到 10 行代码的情况下自动创建/发送电子邮件!

背景

LlamaIndex 的核心使命是释放大型语言模型对您的外部数据源的全部能力。它提供了一套工具来定义“状态”(如何解析/结构化您的数据)和“计算”(如何查询您的数据)。直到现在,我们的框架主要关注搜索和检索用例。我们拥有一套强大的工具和能力,不仅允许您围绕向量数据库 + top-k 检索构建基本的 RAG 堆栈,还提供了 远超于此 的更多功能。

很多技术曾经存在于我们的查询引擎中。我们的目标是增强查询引擎回答各种不同查询的能力。为此,我们必须改进这些查询引擎的“推理”能力。因此,我们现有的一些查询能力包含“代理式”组件:我们有能够进行思维链推理、查询分解和路由的查询引擎。在此过程中,用户可以选择具有更受限推理能力到较少受限能力的查询引擎。

但是大型语言模型还有巨大的机会与数据进行更丰富的交互;它们应该能够对任何一套工具进行通用推理,无论是来自数据库还是 API。它们还应该具备“读取”和“写入”能力——不仅理解状态,还能修改状态。因此,它们应该能够做比从静态知识源搜索和检索更多的事情。

一些现有的 服务工具包研究 论文 已经展示了大型语言模型驱动的“代理”与外部环境交互的可能性。以这些现有方法为灵感,我们看到了一个机会,可以构建一系列有原则的抽象,使任何人都能基于他们的数据构建知识工作者。

数据代理的核心组件

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

  • 推理循环
  • 工具抽象

从高层次来看,数据代理被提供了一组 API 或工具来与之交互。这些 API 可以返回有关世界的信息,或执行修改状态的操作。每个工具都暴露了一个请求/响应接口。请求是一组结构化参数,响应可以是任何格式(至少在概念上是如此,在大多数情况下,这里的响应是某种形式的文本字符串)。

给定一个输入任务,数据代理使用一个推理循环来决定使用哪些工具、按什么顺序以及调用每个工具的参数。“循环”在概念上可以非常简单(一步式工具选择过程),也可以很复杂(多步选择过程,其中每一步都会选择大量工具)。

下面将详细描述这些组件。

代理抽象 + 推理循环

我们支持以下代理:

  • OpenAI 函数代理(构建在 OpenAI 函数 API 之上)
  • ReAct 代理(可与任何聊天/文本完成端点一起使用)。

您可以按如下方式使用它们:

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?")

每个代理都接受一组工具。我们工具抽象的详细信息如下所示。每个代理还支持两种主要的输入任务方法——chatquery。请注意,这些分别是我们的 ChatEngineQueryEngine 中使用的核心方法。事实上,我们的基础代理类(BaseAgent)简单地继承自 BaseChatEngineBaseQueryEnginechat 允许代理利用之前存储的对话历史,而 query 是无状态调用 - 历史/状态不会随时间保留。

推理循环取决于代理类型。OpenAI 代理在一个 while 循环中调用 OpenAI 函数 API,因为工具决策逻辑内置于函数 API 中。给定输入提示和先前的聊天历史(包括先前的函数调用),函数 API 将决定是进行另一个函数调用(选择一个工具),还是返回一个助手消息。如果 API 返回函数调用,那么我们负责执行该函数并在聊天历史中传入一个函数消息。如果 API 返回助手消息,则循环完成(我们假定任务已解决)。

ReAct 代理使用通用的文本完成端点,因此它可以与任何大型语言模型一起使用。文本完成端点具有简单的输入字符串 → 输出字符串格式,这意味着推理逻辑必须编码在提示中。ReAct 代理使用受 ReAct 论文启发(并适应其他版本)的输入提示来决定选择哪个工具。它看起来像这样:

...
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
```
...

我们在聊天提示中原生实现了 ReAct;推理循环被实现为助手和用户消息的交替序列。Thought/Action/Action Input(思考/行动/行动输入)部分表示为助手消息,Observation(观察)部分表示为用户消息。

注意: ReAct 提示不仅期望要选择的工具名称,还期望以 JSON 格式填充工具的参数。这使得其输出与 OpenAI 函数 API 的输出并非截然不同——主要区别在于,在函数 API 的情况下,工具选择逻辑内置于 API 本身(通过微调模型),而在这里则是通过显式提示来诱导的。

工具抽象

拥有恰当的工具抽象是构建数据代理的核心。定义一组工具类似于定义任何 API 接口,区别在于这些工具是为代理而非人类使用而设计的。我们允许用户定义单个工具,以及包含一系列内部函数的“ToolSpec”(工具规范)。

我们将描述基础工具抽象,以及如何轻松地在现有查询引擎、其他工具之上定义工具。

基础工具抽象

基础工具定义了一个非常通用的接口。__call__ 函数可以接受任何系列参数,并返回一个通用的 ToolOutput 容器,该容器可以捕获任何响应。工具还包含元数据,包括其名称、描述和函数模式。

@dataclass
class ToolMetadata:
    description: str
    name: Optional[str] = None
    fn_schema: Optional[Type[BaseModel]] = DefaultToolFnSchema

class BaseTool:
    @property
    @abstractmethod
    def metadata(self) -> ToolMetadata:
        pass
    @abstractmethod
    def __call__(self, input: Any) -> ToolOutput:
        pass

函数工具

函数工具允许用户轻松地将任何函数转换为工具。它接受一个用户定义的函数(可以接受任何输入/输出),并将其包装到工具接口中。如果事先未指定函数模式,它还可以“自动推断”函数模式。

我们的 ToolSpec 类利用此 FunctionTool 抽象,将工具规范中定义的函数转换为一组代理工具(见下文)。

下面是一个定义 FunctionTool 的简单示例。

from llama_index.tools.function_tool import FunctionTool

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)

查询引擎工具

当然,我们也提供了工具抽象来封装我们现有的查询引擎。这提供了从处理查询引擎到处理代理的无缝过渡。我们的查询引擎可以被视为用于读/写设置并围绕检索目的构建的“受限”代理。这些查询引擎可以在整个代理设置中使用。

from llama_index.tools import QueryEngineTool

query_engine_tools = [
    QueryEngineTool(
        query_engine=query_engine, 
        metadata=ToolMetadata(
            name='<tool_name>', 
            description="Queries over X data source."
        )
    ),
 ...
]

工具规范

工具规范是一个 Python 类,它代表了代理可以与之交互的完整 API 规范,并且可以将工具规范转换为代理可以初始化的一系列工具。

这个类允许用户定义整个服务,而不仅仅是执行单个任务的工具。每个工具规范可能包含读/写端点,允许代理以有意义的方式与服务交互。例如,Slack 工具规范可以允许用户读取现有消息和频道(load_data, fetch_channels),以及发送消息(send_message)。它的大致定义如下:

class SlackToolSpec(BaseToolSpec):
    """Slack tool spec."""
    spec_functions = ["load_data", "send_message", "fetch_channels"]

    def load_data(
          self,
          channel_ids: List[str],
          reverse_chronological: bool = True,
      ) -> List[Document]:
          """Load data from the input directory."""
          ...
      def send_message(
          self,
          channel_id: str,
          message: str,
      ) -> None:
          """Send a message to a channel given the channel ID."""
          ...
      def fetch_channels(
          self,
      ) -> List[str]:
          """Fetch a list of relevant channels."""
          ...

如果一个工具规范被初始化,它可以通过 to_tool_list 转换为一个工具列表,该列表可以提供给代理。例如:

tool_spec = SlackToolSpec()
# initialize openai agent
agent = OpenAIAgent.from_tools(tool_spec.to_tool_list(), llm=llm, verbose=True)

定义工具规范与定义 Python 类没有太大区别。每个函数都被转换为一个工具,并且默认情况下,每个函数的文档字符串被用作工具描述(尽管您可以在 to_tool_list(func_to_metadata_mapping=...) 中自定义名称/描述)。

我们还特意选择让输入参数和返回类型可以是任何类型。主要原因是为后续版本的代理保留工具接口的通用性。即使当前版本的代理期望工具输出为字符串格式,未来可能会发生变化,我们不想随意限制工具接口的类型。

LlamaHub 工具仓库

本次发布的一个重要组成部分是 LlamaHub 中的全新增添:工具仓库。工具仓库包含 15+ 个工具规范,代理可以使用这些规范。这些工具规范代表了一份经过初步策划的服务列表,代理可以与这些服务交互并丰富其执行不同操作的能力。

其中包括以下规范:

  • Gmail 规范
  • Zapier 规范
  • Google 日历规范
  • OpenAPI 规范
  • SQL + 向量数据库规范

我们还提供了一系列实用工具,这些工具在设计代理与返回大量数据的不同 API 服务交互时,有助于抽象化痛点。

例如,我们的 Gmail 工具规范允许代理搜索现有电子邮件、创建草稿、更新草稿和发送电子邮件。我们的 Zapier 规范允许代理通过其 自然语言操作 接口向 Zapier 执行任何自然语言查询。

最棒的是,您无需花费大量时间弄清楚如何使用这些工具——我们提供了 10+ 个 Jupyter Notebook,展示了如何为每个服务构建代理,甚至构建结合使用多个服务(例如 Gmail、Google 日历和搜索)的代理。

示例演示

让我们看几个例子吧!我们使用 Gmail 规范初始化一个 OpenAIAgent。如上所述,该规范包含搜索电子邮件、创建/更新草稿和发送电子邮件的工具。

现在,让我们给代理一系列命令,以便它可以创建电子邮件草稿,对其进行一些编辑,然后发送出去。

首先,让我们创建一个初始电子邮件草稿。请注意,代理选择了 create_draft 工具,该工具接受“收件人”、“主题”和“消息”参数。代理在选择工具的同时能够推断出参数。

接下来,让我们稍微修改草稿并更新它

接下来,让我们看看草稿的当前状态。

最后,发送电子邮件!

这是一个很好的开始,但这仅仅是开始。我们正在积极地向此仓库贡献更多工具,并且也向社区开放贡献。如果您有兴趣为 LlamaHub 贡献一个工具,请随时在此仓库中提交 PR。

实用工具

通常,直接查询 API 可能会返回大量数据,这本身可能会溢出大型语言模型的上下文窗口(或者至少不必要地增加您使用的 token 数量)。

为了解决这个问题,我们在核心 LlamaIndex 仓库中提供了一组初始的“实用工具”——实用工具在概念上不与特定的服务(例如 Gmail、Notion)绑定,而是可以增强现有工具的能力。在这种特定情况下,实用工具有助于抽象化缓存/索引和查询从任何 API 请求返回的数据的常见模式。

下面让我们介绍一下我们的两个主要实用工具。

OnDemandLoaderTool

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

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

下面提供了一个使用示例:

from llama_hub.wikipedia.base import WikipediaReader
from llama_index.tools.on_demand_loader_tool import OnDemandLoaderTool

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

LoadAndSearchToolSpec

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

load 工具执行会调用底层工具,并索引输出(默认使用向量索引)。 的 search 工具执行会接受查询字符串作为输入并调用底层索引。

这对于默认返回大量数据的任何 API 端点都很有帮助——例如,我们的 WikipediaToolSpec 默认会返回完整的维基百科页面,这很容易溢出大多数大型语言模型的上下文窗口。

下面展示了使用示例:

from llama_hub.tools.wikipedia.base import WikipediaToolSpec
from llama_index.tools.tool_spec.load_and_search.base 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
)

这是我们运行输入提示时的输出:

agent.chat('what is the capital of poland')

输出

=== Calling Function ===
Calling function: search_data with args: {
  "query": "capital of Poland"
}
Got output: Content loaded! You can now search the information using read_search_data
========================
=== Calling Function ===
Calling function: read_search_data with args: {
  "query": "What is the capital of Poland?"
}
Got output: 
The capital of Poland is Warsaw.
========================
AgentChatResponse(response='The capital of Poland is Warsaw.', sources=[])

注意,代理会首先调用“load”工具(由工具的原始名称“search_data”表示)。这个 load 工具会在底层加载维基百科页面并进行索引。输出仅提及“内容已加载”,并告诉代理下一步是使用 read_search_data。然后,代理推理出它需要调用 read_search_data 工具,该工具将查询索引以获取正确答案。

常见问题

我应该使用数据代理进行搜索和检索,还是继续使用查询引擎?

简短回答:两者皆可。查询引擎让您可以以受限推理和非受限推理的方式在您的数据上定义自己的工作流程。例如,您可能希望使用我们的 NLStructStoreQueryEngine 定义特定的文本到 SQL 工作流程(受限),或者使用路由模块在语义搜索或摘要之间进行选择(较少受限),或者使用我们的 SubQuestionQueryEngine 将问题分解到子文档中(更少受限)。

默认情况下,代理循环不受约束,理论上可以对您提供的任何工具集进行推理。这意味着您可以获得开箱即用的高级搜索/检索功能——例如,在我们的 OpenAI Cookbook 中,我们展示了您只需提供一个 SQL 查询引擎和向量存储查询引擎作为工具,就可以获得联合的文本到 SQL 功能。但另一方面,以这种方式构建的代理可能相当不可靠(有关更多见解,请参阅我们的博客文章)。如果您使用代理进行搜索/检索,请注意 1)您选择的大型语言模型,以及 2)您选择的工具集。

LlamaIndex 数据代理与现有的代理框架(如 LangChain、Hugging Face 等)有何不同?

这些核心概念大多不新颖。我们的整体设计借鉴了流行工具和代理构建框架的灵感。但在我们的“数据代理”设计中,我们尽最大努力很好地回答了以下关键问题:

  • 我们如何有效地预先索引/查询和检索数据?
  • 我们如何有效地动态索引/查询和检索数据?
  • 我们如何设计读/写 API 接口,使其既丰富(可以接受结构化输入)又易于代理理解?
  • 我们如何在引文中正确获取来源?

我们设计数据代理的目标是创建能够对数据进行推理和交互的自动化知识工作者。我们的核心工具包为正确索引、检索和查询数据提供了基础——这些可以轻松集成工具。我们提供了一些额外的工具抽象来处理您希望动态“缓存”API 输出的情况(见上文)。最后,我们提供了有原则的工具抽象和设计原则,以便代理能够以结构化的方式与外部服务对接。

我可以使用工具与 LangChain 代理一起使用吗? 您也可以轻松地将我们的任何工具与 LangChain 代理一起使用。

tools = tool_spec.to_tool_list()
langchain_tools = [t.to_langchain_tool() for t in tools]

请参阅我们的 工具使用指南 以获取更多详细信息!

结论

总之,今天我们发布了两个关键项:数据代理组件(包括代理推理循环和工具抽象)以及 LlamaHub 工具仓库。

资源

我们在文档中编写了一个全面的部分——请看这里: https://gpt-index.readthedocs.io/en/latest/core_modules/agent_modules/agents/root.html

请查看我们的 LlamaHub 工具部分: https://llamahub.ai/

LlamaHub 工具的 Notebook 教程: https://github.com/emptycrown/llama-hub/tree/main/llama_hub/tools/notebooks

如果您有问题,请加入我们的 Discord: https://discord.gg/dGcwcsnxhU