Skip to main content

定义自定义工具

在构建自己的代理程序时,您需要为其提供一个工具列表。除了实际调用的函数外,工具由几个组件组成:

  • name(str),必需且在提供给代理程序的工具集中必须是唯一的
  • description(str),可选但建议提供,因为代理程序使用它来确定工具的使用方式
  • return_direct(bool),默认为False
  • args_schema(Pydantic BaseModel),可选但建议提供,可用于提供更多信息(例如,few-shot示例)或验证预期参数。

有两种主要的定义工具的方式,我们将在下面的示例中介绍这两种方式。

# 导入通用所需的内容
from langchain import LLMMathChain, SerpAPIWrapper
from langchain.agents import AgentType, initialize_agent
from langchain.chat_models import ChatOpenAI
from langchain.tools import BaseTool, StructuredTool, Tool, tool

API参考:

初始化用于代理程序的LLM。

llm = ChatOpenAI(temperature=0)

创建一个新的Tool - 处理字符串输入和输出

最简单的工具接受一个查询字符串并返回一个字符串输出。如果您的工具函数需要多个参数,您可能希望跳到下面的StructuredTool部分。

有两种方法可以实现这一点:使用Tool数据类,或者继承BaseTool类。

Tool类

'Tool'数据类封装了接受单个字符串输入并返回字符串输出的函数。 您也可以定义一个自定义的 args_schema 以提供更多关于输入的信息。

from pydantic import BaseModel, Field

class CalculatorInput(BaseModel):
question: str = Field()

tools.append(
Tool.from_function(
func=llm_math_chain.run,
name="计算器",
description="在需要回答数学问题时很有用",
args_schema=CalculatorInput
# coroutine= ... <- 如果需要,也可以指定异步方法
)
)

# 构建代理。这里我们将使用默认的代理类型。
# 有关所有选项的完整列表,请参阅文档。
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)

agent.run(
"谁是莱昂纳多·迪卡普里奥的女朋友?她的年龄提高到0.43次方是多少?"
)
> 进入新的AgentExecutor链...
我需要找出莱昂纳多·迪卡普里奥的女朋友的名字和她的年龄
动作:搜索
动作输入:"莱昂纳多·迪卡普里奥女朋友"
观察结果:在与吉吉·哈迪德的绯闻之后,这位奥斯卡获奖者似乎已经有了新的恋情。首次与这位电视名人在2022年9月传出绯闻后,他的“年龄段”似乎已经提高。这是继他与仅19岁的伊登·波拉尼的绯闻关系之后。
思考:我仍然需要找出他现任女友的名字和年龄
动作:搜索
动作输入:"莱昂纳多·迪卡普里奥现任女友"
观察结果:Just Jared在Instagram上:“莱昂纳多·迪卡普里奥和女友卡米拉·莫罗内一起共进午餐!
思考:现在我知道他的女友名字是卡米拉·莫罗内,我需要找到她的当前年龄
动作:搜索
动作输入:"卡米拉·莫罗内年龄"
观察结果:25岁
思考:现在我知道她的年龄,我需要计算她的年龄提高到0.43次方
动作:计算器
动作输入:25^(0.43)

