[对比学习LangChain和MAF-06]工具的定义和注册
工具赋予Agent执行特定任务的能力,工具调用是Agent与外部环境交互的主要方式。无论是LangChain还是MAF,都提供了丰富的工具类型来满足不同的应用场景。下面我们来看看它们都提供了哪些工具,以及它们之间有什么异同。
1. LangChain
从create_agent函数的定义可以看出,注册的工具具有三种形式:BaseTool、Callable[..., Any]和dict[str, Any]。BaseTool是工具的基类,Callable[..., Any]是一个可执行对象,这个我们都好理解,为什么还可以指定一个dict[str, Any]类型的工具呢?其实这里的工具更多体现为提供给LLM用于描述工具的元信息,比如工具的名称、功能描述、输入输出Schema信息等。由于LLM只需要根据这些信息生成针对工具的调用意图,至于工具的执行则是Agent自身的行为。所以这里的create_agent函数的tools参数,与其说是提供注册的工具,还不如说是提供注册工具的声明。
def create_agent(
...
tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,
...
)
1.1 BaseTool
作为工具基类的BaseTool继承自RunnableSerializable,这意味着它可以成为LCEL链上的一环。它的输入可以是一个ToolCall对象,也可以是一个字符串或者字典,它们承载着调用工具的输入参数。下面给出了BaseTool字段成员的定义:
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
name: str
description: str
args_schema: Annotated[ArgsSchema | None, SkipValidation()]
return_direct: bool = False
verbose: bool = False
callbacks: Callbacks
tags: list[str] | None = None
metadata: dict[str, Any] | None = None
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"
extras: dict[str, Any] | None = None
字段成员说明如下:
- name:工具的名称,必须唯一,用于在LLM生成工具调用意图时标识要调用的工具;
- description:工具的功能描述,用于向LLM说明这个工具是干什么的;
- args_schema:工具调用的输入参数Schema,用于向LLM说明调用这个工具需要提供哪些参数,这些参数的类型是什么,以及它们的功能说明等信息;
- return_direct:一个布尔值,表示工具调用的结果是否直接返回给LLM,如果为true,表示工具调用的结果会直接返回给LLM,而不是经过Agent的后续处理;
- verbose:一个布尔值,表示工具调用是否启用详细日志,如果为true,工具调用过程中会输出更多的日志信息,便于调试和监控;
- callbacks:工具调用的回调函数,用于在工具调用的不同阶段执行一些自定义的操作,比如在工具调用前后执行一些额外的逻辑,或者在工具调用过程中处理一些特定的事件等;
- tags:工具的标签列表,用于对工具进行分类和组织,便于在LLM生成工具调用意图时根据标签来选择要调用的工具;
- metadata:工具的元信息字典,可以存储一些额外的信息,比如工具的版本、作者、创建时间等,这些信息可以在LLM生成工具调用意图时提供给LLM,帮助LLM更好地理解这个工具;
- handle_tool_error:一个布尔值、字符串或者函数,用于处理工具调用过程中发生的异常。
- 如果为true,表示工具调用过程中发生的异常会被捕获并处理;
- 如果为字符串,表示当工具调用发生异常时返回这个字符串;
- 如果为函数,表示当工具调用发生异常时会调用这个函数,并将异常对象作为参数传递给函数,函数的返回值会作为工具调用的结果返回给LLM;
- handle_validation_error:一个布尔值、字符串或者函数,用于处理工具调用过程中发生的验证异常。
- 如果为true,表示工具调用过程中发生的验证异常会被捕获并处理;
- 如果为字符串,表示当工具调用发生验证异常时返回这个字符串;
- 如果为函数,表示当工具调用发生验证异常时会调用这个函数,并将验证异常对象作为参数传递给函数,函数的返回值会作为工具调用的结果返回给LLM;
- response_format:一个字符串,表示工具调用结果的格式。
- 默认为"content",表示工具调用的结果只包含内容;
- 如果为"content_and_artifact",表示工具调用的结果既包含内容,也包含一个或多个工件(artifact),工件可以是一些额外的数据或者文件等(比如LLM生成的图片、文档等),这些工件会被封装成ToolCallResult对象的一部分返回给LLM;
- extras:一个字典,用于存储一些额外的信息,这些信息可以在LLM生成工具调用意图时提供给LLM,帮助LLM更好地理解这个工具,或者在工具调用过程中提供一些额外的上下文信息等;
BaseTool通过重写基类的invoke/ainvoke方法来实现了针对工具的调用,但是具体的调用逻辑是由run/arun方法来实现的。
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
@override
def invoke(
self,
input: str | dict | ToolCall,
config: RunnableConfig | None = None,
**kwargs: Any,
) -> Any
@override
async def ainvoke(
self,
input: str | dict | ToolCall,
config: RunnableConfig | None = None,
**kwargs: Any,
) -> Any
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
async def arun(
self,
tool_input: str | dict,
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
1.2 Tool & StructuredTool
BaseTool具有两个直接子类,分别是Tool和StructuredTool。Tool的实现最为简单,它直接使用封装的同步和异步Callable对象来实现run和arun方法。
class Tool(BaseTool):
description: str = ""
func: Callable[..., str] | None
coroutine: Callable[..., Awaitable[str]] | None = None
...
class StructuredTool(BaseTool):
description: str = ""
args_schema: Annotated[ArgsSchema, SkipValidation()]
func: Callable[..., Any] | None = None
coroutine: Callable[..., Awaitable[Any]] | None = None
...
由于没有针对输出Schema的描述,所以Tool这种简单的实现只支持单输入参数的函数。这一缺陷在StructuredTool中利用args_schema得到了修复,所以Agent中基本上使用的工具都是一个StructuredTool对象。
1.3 @tool装饰器
如果我们调用create_agent指定的工具是一个函数,它会利用@tool装饰器函数将其转换成一个BaseTool对象。由于BaseTool的description是通过函数的docstring创建的,鉴于此字段的重要性,如果指定的函数没有定义docstring,转换过程将会失败。我们也可以将这个装饰器显式应用到自定义的函数上,并指定相应的参数对创建的BaseTool作相应的定制。LangChain为@tool装饰器函数定义了很多重载,最终调用的则是如下这个。这个装饰器会将函数转换成一个StructuredTool对象。
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]:
再回到create_agent函数的定义,看看它针对三种不同形态注册工具的处理:
- 标注了
@tool装饰器的函数:这是一个StructuredTool对象,它不仅承载了提供给LLM用于描述工具的元数据,自身也是一个可执行对象。当Agent得到LLM生成的代表调用意图的ToolCall对象时,直接调用此对象就能完成工具的调用; - 一个常规的函数:会自动调用
@tool装饰器将其转换成一个StructuredTool对象; - 一个字典对象:它只提供了LLM所需的工具声明,所以Agent在得到LLM生成的代表调用意图的
ToolCall对象时,需要采用其他的方式完成工具的调用。比如我们会在Agent中维护一个工具名称到工具执行函数的映射表,当得到LLM生成的ToolCall对象时,根据其中的工具名称从映射表中找到对应的工具执行函数,并调用它来完成工具调用;
关于LangChain工具的更多细节,在我的文章“赋予Agent执行力的工具是个什么东西?”中有更详细的介绍,感兴趣的读者可以点击链接查看。
2. MAF
虽然LangChain为工具定义了两种不同的实现(Tool和StructuredTool),但是它并没有严格区分具体的实现类型,很多开发人员基本都意识不到这两个类型的存在。类似于MAF和LangChain针对Agent自身设计的差异一样,MAF针对工具也采用多态的设计:定义名为AITool的基类,然后针对具体执行操作的差异定义了一系列派生工具类。
2.1 AITool & AIFunctionDeclaration
与LangChain的BaseTool被定义成可执行对象不同,AITool这个抽象类并未提供用于调用工具的方法。它只是提供了作为公共属性的Name和Description,以及一个用来存放任意属性的AdditionalProperties容器。两个方法GetService和GetService<TService>作为一个DI容器多外提供注入的服务。
public abstract class AITool
{
public virtual string Name { get; }
public virtual string Description { get; }
public virtual IReadOnlyDictionary<string, object?> AdditionalProperties { get; }
public virtual object? GetService(Type serviceType, object? serviceKey = null);
public TService? GetService<TService>(object? serviceKey = null);
}
2.2 AIFunctionDeclaration
工具在Agent中的首要任务是为LLM提供描述信息的工具声明,这样LLM才才知道每个工具应该在怎样的场景下被调用,并根据输入Schema来提取和生成参数列表。MAF转为为工具声明定义了专门的基类,不过被声明的工具主要体现为代码编写的函数,所以这个基类被命名为AIFunctionDeclaration。它继承自AITool,利用JsonSchema和ReturnJsonSchema这两个虚属性提供函数的输入和输出的JSON Schema。
public abstract class AIFunctionDeclaration : AITool
{
public virtual JsonElement JsonSchema => AIJsonUtilities.DefaultJsonSchema;
public virtual JsonElement? ReturnJsonSchema => null;
}
2.3 AIFunction
抽象类AIFunction表示以自定义的C#方法定义的工具,它继承自AIFunctionDeclaration。UnderlyingMethod返回的MethodInfo提供绑定的方法,我们可以调用InvokeAsync方法利用传入的参数来调用此方法。InvokeAsync方法最终会调用抽象方法InvokeCoreAsync来完成工具调用的具体逻辑实现,派生类通过重写此方法完成具体的调用。除此之外,AIFunction还提供了JsonSerializerOptions属性用于指定工具调用过程中使用的JsonSerializerOptions对象,以及AsDeclarationOnly方法用于将一个可调用的AIFunction转换成一个仅包含声明信息的AIFunctionDeclaration对象,具体是一个NonInvocableAIFunction对象。
public abstract class AIFunction : AIFunctionDeclaration
{
private sealed class NonInvocableAIFunction : DelegatingAIFunctionDeclaration
{
public NonInvocableAIFunction(AIFunction function)
: base(function)
{
}
}
public virtual MethodInfo? UnderlyingMethod => null;
public virtual JsonSerializerOptions JsonSerializerOptions => AIJsonUtilities.DefaultOptions;
public ValueTask<object?> InvokeAsync(
AIFunctionArguments? arguments = null,
CancellationToken cancellationToken = default)
=>InvokeCoreAsync(arguments ?? new AIFunctionArguments(), cancellationToken);
protected abstract ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken);
public AIFunctionDeclaration AsDeclarationOnly()=>new NonInvocableAIFunction(this);
}
public class AIFunctionArguments : IDictionary<string, object?>, IReadOnlyDictionary<string, object?>
2.4 DelegatingAIFunction
与利用DelegatingAIAgent和DelegatingChatClient作为中间件装饰AIAgent和IChatClient类似,MAF也提供了DelegatingAIFunction作为中间件来装饰AIFunction。我们可以通过重写它的InvokeAsync方法来在工具调用前后添加一些额外的操作,比如日志记录、输入输出参数的加工处理、异常处理、调用链路追踪等。
public class DelegatingAIFunction : AIFunction
{
protected AIFunction InnerFunction { get; }
public override string Name => InnerFunction.Name;
public override string Description => InnerFunction.Description;
public override JsonElement JsonSchema => InnerFunction.JsonSchema;
public override JsonElement? ReturnJsonSchema => InnerFunction.ReturnJsonSchema;
public override JsonSerializerOptions JsonSerializerOptions => InnerFunction.JsonSerializerOptions;
public override MethodInfo? UnderlyingMethod => InnerFunction.UnderlyingMethod;
public override IReadOnlyDictionary<string, object?> AdditionalProperties => InnerFunction.AdditionalProperties;
protected DelegatingAIFunction(AIFunction innerFunction)=>InnerFunction = innerFunction;
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
=>InnerFunction.InvokeAsync(arguments, cancellationToken);
public override object? GetService(Type serviceType, object? serviceKey = null)
{
if (serviceKey != null || !serviceType.IsInstanceOfType(this))
{
return InnerFunction.GetService(serviceType, serviceKey);
}
return this;
}
}
如下这个派生自DelegatingAIFunction的ApprovalRequiredAIFunction,虽然只是一个空架子,但是却是引入人机交互(Human-in-the-Loop)的基础。如果某个工具的调用需要经过人工审批,我们就可以利用这个ApprovalRequiredAIFunction来装饰它。它没有任何逻辑,所以被作为一个强类型的标签(Marker)使用。
public sealed class ApprovalRequiredAIFunction : DelegatingAIFunction
{
public ApprovalRequiredAIFunction(AIFunction innerFunction)
: base(innerFunction)
{
}
}
2.5 AIFunctionFactory
AIFunction是一个抽象类,DelegatingAIFunction是一个包装器,ApprovalRequiredAIFunction是一个标签,那么具体的工具实现又应该是什么样子的呢?MAF提供了一个AIFunctionFactory的抽象类来定义工具实现的工厂,我们可以调用如下所示的一系列静态Create方法根据指定MethodInfo或者Delegate来创建一个AIFunction对象。这些方法返回的是一个ReflectionAIFunction对象,它利用反射来调用被包装的方法(其实可以通过Express进行优化以提高性能),并且要求是静态方法。我们也可以通过CreateDeclaration方法来创建一个仅包含声明信息的AIFunctionDeclaration对象。
public static partial class AIFunctionFactory
{
public static AIFunction Create(
Delegate method,
AIFunctionFactoryOptions? options);
public static AIFunction Create(
Delegate method,
string? name = null,
string? description = null,
JsonSerializerOptions? serializerOptions = null);
public static AIFunction Create(
MethodInfo method,
object? target,
AIFunctionFactoryOptions? options);
public static AIFunction Create(
MethodInfo method,
object? target,
string? name = null,
string? description = null,
JsonSerializerOptions? serializerOptions = null);
public static AIFunction Create(
MethodInfo method,
Func<AIFunctionArguments, object> createInstanceFunc,
AIFunctionFactoryOptions? options = null);
public static AIFunctionDeclaration CreateDeclaration(
string name,
string? description,
JsonElement jsonSchema,
JsonElement? returnJsonSchema = null);
private sealed class ReflectionAIFunction : AIFunction;
}
AIFunctionFactory的Create方法还提供了AIFunctionFactoryOptions参数用于定制创建的工具对象。
public sealed class AIFunctionFactoryOptions
{
public JsonSerializerOptions? SerializerOptions { get; set; }
public AIJsonSchemaCreateOptions? JsonSchemaCreateOptions { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public IReadOnlyDictionary<string, object?>? AdditionalProperties { get; set; }
public Func<ParameterInfo, ParameterBindingOptions>? ConfigureParameterBinding { get; set; }
public Func<object?, Type?, CancellationToken, ValueTask<object?>>? MarshalResult { get; set; }
public bool ExcludeResultSchema { get; set; }
public readonly record struct ParameterBindingOptions
{
public Func<ParameterInfo, AIFunctionArguments, object?>? BindParameter { get; init; }
public bool ExcludeFromSchema { get; init; }
}
}
配置选项说明如下:
- SerializerOptions:工具调用过程中使用的
JsonSerializerOptions对象; - JsonSchemaCreateOptions:生成工具输入参数JSON Schema的选项;
- Name:工具的名称,必须唯一,用于在LLM生成工具调用意图时标识要调用的工具;
- Description:工具的功能描述,用于向LLM说明这个工具是干什么的
- AdditionalProperties:一个字典,用于存储一些额外的信息,这些信息可以在LLM生成工具调用意图时提供给LLM,帮助LLM更好地理解这个工具,或者在工具调用过程中提供一些额外的上下文信息等;
- ConfigureParameterBinding:一个函数,用于配置工具方法参数的绑定方式。它接受一个
ParameterInfo对象,表示工具方法的参数信息,以及一个AIFunctionArguments对象,表示工具调用时传入的参数集合。函数返回一个ParameterBindingOptions对象,用于指定这个参数的绑定方式,比如提供一个BindParameter函数来实现自定义的参数绑定逻辑,或者指定ExcludeFromSchema为true来将这个参数从输入参数JSON Schema中排除等; - MarshalResult:一个函数,用于对工具方法的返回结果进行处理。它接受工具方法的返回值、返回值的类型以及一个
CancellationToken对象,函数返回一个经过处理的结果对象,这个结果对象会作为工具调用的结果返回给LLM; - ExcludeResultSchema:一个布尔值,表示是否在工具输入参数JSON Schema中排除返回值的Schema信息。如果为true,生成的工具输入参数JSON Schema将不包含返回值的Schema信息;
除了AIFunctionDeclaration,AITool还有很多子类,并由此引出一系列的工具类型,这里我们就不一一展开了,感兴趣的读者可以参考官方文档进行了解。
2.6 工具注册
ChatClientAgent使用IChatClient来与LLM进行交互,当我们调用ChatClient的GetResponseAsync或者GetStreamingResponseAsync方法时,会传入一个ChatOptions对象,其中包含了注册的工具列表。而ChatOptions是ChatClientAgentOptions的一部分。
public interface IChatClient : IDisposable
{
Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default);
IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default);
object? GetService(Type serviceType, object? serviceKey = null);
}
public class ChatOptions
{
public IList<AITool>? Tools { get; set; }
...
}
public sealed class ChatClientAgentOptions
{
public ChatOptions? ChatOptions { get; set; }
...
}
具体来说,我们可以调用IChatClient如下所示的两个AsAIAgent扩展方法来创建一个ChatClientAgent对象,直接通过tools参数注册工具,或者将工具注册到ChatClientAgentOptions配置选项上。
public static class ChatClientExtensions
{
public static ChatClientAgent AsAIAgent(
this IChatClient chatClient,
string? instructions = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null);
public static ChatClientAgent AsAIAgent(
this IChatClient chatClient,
ChatClientAgentOptions? options,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null);
对于ChatClientAgent管道中用来增强输入和输出的AIContextProvider来说,InvokingAsync方法执行的InvokingContext上下文是对AIContext的一个包装,工具是AIContext上下文中的一个重要组成部分。所以我们可以自定义AIContextProvider,并利用重写的InvokingCoreAsync或者ProvideAIContextAsync方法实现工具的动态注册。
public sealed class AIContext
{
public string? Instructions { get; set; }
public IEnumerable<ChatMessage>? Messages { get; set; }
public IEnumerable<AITool>? Tools { get; set; }
}
public abstract class AIContextProvider
{
public sealed class InvokingContext
{
public AIContext AIContext { get; }
...
}
protected virtual async ValueTask<AIContext> InvokingCoreAsync(
InvokingContext context,
CancellationToken cancellationToken = default);
protected virtual ValueTask<AIContext> ProvideAIContextAsync(
InvokingContext context,
CancellationToken cancellationToken = default);
}
更多推荐

所有评论(0)