欢迎光临
我们一直在努力

[MAF的Agent管道详解-08]依赖注入的应用

借助于MAF提供的这个极具扩展性的Agent管道,我们可以利用它提供的众多扩展点来实现我们针对Agent执行的控制。具体来说,它提供的扩展点主要体现在如下几个方面:

  • 三大中间件:Agent、ChatClient和AIFunction中间件,运行我们将基于请求拦截的横切关注点注入应用到针对Agent、ChatClient和AIFunction的调用;
  • AIContextProvider:通过在调用ChatClient管道前后对AIContext和响应结果(响应消息或者异常处理)的定制,将输入和输出增强机制应用到Agent管道;
  • ChatHistoryProvider:实现针对AIAgent的对话历史持久化。

当我们在自定义上述组件的时候,很多情况下都需要使用到注入的依赖服务。如何提取这些依赖服务对象?以及这些服务对象来源于何处?这就是这篇文章需要解答的问题。

1. 获取当前AgentRunContext

当AIAgent通过调用RunAsync或者RunStreamingAsync方法开始运行后,会创建一个表示Agent运行上下文的AgentRunContext对象。此上下文会被设置到AIAgent类型的静态字段s_currentContex表示的AsyncLocal<AgentRunContext?>对象上,我们可以通过调用静态属性CurrentRunContext得到它。

public abstract class AIAgent
{
private static readonly AsyncLocal<AgentRunContext?> s_currentContext = new AsyncLocal<AgentRunContext>();
public static AgentRunContext? CurrentRunContext
{
get
{
return s_currentContext.Value;
}
protected set
{
s_currentContext.Value = value;
}
}
}

public sealed class AgentRunContext
{
public AIAgent Agent { get; }
public AgentSession? Session { get; }
public IReadOnlyCollection<ChatMessage> RequestMessages { get; }
public AgentRunOptions? RunOptions { get; }
}

AgentRunContext通过四个属性提供了如下的上下文:

  • Agent: 当前执行的Agent对象;
  • Session:执行Agent所在的AgentSession;
  • RequestMessages:调用Agent输入的消息列表;
  • RunOptions: 用于控制Agent运行的配置选项。

2. 利用当前AIAgent提取服务对象

在得到了当前运行的AIAgent对象后,我们可以调用它的GetService或者GetService<TService>方法提取所需的依赖服务。我们知道AIAgent相关的很多组件类型和接口,比如IChatClient、AIContextProvider和ChatHistoryProvider都定义了这两个方法,这意味着可以将这些组件视为一个IServiceProvider。

public abstract class AIAgent
{
public virtual object? GetService(Type serviceType, object? serviceKey = null)
=> (serviceKey == null && serviceType.IsInstanceOfType(this))
? this
: null;

public TService? GetService<TService>(object? serviceKey = null)
=> (GetService(typeof(TService), serviceKey) is TService val)
? val
: default(TService);
}

定义在AIAgent中的GetService和GetService<TService>方法也体现了在其他组件基类中的标准定义:只能把自己作为服务实例提供出去。如果需要对外提供依赖服务,可以重写虚方法GetService。

作为整个Agent管道核心的ChatClientAgent采用如下的方式重写了GetService方法。

public sealed class ChatClientAgent : AIAgent
{
public override object? GetService(Type serviceType, object? serviceKey = null) =>
base.GetService(serviceType, serviceKey) ??
(serviceType == typeof(AIAgentMetadata) ? this._agentMetadata
: serviceType == typeof(IChatClient) ? this.ChatClient
: serviceType == typeof(ChatOptions) ? this._agentOptions?.ChatOptions
: serviceType == typeof(ChatClientAgentOptions) ? this._agentOptions
: this.AIContextProviders?.Select(provider => provider.GetService(serviceType, serviceKey)).FirstOrDefault(s => s is not null)
?? this.ChatHistoryProvider?.GetService(serviceType, serviceKey)
?? this.ChatClient.GetService(serviceType, serviceKey));
}

重写的GetService方法采用如下的策略(顺序)提供指定类型的服务对象:

  • 调用基类的同名方法,意味着如果指定的服务类型为ChatClientAgent,会返回当前这个ChatClientAgent对象,否则返回null;
  • 如果服务类型为AIAgentMetadata(目前的提供的元数据只有提供商的名称),返回_agentMetadata字段;
  • 如果服务类型为IChatClient,返回ChatClient属性,该属性表示有注册的ChatClient中间件和连接LLM的IChatClient对象组成的ChatClient管道;
  • 如果服务类型为ChatClientAgentOptions,返回_agentOptions字段,该字段表示自身的配置选项;
  • 依次利用注册的AIContextProvider、ChatHistoryProvider和ChatClient管道来提供指定的服务对象。

