ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

[LangChain智能体本质论-06]赋予Agent执行力的工具是个什么东西?

[LangChain智能体本质论-06]赋予Agent执行力的工具是个什么东西? 工具是连接大语言模型与外部世界的桥梁。它们赋予了模型行动的能力使其能够超越文本生成的范畴去执行搜索、计算、运行代码或调用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]]|NoneNone,...)1. BaseToolBaseTool是LangChain中所有工具的基类。它继承自RunnableSerializable这意味着它不仅是一个可调用的函数还可以成为LCEL链上的一环。作为一个Runnable对象它的输入可以是一个ToolCall对象我们知道模型生成的AIMessage中利用该对象描述工具调用我们可以从之提取工具名称、参数以及唯一标识工具调用的ID。输入也可以是一个字符串或者字典它们承载着调用工具的输入参数。classBaseTool(RunnableSerializable[str|dict|ToolCall,Any]):name:strdescription:strargs_schema:Annotated[ArgsSchema|None,SkipValidation()]Field(defaultNone,descriptionThe tool schema.)propertydefargs(self)-dictpropertydefis_single_input(self)-bool:propertydeftool_call_schema(self)-ArgsSchema:ArgsSchemaTypeBaseModel|dict[str,Any]工具的名称通过name字段表示它的description字段提供了描述工具的文本。如果将描述理解成可有可无的补充性说明性文本那就大错特错了。这是最重要的字段之一因为它作为提示词的一部分传给LLM指导模型在什么场景下使用该工具。标准的描述应该回答When/Why/How三个问题即工具在何时被调用调用工具可以达到什么目的如何调用并且最好提供少样本示例。BaseTool提供了几个与Schema相关的字段和属性很难区分args_schema描述参数结构的原始Schema形式可以是一个Pydantic类型或者描述JSON Schema的字典它是下面两个属性的源头args: 描述输出参数JSON Schema的字典如果该字典的长度为1is_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:boolFalsehandle_tool_error:bool|str|Callable[[ToolException],str]|NoneFalsehandle_validation_error:(bool|str|Callable[[ValidationError|ValidationErrorV1],str]|None)Falseresponse_format:Literal[content,content_and_artifact]contentresponse_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:boolFalsecallbacks:CallbacksField(defaultNone,excludeTrue)tags:list[str]|NoneNonemetadata:dict[str,Any]|NoneNone我们最后来看看作为一个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|NoneNone,**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|NoneNone,# noqa: FBT001start_color:str|Nonegreen,color:str|Nonegreen,callbacks:CallbacksNone,*,tags:list[str]|NoneNone,metadata:dict[str,Any]|NoneNone,run_name:str|NoneNone,run_id:uuid.UUID|NoneNone,config:RunnableConfig|NoneNone,tool_call_id:str|NoneNone,**kwargs:Any,)-Anyabstractmethoddef_run(self,*args:Any,**kwargs:Any)-Any:2. ToolTool类是对BaseTool最简单的实现。它利用func和coroutine字段分别提供了同步和异步可执行对象实现的_run方法和重写的_arun方法会分别调用它们完成工具的调用。它还提供了类方法from_function作为创建Tool对象的工厂方法。classTool(BaseTool):description:strfunc:Callable[...,str]|Nonecoroutine:Callable[...,Awaitable[str]]|NoneNoneoverrideasyncdefainvoke(self,input:str|dict|ToolCall,config:RunnableConfig|NoneNone,**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|NoneNone,**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)msgTool does not support sync invocation.raiseNotImplementedError(msg)asyncdef_arun(self,*args:Any,config:RunnableConfig,run_manager:AsyncCallbackManagerForToolRun|NoneNone,**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,configconfig,run_managerrun_manager,**kwargs)classmethoddeffrom_function(cls,func:Callable|None,name:str,# We keep these required to support backwards compatibilitydescription:str,return_direct:boolFalse,# noqa: FBT001,FBT002args_schema:ArgsSchema|NoneNone,coroutine:Callable[...,Awaitable[Any]]|NoneNone,# This is last for compatibility, but should be after func**kwargs:Any,)-Tool:iffuncisNoneandcoroutineisNone:msgFunction and/or coroutine must be providedraiseValueError(msg)returncls(namename,funcfunc,coroutinecoroutine,descriptiondescription,return_directreturn_direct,args_schemaargs_schema,**kwargs,)在如下的演示程序中我们根据函数greet创建了一个Tool对象然后分别以字符串、字典和ToolCall对象作为输入对它进行调用。fromlangchain_core.toolsimportToolfromlangchain_core.messages.toolimporttool_calldefgreet(name:str)-str:returnfHello,{name}!toolTool.from_function(funcgreet,namegreet,descriptionGreet a person by name.)resulttool.invoke(Alice)assertresultHello, Alice!resulttool.invoke({name:Alice})assertresultHello, Alice!resulttool.invoke(tool_call(namegreet,args{name:Alice},idNone))assertresultHello, Alice!Tool这种简单的实现只支持单输入参数的函数一旦输入参数超过两个就会出错。如下这个演示程序试图根据一个Lambda表达式创建一个执行加法运算的Toolinvoke方法执行的时候会抛出ToolException并提示Too many arguments to single-input tool add. Consider using StructuredTool instead. Args: [1, 2]fromlangchain_core.toolsimportTool toolTool.from_function(funclambdax,y:xy,nameadd,descriptionAdd 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:strargs_schema:Annotated[ArgsSchema,SkipValidation()]Field(...,descriptionThe tool schema.)func:Callable[...,Any]|NoneNonecoroutine:Callable[...,Awaitable[Any]]|NoneNoneoverrideasyncdefainvoke(self,input:str|dict|ToolCall,config:RunnableConfig|NoneNone,**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|NoneNone,**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)msgStructuredTool does not support sync invocation.raiseNotImplementedError(msg)asyncdef_arun(self,*args:Any,config:RunnableConfig,run_manager:AsyncCallbackManagerForToolRun|NoneNone,**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,configconfig,run_managerrun_manager,**kwargs)classmethoddeffrom_function(cls,func:Callable|NoneNone,coroutine:Callable[...,Awaitable[Any]]|NoneNone,name:str|NoneNone,description:str|NoneNone,return_direct:boolFalse,args_schema:ArgsSchema|NoneNone,infer_schema:boolTrue,*,response_format:Literal[content,content_and_artifact]content,parse_docstring:boolFalse,error_on_invalid_docstring:boolFalse,**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 toolStructuredTool.from_function(funclambdax,y:xy,nameadd,descriptionAdd 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|NoneNone,runnable:Runnable|NoneNone,*args:Any,description:str|NoneNone,return_direct:boolFalse,args_schema:ArgsSchema|NoneNone,infer_schema:boolTrue,response_format:Literal[content,content_and_artifact]content,parse_docstring:boolFalse,error_on_invalid_docstring:boolTrue,extras:dict[str,Any]|NoneNone,)-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来表示注册的工具。
返回列表