Structured Tools

结构化工具

6 分钟阅读

TL;DR: 我们正在引入一个新的抽象概念,以允许使用更复杂的工具。之前的工具只接受单个字符串输入,而新工具可以接受任意数量、任意类型的输入。我们还引入了一个新的代理类,它可以很好地与这些新型工具配合使用。

重要链接

早在 2022 年 11 月我们首次发布 LangChain 时,代理和工具的使用就在我们的设计中扮演了核心角色。我们基于 ReAct 构建了首批链之一,这是一篇开创性的论文,将工具使用推到了提示框架的前沿。

在早期,工具的使用很简单。模型会生成两个字符串

  1. 工具名称
  2. 为所选工具提供的输入字符串

这种方法将代理限制为每次轮换只能使用一个工具,并且该工具的输入被限制为单个字符串。这些限制主要是由于模型的约束;模型甚至难以熟练地执行这些基本任务。可靠地执行更复杂的操作(例如选择多个工具或填充复杂模式)将是徒劳的。

然而,像 text-davinci-003gpt-3.5-turbogpt-4 这样更先进的语言模型的快速发展提高了可用模型可以可靠实现的水平。这促使我们重新评估 LangChain 代理框架内工具使用的限制。

今年早些时候,我们推出了“多动作”代理框架,代理可以在代理执行器的每个步骤中计划执行多个动作。在这一成功的基础上,我们现在正在突破单字符串输入的限制,并自豪地提供结构化工具支持!

结构化工具能够实现语言模型和工具之间更复杂、多方面的交互,从而更容易构建创新、适应性强且功能强大的应用程序。

什么是“结构化工具”?

结构化工具代表代理可以采取的动作。它封装了您提供的任何函数,使代理可以轻松地与之交互。结构化工具对象由以下内容定义:

  1. name:一个标签,告诉代理选择哪个工具。例如,名为“GetCurrentWeather”的工具告诉代理它是用于查找当前天气的。
  2. description:一份简短的说明手册,解释代理何时以及为何应使用该工具。
  3. args_schema:传达工具的接口以供代理使用。它通常从封装函数的签名中提取,并允许对工具输入进行额外的验证逻辑。
  4. _run_arun 函数:这些定义了工具的内部工作原理。它可以像返回当前时间这样简单,也可以像发送消息或控制机器人这样复杂。

工具 name 是其唯一标识符。一个好的名称可以明确地传达它的作用,因此名为“GetCurrentWeather”的工具比“GCTW”更有用。如果工具的名称对您来说不清楚,那么它可能对代理来说也不清楚。如果您正在向代理授予对多个工具的访问权限,则名称还可以提供有关它们之间关系的信息。例如,如果您有“AmazonSearch”、“AmazonCurrentBalance”和“NikeShoppingCart”工具,代理可以推断出前两个是相关的,即使不阅读描述也是如此。

description 提供了关于如何使用该工具的更详细的指示。一个好的描述应该简洁但有效地传达工具的作用。如果需要,这也可以提供空间来提供简短的示例(或反例)。

args_schema 是一个 Pydantic BaseModel,它定义了要馈送到工具的参数(以及它们的类型信息)。它有两个主要任务:首先,传达需要从代理获取哪些信息。第二个任务是在执行工具的内部功能之前验证这些输入。

最后,_run 和伴随的异步 _arun 方法定义了工具的逻辑。您可以在此处放置任何内容,从算术运算到 API 请求,再到对其他 LLM 链的调用。

新的结构化工具

除了这个新的基类之外,我们还发布了以下新的工具,它们都继承自这个结构化工具类。

  • 文件管理 - 一个用于您可能需要的所有文件系统操作的工具包,包括写入、grep、移动、复制、list_dir、find
  • Web 浏览器 - 虽然我们之前有用于文档加载器的浏览器,但我们现在发布了一个官方的有状态 PlayWright 浏览器工具包,让代理可以访问网站、单击、提交表单和查询数据

有关所有工具(旧的和新的)的列表,请参阅此处的文档 here

实现您自己的结构化工具

最快的入门方法是调用 StructuredTool.from_function(your_callable) 构造函数。

例如,假设您想要一个工具通过 requests 库与 Hugging Face 模型进行交互。

import requests
from langchain.tools.base import StructuredTool

API_KEY = "<MY-API-KEY>"