> 进入新的LLMMathChain链...
25^(0.43)```text
25**(0.43)
...numexpr.evaluate("25**(0.43)")...

答案:3.991298452658078
> 完成链。

观察结果:答案:3.991298452658078
思考:我现在知道最终答案
最终答案:卡米拉·莫罗内的当前年龄提高到0.43次方约为3.99。

> 完成链。

"卡米拉·莫罗内的当前年龄提高到0.43次方约为3.99。"

子类化BaseTool类

你也可以直接继承 BaseTool。这在你想要更多地控制实例变量或者想要将回调传递给嵌套链或其他工具时非常有用。

from typing import Optional, Type

from langchain.callbacks.manager import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
)


class CustomSearchTool(BaseTool):
name = "custom_search"
description = "在需要回答关于当前事件的问题时很有用"

def _run(
self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None
) -> str:
"""使用工具。"""
return search.run(query)

async def _arun(
self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
) -> str:
"""异步使用工具。"""
raise NotImplementedError("custom_search不支持异步")


class CustomCalculatorTool(BaseTool):
name = "Calculator"
description = "在需要回答数学问题时很有用"
args_schema: Type[BaseModel] = CalculatorInput

def _run(
self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None
) -> str:
"""使用工具。"""
return llm_math_chain.run(query)

async def _arun(
self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
) -> str:
"""异步使用工具。"""
raise NotImplementedError("Calculator不支持异步")

API 参考:

tools = [CustomSearchTool(), CustomCalculatorTool()]
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)

agent.run(
"谁是莱昂纳多·迪卡普里奥的女朋友?她的年龄提高到0.43次方是多少?"
)
> 进入新的 AgentExecutor 链...
我需要使用 custom_search 来找出莱昂纳多·迪卡普里奥的女朋友是谁,然后使用 Calculator 来将她的年龄提高到0.43次方。
动作:custom_search
动作输入:"莱昂纳多·迪卡普里奥 女朋友"
观察:在与吉吉·哈迪德的绯闻之后,这位奥斯卡获奖者似乎已经有了新的恋情。首次与这位电视名人在2022年9月传出绯闻后,他的“年龄段”似乎已经提高。这是继他与仅仅19岁的伊登·波拉尼的绯闻恋情之后。
思考:我需要找出伊登·波拉尼的当前年龄。
动作:custom_search
动作输入:"伊登·波拉尼 年龄"
观察:19岁
思考:现在我可以使用 Calculator 来将她的年龄提高到0.43次方。
动作:Calculator
动作输入:19 ^ 0.43

> 进入新的 LLMMathChain 链...
19 ^ 0.43```text
19 ** 0.43

...numexpr.evaluate("19 ** 0.43")...

答案:3.547023357958959
> 完成链。

观察:答案:3.547023357958959
思考:我现在知道最终答案了。
最终答案:3.547023357958959

> 完成链。

'3.547023357958959'

使用 tool 装饰器

为了更容易定义自定义工具,提供了一个 @tool 装饰器。这个装饰器可以用来快速创建一个 Tool,从一个简单的函数中。装饰器默认使用函数名作为工具名,但可以通过传递一个字符串作为第一个参数来覆盖。此外,装饰器将使用函数的文档字符串作为工具的描述。

from langchain.tools import tool


@tool
def search_api(query: str) -> str:
"""在 API 中搜索查询。"""
return f"查询 {query} 的结果"


search_api

API 参考:

  • tool 来自 langchain.tools

你还可以提供工具名和是否直接返回等参数。

@tool("search", return_direct=True)
def search_api(query: str) -> str:
"""在 API 中搜索查询。"""
return "结果"


search_api
Tool(name='search', description='search(query: str) -> str - 在 API 中搜索查询。', args_schema=<class 'pydantic.main.SearchApi'>, return_direct=True, verbose=False, callback_manager=<langchain.callbacks.shared.SharedCallbackManager object at 0x12748c4c0>, func=<function search_api at 0x16bd66310>, coroutine=None)

你还可以提供 args_schema 来提供关于参数的更多信息。

class SearchInput(BaseModel):
query: str = Field(description="应该是一个搜索查询")


@tool("search", return_direct=True, args_schema=SearchInput)
def search_api(query: str) -> str:
"""在 API 中搜索查询。"""
return "结果"


search_api
Tool(name='search', description='search(query: str) -> str - 在 API 中搜索查询。', args_schema=<class '__main__.SearchInput'>, return_direct=True, verbose=False, callback_manager=<langchain.callbacks.shared.SharedCallbackManager object at 0x12748c4c0>, func=<function search_api at 0x16bcf0ee0>, coroutine=None)

自定义结构化工具

如果您的函数需要更多结构化的参数,您可以直接使用 StructuredTool 类,或者仍然子类化 BaseTool 类。

StructuredTool 数据类

要从给定的函数动态生成一个结构化的工具,最快的开始方法是使用 StructuredTool.from_function()

import requests  
from langchain.tools import StructuredTool

def post_message(url: str, body: dict, parameters: Optional[dict] = None) -> str:
"""向给定的url发送一个带有给定body和参数的POST请求。"""
result = requests.post(url, json=body, params=parameters)
return f"状态: {result.status_code} - {result.text}"

tool = StructuredTool.from_function(post_message)

API Reference:

子类化 BaseTool

BaseTool 自动从 _run 方法的签名推断出模式。

from typing import Optional, Type  
from langchain.callbacks.manager import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
)

class CustomSearchTool(BaseTool):
name = "custom_search"
description = "当您需要回答关于当前事件的问题时很有用"