表示Agent中间件的DelegatingAIAgent以虚方法的形式实现了此方法:如果类型为当前类型,则返回自己,否则调用内部AIAgent的同名方法。

public abstract class DelegatingAIAgent : AIAgent
{
protected AIAgent InnerAgent { get; }
public override object? GetService(Type serviceType, object? serviceKey = null)
{
return (serviceKey == null && serviceType.IsInstanceOfType(this))
? this
: InnerAgent.GetService(serviceType, serviceKey);
}
}

3. 作为服务提供者的AIContextProvider和ChatHistoryProvider

正如上面所说,AIContextProvider和ChatHistoryProvider这些基础组件和AIAgent一样定义了GetService和GetService<TService>方法,并且它们的定义方式如出一辙:只能把自己作为服务实例提供出去。

public abstract class AIContextProvider
{
public virtual object? GetService(Type serviceType, object? serviceKey = null)
=> (serviceKey == null && serviceType.IsInstanceOfType(this))
? this
: null;

public TService? GetService<TService>(object? serviceKey = null)
=> (GetService(typeof(TService), serviceKey) is TService val)
? val
: default(TService);
}

public abstract class ChatHistoryProvider
{
public virtual object? GetService(Type serviceType, object? serviceKey = null)
=> (serviceKey == null && serviceType.IsInstanceOfType(this))
? this
: null;

public TService? GetService<TService>(object? serviceKey = null)
=> (GetService(typeof(TService), serviceKey) is TService val)
? val
: default(TService);
}

我已经多次诟病MAF的设计,我觉得这里的设计有违基本的设计原则。作为最外层组件的AIAgent,将它设计成服务提供者,我还能接受,但是将这种设计应用到内部组件类型,比如AIContextProvider和ChatHistoryProvider就说不过去了。作为一个独立的组件,它只需要关注自身的功能实现就可以了,意味着它只需要关注实现自身的功能需要依赖怎样的服务对象,而不需要关心需要对外提供哪些服务对象。

采用这样的设计,单一职责被破坏,基础组件既要完成自己的核心功能,又要承担服务发现。封装性被削弱,调用方不再通过明确依赖注入,而是通过GetService动态解析。耦合性增强,从头到尾都必须遵循同样的服务发现接口,否则链条断裂。这样的设计有违依赖倒置的原则,如果我们将Agent最为最终的产物,正确的做法是框架在构建Agent的时候利用自身的服务注册来为具体的组件注入依赖服务,而不是让Agent作为一个代理的服务提供者,反过来向这些基础组件拉取依赖服务。

可能有人依然无法理解这种反模式。我们具体个例子,假设我们需要自定义一个ChatHistoryProvider,原则上我们只需要实现基于AgentSession针对对话历史的读写就可以,我们无法确定也没有必要对外提供额外的服务对象。所以你会发现,MAF提供的系统预定义组件几乎没有一个会重写GetService方法。换句话说,每个组件只能将自己作为服务对象提供出去(实现在基类的GetService方法中)。既然如此,只需要将提供组件自身实例的逻辑实现在自身的GetService方法上就可以了,完全没有必要将这个Service Location链条延申下去。

4. 依赖在ChatClient管道构建上的应用

从GetService方法在ChatClientAgent中的定义可知,此方法同样定义在IChatClient接口上。表示ChatClient中间件的DelegatingChatClient以虚方法的形式实现了此方法:如果类型为当前类型,则返回自己,否则调用内部IChatClient的同名方法。我认为在此接口上定义GetService方法依然是一种反模式设计,理由同上。

public interface IChatClient : IDisposable
{
...
object? GetService(Type serviceType, object? serviceKey = null);
}

public class DelegatingChatClient : IChatClient, IDisposable
{
...
protected IChatClient InnerClient { get; }
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
if (serviceKey != null || !serviceType.IsInstanceOfType(this))
{
return InnerClient.GetService(serviceType, serviceKey);
}
return this;
}
}

但是在实现在ChatClientBuilder上基于依赖注入的方式来构件ChatClient管道是没有问题的。如下面代码所示,不论是针对内部IChatClient对象还是ChatClient中间件的创建,都体现为一个Func<IServiceProvider, IChatClient>委托,并在Build方法中利用提供的IServiceProvider对象作为依赖注入容器为这些IChatClient对象(含中间件)的创建提供依赖服务。