def get_huggingface_models(
    path: Optional[str] = None, query_params: Optional[dict] = None
) -> dict:
    """Tool that calls GET on <https://hugging-face.cn/models*> apis. Valid params include "search":"search", "author":"author", "filter":"filter" and "sort":"sort"."""
    base_url = "<https://hugging-face.cn/api/models>"
    headers = {"authorization": f"Bearer {API_KEY}"}
    result = requests.get(base_url + (path or ""), params=query_params, headers=headers)
    return result.json()

get_huggingface_models_tool = StructuredTool.from_function(get_huggingface_models)
models = get_huggingface_models_tool.run({"query_params": {"search": "gpt-j"}})
print(models)

在幕后,这会从函数的签名中推断出 args_schema。这用于告诉代理它可以提供查询参数进行搜索,以及路径参数来调用其他子端点。

如果您想要更多地控制工具定义,您可以直接子类化 BaseTool。例如,也许您希望 API 密钥从环境变量中自动加载。

from typing import Optional, Type

import aiohttp
import requests

from langchain.callbacks.manager import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)
from langchain.tools import BaseTool
from pydantic import BaseModel, BaseSettings, Field

class GetHuggingFaceModelsToolSchema(BaseModel):
    path: str = Field(default="", description="the api path")
    query_params: Optional[dict] = Field(
        default=None, description="Optional search parameters"
    )

class GetHuggingFaceModelsTool(BaseTool, BaseSettings):
    """My custom tool."""

    name: str = "get_huggingface_models"
    description: str = """Tool that calls GET on <https://hugging-face.cn/models*> apis. Valid params include "search":"search", "author":"author", "filter":"filter" and "sort":"sort"."""
    args_schema: Type[GetHuggingFaceModelsToolSchema] = GetHuggingFaceModelsToolSchema
    base_url: str = "<https://hugging-face.cn/api/models>"
    api_key: str = Field(..., env="HUGGINGFACE_API_KEY")

    @property
    def _headers(self) -> dict:
        return {"authorization": f"Bearer {self.api_key}"}

    def _run(
        self,
        path: str = "",
        query_params: Optional[dict] = None,
        run_manager: Optional[CallbackManagerForToolRun] = None,
    ) -> dict:
        """Run the tool"""
        result = requests.get(
            self.base_url + path, params=query_params, headers=self._headers
        )
        return result.json()

    async def _arun(
        self,
        path: str = "",
        query_params: Optional[dict] = None,
        run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
    ) -> dict:
        """Run the tool asynchronously."""

        async with aiohttp.ClientSession() as session:
            async with session.get(
                self.base_url + path, params=query_params, headers=self._headers
            ) as response:
                return await response.json()

get_models_tool = GetHuggingFaceModelsTool()
models = get_models_tool.run({"query_params": {"search": "gpt-j"}})
print(models)

如何使用结构化工具?

我们添加了一个新的 StructuredChatAgent,它可以与这些结构化工具原生配合使用。请参阅 此页面 以获取演练。

由于先前代理的默认提示和输出解析器的限制,它们在没有额外自定义的情况下无法有效地与结构化工具配合使用。

要开始使用,您可以使用以下代码片段实例化结构化聊天代理执行器

from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatAnthropic
tools = [] # Add any tools here
llm = ChatAnthropic(temperature=0) # or any other LLM
agent_chain = initialize_agent(tools, llm, agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION)

这些工具也与来自 langchain.experimentalAutoGPT 代理兼容。

常见问题解答

问:我可以在现有代理中使用结构化工具吗?

答:如果您的结构化工具接受一个字符串参数:可以,它仍然可以与现有代理一起使用。但是,具有多个参数的结构化工具与以下代理不直接兼容,需要进一步自定义

  • zero-shot-react-description
  • react-docstore
  • self-ask-with-search
  • conversational-react-description
  • chat-zero-shot-react-description
  • chat-conversational-react-description

问:我仍然可以创建字符串工具吗?

答:您仍然可以使用 Tool 构造函数和 @tool 装饰器来定义简单的字符串工具。从 BaseTool 类继承并接受单个字符串参数的工具仍将被视为字符串工具。

问:我可以在为 StructuredTool 构建的新代理中使用以前定义的字符串 BaseTool 吗?

答:是的!结构化工具不需要新的代理执行器,并且旧工具是向前兼容的。原始 Tool 类与 StructuredTool 共享相同的基类,这换句话说就是您的工具应该可以开箱即用。

期望 JSON 序列化字符串输入的工具可能需要进行一些修改才能与较新代理的输出解析器互操作,或者可以将它们更新为新格式,新格式应该为更复杂的接口提供更好的支持。