工具赋予Agent执行特定任务的能力,工具调用是Agent与外部环境交互的主要方式。无论是LangChain还是MAF,都提供了丰富的工具类型来满足不同的应用场景。下面我们来看看它们都提供了哪些工具,以及它们之间有什么异同。

1. LangChain

create_agent函数的定义可以看出,注册的工具具有三种形式:BaseToolCallable[..., 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具有两个直接子类,分别是ToolStructuredToolTool的实现最为简单,它直接使用封装的同步和异步Callable对象来实现runarun方法。

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对象。由于BaseTooldescription是通过函数的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为工具定义了两种不同的实现(ToolStructuredTool),但是它并没有严格区分具体的实现类型,很多开发人员基本都意识不到这两个类型的存在。类似于MAF和LangChain针对Agent自身设计的差异一样,MAF针对工具也采用多态的设计:定义名为AITool的基类,然后针对具体执行操作的差异定义了一系列派生工具类。

2.1 AITool & AIFunctionDeclaration

与LangChain的BaseTool被定义成可执行对象不同,AITool这个抽象类并未提供用于调用工具的方法。它只是提供了作为公共属性的NameDescription,以及一个用来存放任意属性的AdditionalProperties容器。两个方法GetServiceGetService<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,利用JsonSchemaReturnJsonSchema这两个虚属性提供函数的输入和输出的JSON Schema。

public abstract class AIFunctionDeclaration : AITool
{
	public virtual JsonElement JsonSchema => AIJsonUtilities.DefaultJsonSchema;
	public virtual JsonElement? ReturnJsonSchema => null;
}

2.3 AIFunction

抽象类AIFunction表示以自定义的C#方法定义的工具,它继承自AIFunctionDeclarationUnderlyingMethod返回的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

与利用DelegatingAIAgentDelegatingChatClient作为中间件装饰AIAgentIChatClient类似,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;
	}
}

如下这个派生自DelegatingAIFunctionApprovalRequiredAIFunction,虽然只是一个空架子,但是却是引入人机交互(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;
}

AIFunctionFactoryCreate方法还提供了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信息;

除了AIFunctionDeclarationAITool还有很多子类,并由此引出一系列的工具类型,这里我们就不一一展开了,感兴趣的读者可以参考官方文档进行了解。

2.6 工具注册

ChatClientAgent使用IChatClient来与LLM进行交互,当我们调用ChatClientGetResponseAsync或者GetStreamingResponseAsync方法时,会传入一个ChatOptions对象,其中包含了注册的工具列表。而ChatOptionsChatClientAgentOptions的一部分。

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);
}
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