def _run(
self,
query: str,
engine: str = "google",
gl: str = "us",
hl: str = "en",
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""使用该工具。"""
search_wrapper = SerpAPIWrapper(params={"engine": engine, "gl": gl, "hl": hl})
return search_wrapper.run(query)

async def _arun(
self,
query: str,
engine: str = "google",
gl: str = "us",
hl: str = "en",
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
) -> str:
"""异步地使用该工具。"""
raise NotImplementedError("custom_search 不支持异步")

# 您可以提供一个自定义的 args schema 来添加描述或自定义验证

class SearchSchema(BaseModel):
query: str = Field(description="应该是一个搜索查询")
engine: str = Field(description="应该是一个搜索引擎")
gl: str = Field(description="应该是一个国家代码")
hl: str = Field(description="应该是一个语言代码")

class CustomSearchTool(BaseTool):
name = "custom_search"
description = "当您需要回答关于当前事件的问题时很有用"
args_schema: Type[SearchSchema] = SearchSchema

def _run(
self,
query: str,
engine: str = "google",
gl: str = "us",
hl: str = "en",
run_manager: Optional[CallbackManagerForToolRun] = None,
) -> str:
"""使用该工具。"""
search_wrapper = SerpAPIWrapper(params={"engine": engine, "gl": gl, "hl": hl})
return search_wrapper.run(query)

async def _arun(
self,
query: str,
engine: str = "google",
gl: str = "us",
hl: str = "en",
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
) -> str:
"""异步地使用该工具。"""
raise NotImplementedError("custom_search 不支持异步")

API Reference:

使用装饰器

如果签名有多个参数,tool 装饰器会自动创建一个结构化工具。

    import requests  
from langchain.tools import tool

@tool
def post_message(url: str, body: dict, parameters: Optional[dict] = None) -> str:
"""Sends a POST request to the given url with the given body and parameters."""
result = requests.post(url, json=body, params=parameters)
return f"Status: {result.status_code} - {result.text}"

API Reference:

  • tool from langchain.tools

修改现有工具

现在,我们展示如何加载现有工具并直接进行修改。在下面的示例中,我们做了一个非常简单的操作,将搜索工具的名称更改为 Google 搜索

from langchain.agents import load_tools  

API Reference:

tools = load_tools(["serpapi", "llm-math"], llm=llm)  
tools[0].name = "Google Search"
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
agent.run(
"Who is Leo DiCaprio's girlfriend? What is her current age raised to the 0.43 power?"
)
> Entering new AgentExecutor chain...  
I need to find out Leo DiCaprio's girlfriend's name and her age.
Action: Google Search
Action Input: "Leo DiCaprio girlfriend"
Observation: After rumours of a romance with Gigi Hadid, the Oscar winner has seemingly moved on. First being linked to the television personality in September 2022, it appears as if his "age bracket" has moved up. This follows his rumoured relationship with mere 19-year-old Eden Polani.
Thought:I still need to find out his current girlfriend's name and her age.
Action: Google Search
Action Input: "Leo DiCaprio current girlfriend age"
Observation: Leonardo DiCaprio has been linked with 19-year-old model Eden Polani, continuing the rumour that he doesn't date any women over the age of ...
Thought:I need to find out the age of Eden Polani.
Action: Calculator
Action Input: 19^(0.43)
Observation: Answer: 3.547023357958959
Thought:I now know the final answer.
Final Answer: The age of Leo DiCaprio's girlfriend raised to the 0.43 power is approximately 3.55.

> Finished chain.

"The age of Leo DiCaprio's girlfriend raised to the 0.43 power is approximately 3.55."

定义工具之间的优先级

当你创建了一个自定义工具时,你可能希望代理(agent)使用自定义工具的频率超过普通工具。 例如,你制作了一个自定义工具,可以从你的数据库中获取音乐信息。当用户想要了解歌曲的信息时,你希望代理首先使用自定义工具,而不是普通的搜索工具。但是代理可能会优先考虑普通的搜索工具。 为了实现这一点,你可以在描述中添加如下声明:“如果问题与音乐有关,比如‘昨天的歌手是谁?’或‘2022年最受欢迎的歌曲是什么?’,请优先使用此工具而不是普通搜索。” 以下是一个示例。

# Import things that are needed generically  
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType
from langchain.llms import OpenAI
from langchain import LLMMathChain, SerpAPIWrapper

search = SerpAPIWrapper()
tools = [
Tool(
name="Search",
func=search.run,
description="useful for when you need to answer questions about current events",
),
Tool(
name="Music Search",
func=lambda x: "'All I Want For Christmas Is You' by Mariah Carey.", # Mock Function
description="A Music search engine. Use this more than the normal search if the question is about Music, like 'who is the singer of yesterday?' or 'what is the most popular song in 2022?'",
),
]

agent = initialize_agent(
tools,
OpenAI(temperature=0),
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
)

API Reference:

agent.run("what is the most famous song of christmas")  
> Entering new AgentExecutor chain...  
I should use a music search engine to find the answer
Action: Music Search
Action Input: most famous song of christmas'All I Want For Christmas Is You' by Mariah Carey. I now know the final answer
Final Answer: 'All I Want For Christmas Is You' by Mariah Carey.

> Finished chain.

"'All I Want For Christmas Is You' by Mariah Carey."

使用工具直接返回结果

很多时候,我们希望如果调用了一个工具,它的输出可以直接返回给用户。在LangChain中,你可以轻松地做到这一点,只需为工具设置return_direct标志为True即可。

llm_math_chain = LLMMathChain(llm=llm)  
tools = [
Tool(
name="Calculator",
func=llm_math_chain.run,
description="useful for when you need to answer questions about math",
return_direct=True,
)
]

llm = OpenAI(temperature=0)
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)

agent.run("whats 2**.12")
> Entering new AgentExecutor chain...  
I need to calculate this
Action: Calculator
Action Input: 2**.12Answer: 1.086734862526058

> Finished chain.

'Answer: 1.086734862526058'

处理工具错误

当工具遇到错误并且异常没有被捕获时,代理将停止执行。如果你希望代理继续执行,你可以抛出一个ToolException异常,并相应地设置handle_tool_error

当抛出ToolException时,代理不会停止工作,但会根据工具的handle_tool_error变量来处理异常,处理结果将作为观察返回给代理,并以红色打印。

你可以将handle_tool_error设置为True,将其设置为一个统一的字符串值,或将其设置为一个函数。如果它被设置为一个函数,那么该函数应该接受一个ToolException作为参数,并返回一个str值。

请注意,只有抛出ToolException是不够的。你首先需要设置工具的handle_tool_error,因为其默认值是False

from langchain.tools.base import ToolException  

from langchain import SerpAPIWrapper
from langchain.agents import AgentType, initialize_agent
from langchain.chat_models import ChatOpenAI
from langchain.tools import Tool
from langchain.chat_models import ChatOpenAI

def _handle_error(error: ToolException) -> str:
return (
"The following errors occurred during tool execution:"
+ error.args[0]
+ "Please try another tool."
)

def search_tool1(s: str):
raise ToolException("The search tool1 is not available.")

def search_tool2(s: str):
raise ToolException("The search tool2 is not available.")

search_tool3 = SerpAPIWrapper()

API Reference:

description = "useful for when you need to answer questions about current events.You should give priority to using it."  
tools = [
Tool.from_function(
func=search_tool1,
name="Search_tool1",
description=description,
handle_tool_error=True,
),
Tool.from_function(
func=search_tool2,
name="Search_tool2",
description=description,
handle_tool_error=_handle_error,
),
Tool.from_function(
func=search_tool3.run,
name="Search_tool3",
description="useful for when you need to answer questions about current events",
),
]
agent = initialize_agent(
tools,
ChatOpenAI(temperature=0),
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
)

agent.run("Who is Leo DiCaprio's girlfriend?")
> Entering new AgentExecutor chain...  
I should use Search_tool1 to find recent news articles about Leo DiCaprio's personal life.
Action: Search_tool1
Action Input: "Leo DiCaprio girlfriend"
Observation: The search tool1 is not available.
Thought:I should try using Search_tool2 instead.
Action: Search_tool2
Action Input: "Leo DiCaprio girlfriend"
Observation: The following errors occurred during tool execution:The search tool2 is not available.Please try another tool.
Thought:I should try using Search_tool3 as a last resort.
Action: Search_tool3
Action Input: "Leo DiCaprio girlfriend"
Observation: Leonardo DiCaprio and Gigi Hadid were recently spotted at a pre-Oscars party, sparking interest once again in their rumored romance. The Revenant actor and the model first made headlines when they were spotted together at a New York Fashion Week afterparty in September 2022.
Thought:Based on the information from Search_tool3, it seems that Gigi Hadid is currently rumored to be Leo DiCaprio's girlfriend.
Final Answer: Gigi Hadid is currently rumored to be Leo DiCaprio's girlfriend.

> Finished chain.

"Gigi Hadid is currently rumored to be Leo DiCaprio's girlfriend."