在 LangChain框架中,工具是连接大语言模型与外部世界的桥梁。它们赋予了模型“行动”的能力,使其能够超越文本生成的范畴,去执行搜索、计算、运行代码或调用API等实际任务。工具本质上是带有名称 、描述 和Shema的函数。LLM并不直接运行工具(服务端工具除外),而是根据用户的查询,参考工具的描述来决定是否需要调用某个工具,并输出该工具所需的结构化参数。Agent接收到模型生成的参数后,在本地或服务器端执行该工具,并将结果返回给LLM,供其进行后续推理或总结。
从create_agent函数的定义可以看出,注册的工具具有三种形式:BaseTool、Callable[..., Any]和dict[str, Any]。提供的字典表示注册工具的声明,也就是以这种方式注册的工具只包含描述工具的JSON Schema,并不具有执行的能力。由于工具是由Agent负责执行的(服务端工具除外)的,所以当它接收到携带ToolCall的AIMessage后,如果待执行的是这种类型的工具,可以利用中间件调用对应的工具,我们将在介绍中间件的时候演示这种实现方式。BaseTool是具体工具类型的基类,以Callable[..., Any]对象形式提供的可执行对象最终也会转换成BaseTool对象。
def create_agent(
...
tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,
...
)
1. BaseTool
BaseTool是LangChain中所有工具的基石。它继承自RunnableSerializable,这意味着它不仅是一个可调用的函数,还可以成为LCEL链上的一环。作为一个Runnable对象,它的输入可以是一个ToolCall对象,我们知道模型生成的AIMessage中利用该对象描述工具调用,我们可以从之提取工具名称、参数以及唯一标识工具调用的ID。输入也可以是一个字符串或者字典,它们承载着调用工具的输入参数。
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
name: str
description: str
args_schema: Annotated[ArgsSchema | None, SkipValidation()] = Field(
default=None, description="The tool schema."
)
@property
def args(self) -> dict
@property
def is_single_input(self) -> bool:
@property
def tool_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的字典。如果
args_schema自身就是一个字典,那么两者一致;如果args_schema是一个Pydantic类型,会将每个成员解析为JSON Schema,并生成返回的字典。如果该字典的长度为1,is_single_input属性返回True。 - tool_call_schema:它与
args_schema的表现形式一致(同为表示JSON Schema的字典或者Pydantic类型),但是为LLM生成ToolCall服务的,所以会加上description成员。它是剔除自动注入的参数,因为ToolCall的参数列表只提供外部指定的参数。
如下的字段成员用于控制工具的执行。如果将return_direct字段设为True,工具执行后的结果将直接返回给用户,而不再发回给LLM进行后续的文本生成(常用于“跳转”或“终结”类工具)。handle_tool_error字段决定了如何处理工具执行抛出的ToolException。它可以是布尔值(表示是否再次抛出异常)、字符串(报错时返回给LLM的文本)或用于处理异常的回调函数。handle_validation_error字段的定义方式与之类似,但是它处理的是参数校验失败的情况。
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
return_direct: bool = False
handle_tool_error: bool | str | Callable[[ToolException], str] | None = False
handle_validation_error: (
bool | str | Callable[[ValidationError | ValidationErrorV1], str] | None
) = False
response_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字段提供的标签和元数据会出现在捕捉的跟踪信息中。
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
verbose: bool = False
callbacks: Callbacks = Field(default=None, exclude=True)
tags: list[str] | None = None
metadata: dict[str, Any] | None = None
我们最后来看看作为一个Runnable,它的invoke方法是如何实现的。如下面的代码片段所示,invoke方法会直接调用run方法,传入的参数利用私有方法_prep_run_args根据指定的输入和配置生成。BaseTool是一个基类,它将工具调用的实现利用抽象方法_run下放给子类。run方法最终会调用_run方法,并在此基础上完成一些回调执行和异常处理相关的操作。BaseTool也重写了ainvoke方法,并定义了arun和_arun形成了一条异步调用链。
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
@override
def invoke(
self,
input: str | dict | ToolCall,
config: RunnableConfig | None = None,
**kwargs: Any,
) -> Any:
tool_input, kwargs = _prep_run_args(input, config, **kwargs)
return self.run(tool_input, **kwargs)
def run(
self,
tool_input: str | dict[str, Any],
verbose: bool | None = None, # noqa: FBT001
start_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
@abstractmethod
def _run(self, *args: Any, **kwargs: Any) -> Any:
2. Tool
Tool类是对BaseTool最简单的实现。它利用func和coroutine字段分别提供了同步和异步可执行对象,实现的_run方法和重写的_arun方法会分别调用它们完成工具的调用。它还提供了类方法from_function作为创建Tool对象的工厂方法。
class Tool(BaseTool):
description: str = ""
func: Callable[..., str] | None
coroutine: Callable[..., Awaitable[str]] | None = None
@override
async def ainvoke(
self,
input: str | dict | ToolCall,
config: RunnableConfig | None = None,
**kwargs: Any,
) -> Any:
if not self.coroutine:
return await run_in_executor(config, self.invoke, input, config, **kwargs)
return await super().ainvoke(input, config, **kwargs)
def _run(
self,
*args: Any,
config: RunnableConfig,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> Any:
if self.func:
if run_manager and signature(self.func).parameters.get("callbacks"):
kwargs["callbacks"] = run_manager.get_child()
if config_param := _get_runnable_config_param(self.func):
kwargs[config_param] = config
return self.func(*args, **kwargs)
msg = "Tool does not support sync invocation."
raise NotImplementedError(msg)
async def _arun(
self,
*args: Any,
config: RunnableConfig,
run_manager: AsyncCallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> Any:
if self.coroutine:
if run_manager and signature(self.coroutine).parameters.get("callbacks"):
kwargs["callbacks"] = run_manager.get_child()
if config_param := _get_runnable_config_param(self.coroutine):
kwargs[config_param] = config
return await self.coroutine(*args, **kwargs)
return await super()._arun(
*args, config=config, run_manager=run_manager, **kwargs
)
@classmethod
def from_function(
cls,
func: Callable | None,
name: str, # We keep these required to support backwards compatibility
description: str,
return_direct: bool = False, # noqa: FBT001,FBT002
args_schema: ArgsSchema | None = None,
coroutine: Callable[..., Awaitable[Any]]
| None = None, # This is last for compatibility, but should be after func
**kwargs: Any,
) -> Tool:
if func is None and coroutine is None:
msg = "Function and/or coroutine must be provided"
raise ValueError(msg)
return cls(
name=name,
func=func,
coroutine=coroutine,
description=description,
return_direct=return_direct,
args_schema=args_schema,
**kwargs,
)
在如下的演示程序中,我们根据函数greet创建了一个Tool对象,然后分别以字符串、字典和ToolCall对象作为输入对它进行调用。
from langchain_core.tools import Tool
from langchain_core.messages.tool import tool_call
def greet(name: str) -> str:
return f"Hello, {name}!"
tool = Tool.from_function(
func = greet,
name = "greet",
description = "Greet a person by name.")
result = tool.invoke("Alice")
assert result == "Hello, Alice!"
result = tool.invoke({"name": "Alice"})
assert result == "Hello, Alice!"
result = tool.invoke(tool_call(name="greet", args={"name": "Alice"}, id=None))
assert result == "Hello, Alice!"
Tool这种简单的实现只支持“单输入参数”的函数,一旦输入参数超过两个就会出错。如下这个演示程序试图根据一个Lambda表达式创建一个执行加法运算的Tool,invoke方法执行的时候会抛出ToolException,并提示“Too many arguments to single-input tool add. Consider using StructuredTool instead. Args: [1, 2]”
from langchain_core.tools import Tool
tool = Tool.from_function(
func = lambda x, 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字段),并对输入进行实时校验。它还可以直接从指定的函数创建,自动推断参数类型和文档说明。
class StructuredTool(BaseTool):
description: str = ""
args_schema: Annotated[ArgsSchema, SkipValidation()] = Field(
..., description="The tool schema."
)
func: Callable[..., Any] | None = None
coroutine: Callable[..., Awaitable[Any]] | None = None
@override
async def ainvoke(
self,
input: str | dict | ToolCall,
config: RunnableConfig | None = None,
**kwargs: Any,
) -> Any:
if not self.coroutine:
return await run_in_executor(config, self.invoke, input, config, **kwargs)
return await super().ainvoke(input, config, **kwargs)
def _run(
self,
*args: Any,
config: RunnableConfig,
run_manager: CallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> Any:
if self.func:
if run_manager and signature(self.func).parameters.get("callbacks"):
kwargs["callbacks"] = run_manager.get_child()
if config_param := _get_runnable_config_param(self.func):
kwargs[config_param] = config
return self.func(*args, **kwargs)
msg = "StructuredTool does not support sync invocation."
raise NotImplementedError(msg)
async def _arun(
self,
*args: Any,
config: RunnableConfig,
run_manager: AsyncCallbackManagerForToolRun | None = None,
**kwargs: Any,
) -> Any:
if self.coroutine:
if run_manager and signature(self.coroutine).parameters.get("callbacks"):
kwargs["callbacks"] = run_manager.get_child()
if config_param := _get_runnable_config_param(self.coroutine):
kwargs[config_param] = config
return await self.coroutine(*args, **kwargs)
return await super()._arun(
*args, config=config, run_manager=run_manager, **kwargs
)
@classmethod
def from_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就没问题。
from langchain_core.tools import StructuredTool
tool = StructuredTool.from_function(
func = lambda x, y: x + y,
name = "add",
description = "Add two numbers together.")
assert tool.invoke({"x": 1, "y": 2}) == 3
4. @tool装饰器
如果我们调用create_agent指定的工具是一个函数,它会利用@tool装饰器函数将其转换成一个BaseTool对象。由于BaseTool的description是通过函数的docstring创建的,鉴于此字段的重要性,如果指定的函数没有定义docstring,转换过程将会失败。我们也可以将这个装饰器显式应用到自定义的函数上,并指定相应的参数对创建的BaseTool作相应的定制。LangChain为@tool装饰器函数定义了很多重载,最终调用的则是如下这个。
def tool(
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来表示注册的工具。
241

被折叠的 条评论
为什么被折叠?