public sealed class ChatClientBuilder
{
private readonly Func<IServiceProvider, IChatClient> _innerClientFactory;
private List<Func<IChatClient, IServiceProvider, IChatClient>>? _clientFactories;

public ChatClientBuilder(IChatClient innerClient)
=> _innerClientFactory = (IServiceProvider _) => innerClient;

public ChatClientBuilder(Func<IServiceProvider, IChatClient> innerClientFactory)
=> _innerClientFactory = innerClientFactory;

public IChatClient Build(IServiceProvider? services = null)
{
if (services == null)
{
services = EmptyServiceProvider.Instance;
}
IChatClient chatClient = _innerClientFactory(services);
if (_clientFactories != null)
{
for (int num = _clientFactories.Count 1; num >= 0; num)
{
chatClient = _clientFactories[num](chatClient, services);
if (chatClient == null)
{
Microsoft.Shared.Diagnostics.Throw.InvalidOperationException($"The {"ChatClientBuilder"} entry at index {num} returned null. Ensure that the callbacks passed to {"Use"} return non-null {"IChatClient"} instances.");
}
}
}
return chatClient;
}
}

如果调用Build方法没有显示指定IServiceProvider对象,或默认使用如下这个直接返回null的EmptyServiceProvider单例对象。

private sealed class EmptyServiceProvider : IServiceProvider, IKeyedServiceProvider
{
public static EmptyServiceProvider Instance { get; } = new EmptyServiceProvider();

public object? GetService(Type serviceType) => null;
public object? GetKeyedService(Type serviceType, object? serviceKey) => null;
public object GetRequiredKeyedService(Type serviceType, object? serviceKey)
=> throw new InvalidOperationException($"No service for type '{serviceType}' has been registered.");
}

在ChatClientAgent如下这个构造函数中,如果UseProvidedChatClientAsIs配置选项没有被显式设置成true,IServiceProvider会以参数的形式传入WithDefaultAgentMiddleware扩展方法用来注册一系列默认的ChatClient中间件。该方法内部会利用ChatClientBuilder来构建ChatClient管道,并利用提供的IServiceProvider来提供这些中间件所需的依赖服务。目前WithDefaultAgentMiddleware方法在将中间件注册到ChatClientBuilder时,根本没有使用到ISeriviceProvider,意味着目前ChatClientAgent构造函数的services参数根本不需要传,传了了没有用。虽然这种设计冗余没有意义,不过考虑到services是一个可选的此参数,也能接受。

public sealed class ChatClientAgent : AIAgent
{
public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options, ,
ILoggerFactory? loggerFactory = null, IServiceProvider? services = null)
{
...
ChatClient = ((options?.UseProvidedChatClientAsIs ?? false)
? chatClient
: chatClient.WithDefaultAgentMiddleware(options, services));
}
}

5. AIAgent的GetService究竟可以提供什么?

回到最初的问题:当我们自定义一个组件时,可以从AIAgent的静态属性CurrentRunContext得到代表当前AgentRunContext上下文。有了此上下文,我们就能得到当前运行的AIAgent,自然就可以调用其GetService方法得到所需的依赖服务。根据上面对这个方法实现逻辑的介绍,我们总结了一下指定的服务类型具体返回的服务对象:

  • 具体的Agent中间件类型:组成Agent管道的指定类型的Agent中间件。
  • ChatClientAgent: 当前ChatClientAgent对象;
  • AIAgentMetadata: 承载供应上名称的AIAgentMetadata对象;
  • IChatClient:当前ChatClient管道(中间件+连接LLM的IChatClient对象);
  • ChatClientAgentOptions: 当前ChatClientAgent的配置选项;
  • 具体的AIContextProvider类型:注册的指定类型的AIContextProvider对象;
  • 具体的ChatHistoryProvider:注册的指定类型的ChatHistoryProvider;
  • ChatClient中间件类型:组成ChatClient管道的指定类型的ChatClient中间件。

当然,如果我们注册了自定义的AIContextProvider、ChatHistoryProvider或者Agent中间件和ChatClient中间件,对应的类型重写了GetService方法,提供的类型也可以通过当前AIAgent的GetService方法提取到。

赞(0)
未经允许不得转载:171主机测评 » [MAF的Agent管道详解-08]依赖注入的应用
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址