[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得到它。

publicabstractclassAIAgent{privatestaticreadonlyAsyncLocal<AgentRunContext?>s_currentContext=newAsyncLocal<AgentRunContext>();publicstaticAgentRunContext?CurrentRunContext{get{returns_currentContext.Value;}protectedset{s_currentContext.Value=value;}}}publicsealedclassAgentRunContext{publicAIAgentAgent{get;}publicAgentSession?Session{get;}publicIReadOnlyCollection<ChatMessage>RequestMessages{get;}publicAgentRunOptions?RunOptions{get;}}

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

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

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

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

publicabstractclassAIAgent{publicvirtualobject?GetService(TypeserviceType,object?serviceKey=null)=>(serviceKey==null&&serviceType.IsInstanceOfType(this))?this:null;publicTService?GetService<TService>(object?serviceKey=null)=>(GetService(typeof(TService),serviceKey)isTServiceval)?val:default(TService);}

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

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

publicsealedclassChatClientAgent:AIAgent{publicoverrideobject?GetService(TypeserviceType,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=>sisnotnull)??this.ChatHistoryProvider?.GetService(serviceType,serviceKey)??this.ChatClient.GetService(serviceType,serviceKey));}

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

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

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

publicabstractclassDelegatingAIAgent:AIAgent{protectedAIAgentInnerAgent{get;}publicoverrideobject?GetService(TypeserviceType,object?serviceKey=null){return(serviceKey==null&&serviceType.IsInstanceOfType(this))?this:InnerAgent.GetService(serviceType,serviceKey);}}

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

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

publicabstractclassAIContextProvider{publicvirtualobject?GetService(TypeserviceType,object?serviceKey=null)=>(serviceKey==null&&serviceType.IsInstanceOfType(this))?this:null;publicTService?GetService<TService>(object?serviceKey=null)=>(GetService(typeof(TService),serviceKey)isTServiceval)?val:default(TService);}publicabstractclassChatHistoryProvider{publicvirtualobject?GetService(TypeserviceType,object?serviceKey=null)=>(serviceKey==null&&serviceType.IsInstanceOfType(this))?this:null;publicTService?GetService<TService>(object?serviceKey=null)=>(GetService(typeof(TService),serviceKey)isTServiceval)?val:default(TService);}

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

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

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

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

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

publicinterfaceIChatClient:IDisposable{...object?GetService(TypeserviceType,object?serviceKey=null);}publicclassDelegatingChatClient:IChatClient,IDisposable{...protectedIChatClientInnerClient{get;}publicvirtualobject?GetService(TypeserviceType,object?serviceKey=null){if(serviceKey!=null||!serviceType.IsInstanceOfType(this)){returnInnerClient.GetService(serviceType,serviceKey);}returnthis;}}

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

publicsealedclassChatClientBuilder{privatereadonlyFunc<IServiceProvider,IChatClient>_innerClientFactory;privateList<Func<IChatClient,IServiceProvider,IChatClient>>?_clientFactories;publicChatClientBuilder(IChatClientinnerClient)=>_innerClientFactory=(IServiceProvider_)=>innerClient;publicChatClientBuilder(Func<IServiceProvider,IChatClient>innerClientFactory)=>_innerClientFactory=innerClientFactory;publicIChatClientBuild(IServiceProvider?services=null){if(services==null){services=EmptyServiceProvider.Instance;}IChatClientchatClient=_innerClientFactory(services);if(_clientFactories!=null){for(intnum=_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.");}}}returnchatClient;}}

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

privatesealedclassEmptyServiceProvider:IServiceProvider,IKeyedServiceProvider{publicstaticEmptyServiceProviderInstance{get;}=newEmptyServiceProvider();publicobject?GetService(TypeserviceType)=>null;publicobject?GetKeyedService(TypeserviceType,object?serviceKey)=>null;publicobjectGetRequiredKeyedService(TypeserviceType,object?serviceKey)=>thrownewInvalidOperationException($"No service for type '{serviceType}' has been registered.");}

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

publicsealedclassChatClientAgent:AIAgent{publicChatClientAgent(IChatClientchatClient,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中间件。

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