![[LangChain智能體本質論-06]賦予Agent執(zhí)行力的工具是個什么東西?](http://pic.xiahunao.cn/yaotu/[LangChain智能體本質論-06]賦予Agent執(zhí)行力的工具是個什么東西?)
工具是連接大語言模型與外部世界的橋梁。它們賦予了模型行動的能力使其能夠超越文本生成的范疇去執(zhí)行搜索、計算、運行代碼或調用API等實際任務。工具本質上是帶有名稱、描述和Schema的函數(shù)。LLM并不直接運行工具服務單工具除外而是根據(jù)用戶的查詢參考工具的描述來決定是否需要調用某個工具并輸出該工具所需的結構化參數(shù)。Agent接收到模型生成的參數(shù)后在本地或服務器端執(zhí)行該工具并將結果返回給LLM供其進行后續(xù)推理或總結。從create_agent函數(shù)的定義可以看出注冊的工具具有三種形式BaseTool、Callable[..., Any]和dict[str, Any]。提供的字典表示注冊工具的聲明也就是以這種方式注冊的工具只包含描述工具的JSON Schema并不具有執(zhí)行的能力。由于工具是由Agent負責執(zhí)行的服務端工具除外的所以當它接收到攜帶ToolCall的AIMessage后如果待執(zhí)行的是這種類型的工具可以利用中間件調用對應的工具我們將在介紹中間件的時候演示這種實現(xiàn)方式。BaseTool是具體工具類型的基類以Callable[..., Any]對象形式提供的可執(zhí)行對象最終也會轉換成BaseTool對象。defcreate_agent(...tools:Sequence[BaseTool|Callable[...,Any]|dict[str,Any]]|NoneNone,...)1. BaseToolBaseTool是LangChain中所有工具的基類。它繼承自RunnableSerializable這意味著它不僅是一個可調用的函數(shù)還可以成為LCEL鏈上的一環(huán)。作為一個Runnable對象它的輸入可以是一個ToolCall對象我們知道模型生成的AIMessage中利用該對象描述工具調用我們可以從之提取工具名稱、參數(shù)以及唯一標識工具調用的ID。輸入也可以是一個字符串或者字典它們承載著調用工具的輸入?yún)?shù)。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相關的字段和屬性很難區(qū)分args_schema描述參數(shù)結構的原始Schema形式可以是一個Pydantic類型或者描述JSON Schema的字典它是下面兩個屬性的源頭args: 描述輸出參數(shù)JSON Schema的字典如果該字典的長度為1is_single_input屬性返回True。如果args_schema自身就是一個字典那么兩者一致如果args_schema是一個Pydantic類型會將每個成員解析為JSON Schema并生成返回的字典。tool_call_schema它與args_schema的表現(xiàn)形式一致同為表示JSON Schema的字典或者Pydantic類型它是為LLM生成ToolCall服務的所以會加上description成員。它會剔除自動注入的參數(shù)因為ToolCall的參數(shù)列表只提供外部指定的參數(shù)。如下的字段成員用于控制工具的執(zhí)行。如果將return_direct字段設為True工具執(zhí)行后的結果將直接返回給用戶而不再發(fā)回給LLM進行后續(xù)的文本生成常用于跳轉或終結類工具。handle_tool_error字段決定了如何處理工具執(zhí)行拋出的ToolException。它可以是布爾值表示是否再次拋出異常、字符串報錯時返回給LLM的文本或用于處理異常的回調函數(shù)。handle_validation_error字段的定義方式與之類似但是它處理的是參數(shù)校驗失敗的情況。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決定了工具執(zhí)行后返回給LLM的數(shù)據(jù)結構以及是否保留中間過程的原始數(shù)據(jù)。它目前支持兩個選項:content(default)工具執(zhí)行完畢后返回值會被轉化為字符串并封裝在ToolMessage的content字段中并強制視為傳給LLM的上下文。如果返回的是一張圖片、一個復雜的DataFrame或一個數(shù)據(jù)庫連接對象這些內容都會被嘗試字符串化。這會導致LLM 接收到大量無用甚至亂碼的文本下游的程序也會丟失了原始的對象引用content_and_artifact這是為了解決既要給LLM看簡報又要給程序留原始數(shù)據(jù)而設計的。在這種模式下執(zhí)行工具必須返回一個包含兩個元素的元組(content, artifact)分別存儲與AIMessage的content和artifact節(jié)點。前者通常是字符串后者可以是任何形式的內容包括圖像、Base64、復雜的JSON、模型實例等。只有content部分會傳遞給LLM所以這樣可以降低Token的消費。如下的字典成員涉及調試和跟蹤。verbose用于控制是否打印詳細的執(zhí)行日志。我們可以利用callbacks提供工具在開始、完成和出錯時會自動執(zhí)行的回調。tags和metadata字段提供的標簽和元數(shù)據(jù)會出現(xiàn)在捕捉的跟蹤信息中。classBaseTool(RunnableSerializable[str|dict|ToolCall,Any]):verbose:boolFalsecallbacks:CallbacksField(defaultNone,excludeTrue)tags:list[str]|NoneNonemetadata:dict[str,Any]|NoneNone我們最后來看看作為一個Runnable它的invoke方法是如何實現(xiàn)的。如下面的代碼片段所示invoke方法會直接調用run方法傳入的參數(shù)利用私有方法_prep_run_args根據(jù)指定的輸入和配置生成。BaseTool是一個基類它將工具調用的實現(xiàn)利用抽象方法_run下放給子類。run方法最終會調用_run方法并在此基礎上完成一些回調執(zhí)行和異常處理相關的操作。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最簡單的實現(xiàn)。它利用func和coroutine字段分別提供了同步和異步可執(zhí)行對象實現(xiàn)的_run方法和重寫的_arun方法會分別調用它們完成工具的調用。它還提供了類方法from_function作為創(chuàng)建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,)在如下的演示程序中我們根據(jù)函數(shù)greet創(chuàng)建了一個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這種簡單的實現(xiàn)只支持單輸入?yún)?shù)的函數(shù)一旦輸入?yún)?shù)超過兩個就會出錯。如下這個演示程序試圖根據(jù)一個Lambda表達式創(chuàng)建一個執(zhí)行加法運算的Toolinvoke方法執(zhí)行的時候會拋出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試圖根據(jù)一個具有多個參數(shù)的函數(shù)創(chuàng)建對應Tool對象的時候拋出的ToolException提示使用StructuredTool。StructuredTool和Tool其實很類似它們都是利用指定的同步函數(shù)和coroutine作為同步和異步執(zhí)行體。除了提供針對多參數(shù)的支持外StructuredTool還可以利用Pydantic模型類型定義輸入結構自動為LLM生成符合規(guī)范的參數(shù)描述對應于args_schema字段并對輸入進行實時校驗。它還可以直接從指定的函數(shù)創(chuàng)建自動推斷參數(shù)類型和文檔說明。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作為創(chuàng)建StructuredTool對象的工廠方法。在調用此方法的時候我們可以手工指定表示輸入?yún)?shù)Schema的args_schema。如果沒有指定但是infer_schema參數(shù)為True默認為True則會通過分析函數(shù)簽名對輸入?yún)?shù)Schema進行推斷。如果args_schema沒有指定、并且infer_schema也被設置為False此時args和tool_call_schema屬性對應的Schema將由_run方法的簽名進行推斷。由于_run方法簽名的固定的推斷出來的Schema根本不能體現(xiàn)輸入?yún)?shù)的結構它將使工具將變成一個參數(shù)黑洞。對于前面執(zhí)行失敗的演示程序如果將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指定的工具是一個函數(shù)它會利用tool裝飾器函數(shù)將其轉換成一個BaseTool對象。由于BaseTool的description是通過函數(shù)的docstring創(chuàng)建的鑒于此字段的重要性如果指定的函數(shù)沒有定義docstring轉換過程將會失敗。我們也可以將這個裝飾器顯式應用到自定義的函數(shù)上并指定相應的參數(shù)對創(chuàng)建的BaseTool作相應的定制。LangChain為tool裝飾器函數(shù)定義了很多重載最終調用的則是如下這個。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函數(shù)的定義可以看出被轉換成BaseTool對象的原始對象不僅可以是一個函數(shù)同步或者異步還可以是一個Runnable對象。通過相應關鍵字參數(shù)我們可以指定BaseTool絕大部分的字段。infer_schema和args_schema這兩個參數(shù)很重要如果infer_schema為True或者顯式指定了args_schema最終創(chuàng)建的是一個StructuredTool對象。否則創(chuàng)建的就是一個Tool對象由于Tool對象支持單參數(shù)函數(shù)函數(shù)一旦違反這個約定就會報錯所以要么顯式指定args_schema要么保持infer_schema為True永遠使用StructuredTool來表示注冊的工具。