工具是连接大语言模型与外部世界的桥梁。它们赋予了模型行动的能力,使其能够超越文本生成的范畴,去执行搜索、计算、运行代码或调用API等实际任务。工具本质上是带有名称、描述和Schema的函数。LLM并不直接运行工具(服务单工具除外),而是根据用户的查询,参考工具的描述来决定是否需要调用某个工具,并输出该工具所需的结构化参数。Agent接收到模型生成的参数后,在本地或服务器端执行该工具,并将结果返回给LLM,供其进行后续推理或总结。
从create_agent函数的定义可以看出,注册的工具具有三种形式:BaseTool、Callable[..., Any]和dict[str, Any]。提供的字典表示注册工具的声明,也就是以这种方式注册的工具只包含描述工具的JSON Schema,并不具有执行的能力。由于工具是由Agent负责执行的(服务端工具除外)的,所以当它接收到携带ToolCall的AIMessage后,如果待执行的是这种类型的工具,可以利用中间件调用对应的工具,我们将在介绍中间件的时候演示这种实现方式。BaseTool是具体工具类型的基类,以Callable[..., Any]对象形式提供的可执行对象最终也会转换成BaseTool对象。
defcreate_agent(...tools:Sequence[BaseTool|Callable[...,Any]|dict[str,Any]]|None=None,...)1. BaseTool
BaseTool是LangChain中所有工具的基类。它继承自RunnableSerializable,这意味着它不仅是一个可调用的函数,还可以成为LCEL链上的一环。作为一个Runnable对象,它的输入可以是一个ToolCall对象,我们知道模型生成的AIMessage中利用该对象描述工具调用,我们可以从之提取工具名称、参数以及唯一标识工具调用的ID。输入也可以是一个字符串或者字典,它们承载着调用工具的输入参数。
classBaseTool(RunnableSerializable[str|dict|ToolCall,Any]):name:strdescription:strargs_schema:Annotated[ArgsSchema|None,SkipValidation()]=Field(default=None,description="The tool schema.")@propertydefargs(self)->dict@propertydefis_single_input(self)->bool:@propertydeftool_call_schema(self)->ArgsSchema:ArgsSchema=TypeBaseModel|dict[str,Any]工具的名称通过name字段表示,它的description字段提供了描述工具的文本。如果将描述理解成可有可无的补充性说明性文本,那就大错特错了。这是最重要的字段之一,因为它作为提示词的一部分传给LLM,指导模型在什么场景下使用该工具。标准的描述应该回答When/Why/How三个问题,即工具在何时被调用?调用工具可以达到什么目的?如何调用?并且最好提供少样本示例。BaseTool提供了几个与Schema相关的字段和属性,很难区分:
- args_schema:描述参数结构的原始Schema形式,可以是一个Pydantic类型或者描述JSON Schema的字典,它是下面两个属性的源头;
- args: 描述输出参数JSON Schema的字典,如果该字典的长度为1,
is_single_input属性返回True。- 如果
args_schema自身就是一个字典,那么两者一致; - 如果
args_schema是一个Pydantic类型,会将每个成员解析为JSON Schema,并生成返回的字典。
- 如果
- tool_call_schema:它与
args_schema的表现形式一致(同为表示JSON Schema的字典或者Pydantic类型),它是为LLM生成ToolCall服务的,所以会加上description成员。它会剔除自动注入的参数,因为ToolCall的参数列表只提供外部指定的参数。
如下的字段成员用于控制工具的执行。如果将return_direct字段设为True,工具执行后的结果将直接返回给用户,而不再发回给LLM进行后续的文本生成(常用于跳转或终结类工具)。handle_tool_error字段决定了如何处理工具执行抛出的ToolException。它可以是布尔值(表示是否再次抛出异常)、字符串(报错时返回给LLM的文本)或用于处理异常的回调函数。handle_validation_error字段的定义方式与之类似,但是它处理的是参数校验失败的情况。
classBaseTool(RunnableSerializable[str|dict|ToolCall,Any]):return_direct:bool=Falsehandle_tool_error:bool|str|Callable[[ToolException],str]|None=Falsehandle_validation_error:(bool|str|Callable[[ValidationError|ValidationErrorV1],str]|None)=Falseresponse_format:Literal["content","content_and_artifact"]="content"response_format决定了工具执行后返回给LLM的数据结构以及是否保留中间过程的原始数据。它目前支持两个选项:
- content(default):工具执行完毕后,返回值会被转化为字符串,并封装在
ToolMessage的content字段中,并强制视为传给LLM的上下文。如果返回的是一张图片、一个复杂的DataFrame或一个数据库连接对象,这些内容都会被尝试字符串化。这会导致LLM 接收到大量无用(甚至乱码)的文本,下游的程序也会丢失了原始的对象引用; - content_and_artifact:这是为了解决既要给LLM看简报,又要给程序留原始数据而设计的。在这种模式下,执行工具必须返回一个包含两个元素的元组
(content, artifact),分别存储与AIMessage的content和artifact节点。前者通常是字符串,后者可以是任何形式的内容,包括图像、Base64、复杂的JSON、模型实例等。只有content部分会传递给LLM,所以这样可以降低Token的消费。
如下的字典成员涉及调试和跟踪。verbose用于控制是否打印详细的执行日志。我们可以利用callbacks提供工具在开始、完成和出错时会自动执行的回调。tags和metadata字段提供的标签和元数据会出现在捕捉的跟踪信息中。
classBaseTool(RunnableSerializable[str|dict|ToolCall,Any]):verbose:bool=Falsecallbacks:Callbacks=Field(default=None,exclude=True)tags:list[str]|None=Nonemetadata:dict[str,Any]|None=None我们最后来看看作为一个Runnable,它的invoke方法是如何实现的。如下面的代码片段所示,invoke方法会直接调用run方法,传入的参数利用私有方法_prep_run_args根据指定的输入和配置生成。BaseTool是一个基类,它将工具调用的实现利用抽象方法_run下放给子类。run方法最终会调用_run方法,并在此基础上完成一些回调执行和异常处理相关的操作。BaseTool也重写了ainvoke方法,并定义了arun和_arun形成了一条异步调用链。
classBaseTool(RunnableSerializable[str|dict|ToolCall,Any]):@overridedefinvoke(self,input:str|dict|ToolCall,config:RunnableConfig|None=None,**kwargs:Any,)->Any:tool_input,kwargs=_prep_run_args(input,config,**kwargs)returnself.run(tool_input,**kwargs)defrun(self,tool_input:str|dict[str,Any],verbose:bool|None=None,# noqa: FBT001start_color:str|None="green",color:str|None="green",callbacks:Callbacks=None,*,tags:list[str]|None=None,metadata:dict[str,Any]|None=None,run_name:str|None=None,run_id:uuid.UUID|None=None,config:RunnableConfig|None=None,tool_call_id:str|None=None,**kwargs:Any,)->Any@abstractmethoddef_run(self,*args:Any,**kwargs:Any)->Any:2. Tool
Tool类是对BaseTool最简单的实现。它利用func和coroutine字段分别提供了同步和异步可执行对象,实现的_run方法和重写的_arun方法会分别调用它们完成工具的调用。它还提供了类方法from_function作为创建Tool对象的工厂方法。
classTool(BaseTool):description:str=""func:Callable[...,str]|Nonecoroutine:Callable[...,Awaitable[str]]|None=None@overrideasyncdefainvoke(self,input:str|dict|ToolCall,config:RunnableConfig|None=None,**kwargs:Any,)->Any:ifnotself.coroutine:returnawaitrun_in_executor(config,self.invoke,input,config,**kwargs)returnawaitsuper().ainvoke(input,config,**kwargs)def_run(self,*args:Any,config:RunnableConfig,run_manager:CallbackManagerForToolRun|None=None,**kwargs:Any,)->Any:ifself.func:ifrun_managerandsignature(self.func).parameters.get("callbacks"):kwargs["callbacks"]=run_manager.get_child()ifconfig_param:=_get_runnable_config_param(self.func):kwargs[config_param]=configreturnself.func(*args,**kwargs)msg="Tool does not support sync invocation."raiseNotImplementedError(msg)asyncdef_arun(self,*args:Any,config:RunnableConfig,run_manager:AsyncCallbackManagerForToolRun|None=None,**kwargs:Any,)->Any:ifself.coroutine:ifrun_managerandsignature(self.coroutine).parameters.get("callbacks"):kwargs["callbacks"]=run_manager.get_child()ifconfig_param:=_get_runnable_config_param(self.coroutine):kwargs[config_param]=configreturnawaitself.coroutine(*args,**kwargs)returnawaitsuper()._arun(*args,config=config,run_manager=run_manager,**kwargs)@classmethoddeffrom_function(cls,func:Callable|None,name:str,# We keep these required to support backwards compatibilitydescription:str,return_direct:bool=False,# noqa: FBT001,FBT002args_schema:ArgsSchema|None=None,coroutine:Callable[...,Awaitable[Any]]|None=None,# This is last for compatibility, but should be after func**kwargs:Any,)->Tool:iffuncisNoneandcoroutineisNone:msg="Function and/or coroutine must be provided"raiseValueError(msg)returncls(name=name,func=func,coroutine=coroutine,description=description,return_direct=return_direct,args_schema=args_schema,**kwargs,)在如下的演示程序中,我们根据函数greet创建了一个Tool对象,然后分别以字符串、字典和ToolCall对象作为输入对它进行调用。
fromlangchain_core.toolsimportToolfromlangchain_core.messages.toolimporttool_calldefgreet(name:str)->str:returnf"Hello,{name}!"tool=Tool.from_function(func=greet,name="greet",description="Greet a person by name.")result=tool.invoke("Alice")assertresult=="Hello, Alice!"result=tool.invoke({"name":"Alice"})assertresult=="Hello, Alice!"result=tool.invoke(tool_call(name="greet",args={"name":"Alice"},id=None))assertresult=="Hello, Alice!"Tool这种简单的实现只支持单输入参数的函数,一旦输入参数超过两个就会出错。如下这个演示程序试图根据一个Lambda表达式创建一个执行加法运算的Tool,invoke方法执行的时候会抛出ToolException,并提示Too many arguments to single-input tool add. Consider using StructuredTool instead. Args: [1, 2]
fromlangchain_core.toolsimportTool tool=Tool.from_function(func=lambdax,y:x+y,name="add",description="Add two numbers together.")tool.invoke({"x":1,"y":2})3. StructuredTool
当我们调用Tool的类方法from_function试图根据一个具有多个参数的函数创建对应Tool对象的时候,抛出的ToolException提示使用StructuredTool。StructuredTool和Tool其实很类似,它们都是利用指定的同步函数和coroutine作为同步和异步执行体。除了提供针对多参数的支持外,StructuredTool还可以利用Pydantic模型类型定义输入结构,自动为LLM生成符合规范的参数描述(对应于args_schema字段),并对输入进行实时校验。它还可以直接从指定的函数创建,自动推断参数类型和文档说明。
classStructuredTool(BaseTool):description:str=""args_schema:Annotated[ArgsSchema,SkipValidation()]=Field(...,description="The tool schema.")func:Callable[...,Any]|None=Nonecoroutine:Callable[...,Awaitable[Any]]|None=None@overrideasyncdefainvoke(self,input:str|dict|ToolCall,config:RunnableConfig|None=None,**kwargs:Any,)->Any:ifnotself.coroutine:returnawaitrun_in_executor(config,self.invoke,input,config,**kwargs)returnawaitsuper().ainvoke(input,config,**kwargs)def_run(self,*args:Any,config:RunnableConfig,run_manager:CallbackManagerForToolRun|None=None,**kwargs:Any,)->Any:ifself.func:ifrun_managerandsignature(self.func).parameters.get("callbacks"):kwargs["callbacks"]=run_manager.get_child()ifconfig_param:=_get_runnable_config_param(self.func):kwargs[config_param]=configreturnself.func(*args,**kwargs)msg="StructuredTool does not support sync invocation."raiseNotImplementedError(msg)asyncdef_arun(self,*args:Any,config:RunnableConfig,run_manager:AsyncCallbackManagerForToolRun|None=None,**kwargs:Any,)->Any:ifself.coroutine:ifrun_managerandsignature(self.coroutine).parameters.get("callbacks"):kwargs["callbacks"]=run_manager.get_child()ifconfig_param:=_get_runnable_config_param(self.coroutine):kwargs[config_param]=configreturnawaitself.coroutine(*args,**kwargs)returnawaitsuper()._arun(*args,config=config,run_manager=run_manager,**kwargs)@classmethoddeffrom_function(cls,func:Callable|None=None,coroutine:Callable[...,Awaitable[Any]]|None=None,name:str|None=None,description:str|None=None,return_direct:bool=False,args_schema:ArgsSchema|None=None,infer_schema:bool=True,*,response_format:Literal["content","content_and_artifact"]="content",parse_docstring:bool=False,error_on_invalid_docstring:bool=False,**kwargs:Any,)->StructuredTool它也定义了类方法from_function作为创建StructuredTool对象的工厂方法。在调用此方法的时候,我们可以手工指定表示输入参数Schema的args_schema。如果没有指定,但是infer_schema参数为True(默认为True),则会通过分析函数签名对输入参数Schema进行推断。如果args_schema没有指定、并且infer_schema也被设置为False,此时args和tool_call_schema属性对应的Schema将由_run方法的签名进行推断。由于_run方法签名的固定的,推断出来的Schema根本不能体现输入参数的结构,它将使工具将变成一个参数黑洞。
对于前面执行失败的演示程序,如果将Tool类型替换成StructuredTool就没问题。
fromlangchain_core.toolsimportStructuredTool tool=StructuredTool.from_function(func=lambdax,y:x+y,name="add",description="Add two numbers together.")asserttool.invoke({"x":1,"y":2})==34. @tool装饰器
如果我们调用create_agent指定的工具是一个函数,它会利用@tool装饰器函数将其转换成一个BaseTool对象。由于BaseTool的description是通过函数的docstring创建的,鉴于此字段的重要性,如果指定的函数没有定义docstring,转换过程将会失败。我们也可以将这个装饰器显式应用到自定义的函数上,并指定相应的参数对创建的BaseTool作相应的定制。LangChain为@tool装饰器函数定义了很多重载,最终调用的则是如下这个。
deftool(name_or_callable:str|Callable|None=None,runnable:Runnable|None=None,*args:Any,description:str|None=None,return_direct:bool=False,args_schema:ArgsSchema|None=None,infer_schema:bool=True,response_format:Literal["content","content_and_artifact"]="content",parse_docstring:bool=False,error_on_invalid_docstring:bool=True,extras:dict[str,Any]|None=None,)->BaseTool|Callable[[Callable|Runnable],BaseTool]:从tool函数的定义可以看出,被转换成BaseTool对象的原始对象不仅可以是一个函数(同步或者异步),还可以是一个Runnable对象。通过相应关键字参数,我们可以指定BaseTool绝大部分的字段。infer_schema和args_schema这两个参数很重要,如果infer_schema为True或者显式指定了args_schema,最终创建的是一个StructuredTool对象。否则创建的就是一个Tool对象,由于Tool对象支持单参数函数,函数一旦违反这个约定就会报错,所以要么显式指定args_schema,要么保持infer_schema为True,永远使用StructuredTool来表示注册的工具。