[MAF预定义Agent中间件-03]FunctionInvocationDelegatingAgent:将AOP引入函数调用

[MAF预定义Agent中间件-03]FunctionInvocationDelegatingAgent:将AOP引入函数调用
工具让Agent具备了与外界交互的能力。按照工具的执行MAF的工具可以划分为服务端或者承载端工具和客户端工具两大类。前者在承载LLM的服务器端执行以Hosted前缀命名的工具比如HostedCodeInterceptorTool、HostedWebSearchTool和HostedImageGenerationTool基本属于这一类。后者是在客户端Agent端定义的函数通过AIFunction来表示。大部分Agent都会涉及AIFunction通过一种调用拦截机制将AOP引入函数调用是很有意义的。使用FunctionInvocationDelegatingAgent中间件也容易实现这一点。1. 利用FunctionInvocationDelegatingAgent拦截函数调用我们通过如下的示例来演示如何利用FunctionInvocationDelegatingAgent中间键来拦截指定的函数调用,并篡改其返回的结果。如下面的代码所示我们定义了一个GetWeather函数并利用AIFunctionFactory将其转换成AIFunction对象。在将OpenAIClient构建成AIAgent对象的时候我们指定了一个ChatClientAgentOptions对象其ChatOptions上注册了这个GetWeather工具。我们调用AsBuilder创建了构建Agent管道的AIAgentBuilder对象并通过调用Use方法完成了FunctionInvocationDelegatingAgent中间件的注册。我们在该方法指定了静态函数InterceptAsync表示的委托来处理函数调用的拦截逻辑。usingdotenv.net;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingOpenAI;usingSystem.ClientModel;usingSystem.ComponentModel;DotEnv.Load();varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varopenAIUrlEnvironment.GetEnvironmentVariable(OPENAI_URL)!;varchatOptionsnewChatOptions{Tools[AIFunctionFactory.Create(GetWeather,nameof(GetWeather))]};varagentnewOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{EndpointnewUri(openAIUrl)}).GetChatClient(model:model).AsIChatClient().AsAIAgent(options:newChatClientAgentOptions{ChatOptionschatOptions}).AsBuilder().Use(InterceptAsync).Build();varresponseawaitagent.RunAsync(苏州目前什么天气);Console.WriteLine(${newstring(-,20)}Agent的回复{newstring(-,20)}{response.Text.Trim()});staticasyncValueTaskobject?InterceptAsync(AIAgentagent,FunctionInvocationContextcontext,FuncFunctionInvocationContext,CancellationToken,ValueTaskobject?next,CancellationTokencancellationToken){varresultawaitnext(context,cancellationToken);if(context.Function.NameGetWeather){varlocationcontext.Arguments[location]?.ToString()??未知地点;Console.WriteLine(${newstring(-,20)}真实的结果{newstring(-,20)}{result});return$现在{location}的天气是雨温度10摄氏度;}returnresult;}[Description(根据指定位置获取天气信息)]staticstringGetWeather(stringlocation)$现在{location}的天气是晴温度25摄氏度;在InterceptAsync方法中我们首先调用next委托来执行原始的函数调用逻辑并获取结果。接着我们判断当前被调用的函数是否是GetWeather如果是的话我们就对结果进行篡改返回一个假的天气信息如果不是的话我们就直接返回原始的结果。从如下的输出结果可以看出LLM提供的答复是根据我们篡改后的结果生成的。--------------------真实的结果-------------------- 现在苏州的天气是晴温度25摄氏度 --------------------Agent的回复-------------------- 我来帮您查询苏州的天气情况 根据查询结果苏州现在的天气情况如下 - ️ **天气**雨 - ️ **温度**10摄氏度 目前苏州正在下雨温度偏低出门记得带伞并注意保暖哦2. 另一种解法我们知道IChatClient和AIAgent都具有各自的中间件类型DelegatingChatClient和DelegatingAIAgent其实AIFunction也有对应的DelegateingFunction类型。我们可以将DelegateingFunction视为AIFunction中间件引入人机交互审批流程的ApprovalRequiredAIFunction就派生于DelegateingFunction。既然如此针对AIFunction的拦截自然也可以通过自定义的DelegateingFunction来实现。如果将上面的实例改成如下的形式也能达到一样的效果。usingdotenv.net;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingOpenAI;usingSystem.ClientModel;usingSystem.ComponentModel;DotEnv.Load();varmodelEnvironment.GetEnvironmentVariable(MODEL)!;varapiKeyEnvironment.GetEnvironmentVariable(API_KEY)!;varopenAIUrlEnvironment.GetEnvironmentVariable(OPENAI_URL)!;vartoolAIFunctionFactory.Create(GetWeather,nameof(GetWeather));toolnewInterceptingTool(tool);varchatOptionsnewChatOptions{Tools[tool]};varagentnewOpenAIClient(newApiKeyCredential(apiKey),newOpenAIClientOptions{EndpointnewUri(openAIUrl)}).GetChatClient(model:model).AsIChatClient().AsAIAgent(options:newChatClientAgentOptions{ChatOptionschatOptions});varresponseawaitagent.RunAsync(苏州目前什么天气);Console.WriteLine(${newstring(-,20)}Agent的回复{newstring(-,20)}{response.Text.Trim()});[Description(根据指定位置获取天气信息)]staticstringGetWeather(stringlocation){return$现在{location}的天气是晴温度25摄氏度;}classInterceptingTool(AIFunctioninnerFunction):DelegatingAIFunction(innerFunction){protectedoverrideasyncValueTaskobject?InvokeCoreAsync(AIFunctionArgumentsarguments,CancellationTokencancellationToken){varresultawaitbase.InvokeCoreAsync(arguments,cancellationToken);if(InnerFunction.NameGetWeather){varlocationarguments[location]?.ToString()??未知地点;Console.WriteLine(${newstring(-,20)}真实的结果{newstring(-,20)}{result});return$现在{location}的天气是雨温度10摄氏度;}returnresult;}}如上面的代码片段所示我们通过继承DelegatingAIFunction来创建了一个InterceptingTool类在其中重写了InvokeCoreAsync方法来实现对函数调用的拦截逻辑。我们在构建Agent的时候直接将这个InterceptingTool作为工具注册到ChatOptions中即可。最终的输出结果和前面完全一样。3. FunctionInvocationContext要理解FunctionInvocationDelegatingAgent中间件的实现原理就先得了解这个用来表示函数执行上下文的FunctionInvocationContext的类型。当Agent接受到携带FunctionCallContent的Assistant消息后在进行对应的函数调用之前它会创建一个FunctionInvocationContext对象来描述针对目标函数的执行这个对象被存储在异步上下文中在针对目标函数的执行周期内都可以被访问到。publicclassFunctionInvocationContext{publicAIFunctionFunction{get;set;}publicAIFunctionArgumentsArguments{get;set;}publicFunctionCallContentCallContent{get;set;}publicIListChatMessageMessages{get;set;}publicChatOptions?Options{get;set;}publicintIteration{get;set;}publicintFunctionCallIndex{get;set;}publicintFunctionCount{get;set;}publicboolTerminate{get;set;}publicboolIsStreaming{get;set;}}FunctionInvocationContext各属性说明如下Function表示被调用的函数对象Arguments表示函数调用的参数CallContent表示函数调用消息中的FunctionCallContent对象Messages表示当前函数调用上下文中的消息列表Options表示当前函数调用上下文中的ChatOptions对象Iteration表示当前函数调用是在整个Agent执行过程中的第几轮迭代FunctionCallIndex表示当前函数调用是在本轮迭代中的第几个函数调用FunctionCount表示本轮迭代中总共的函数调用数量Terminate表示是否终止当前函数调用IsStreaming表示当前函数调用是否为流式调用。上述的用来存储FunctionInvocationContext的异步上下文对应FunctionInvokingChatClient的静态字段_currentContext我们可以利用静态属性CurrentContext来访问当前的FunctionInvocationContext对象。publicclassFunctionInvokingChatClient:DelegatingChatClient{privatestaticreadonlyAsyncLocalFunctionInvocationContext?_currentContextnewAsyncLocalFunctionInvocationContext();publicstaticFunctionInvocationContext?CurrentContext{get_currentContext.Value;protectedset_currentContext.Valuevalue;}}4. FunctionInvocationDelegatingAgent前面我们演示了两个实例前者旨在说明如何使用FunctionInvocationDelegatingAgent中间件来拦截函数调用后者则体现的这种编程模式背后的实现原理也就是针对AIFunction调用的拦截是通过装饰的AIFunction中间件实现的。这个中间件就是如下这个继承自DelegatingAIFunction的FunctionInvocationDelegatingAgent类型。privatesealedclassMiddlewareEnabledFunction(AIAgentinnerAgent,AIFunctioninnerFunction,FuncAIAgent,FunctionInvocationContext,FuncFunctionInvocationContext,CancellationToken,ValueTaskobject?,CancellationToken,ValueTaskobject?next):DelegatingAIFunction(innerFunction){protectedoverrideasyncValueTaskobject?InvokeCoreAsync(AIFunctionArgumentsarguments,CancellationTokencancellationToken){FunctionInvocationContextcontextFunctionInvokingChatClient.CurrentContext??newFunctionInvocationContext{Argumentsarguments,Functionbase.InnerFunction,CallContentnewFunctionCallContent(string.Empty,base.InnerFunction.Name,newDictionarystring,object(arguments))};returnawaitnext(innerAgent,context,CoreLogicAsync,cancellationToken).ConfigureAwait(continueOnCapturedContext:false);ValueTaskobject?CoreLogicAsync(FunctionInvocationContextctx,CancellationTokencancellationToken2){returnbase.InvokeCoreAsync(ctx.Arguments,cancellationToken2);}}}如上面的代码片段所示我们创建一个MiddlewareEnabledFunction对象使需要提供如下三个参数innerAgent表示当前函数调用所在的AIAgent对象innerFunction表示需要被拦截的AIFunction对象next实现了整个拦截操作的委托对象四个参数类型分别是AIAgent表示当前函数调用所在的AIAgent对象FunctionInvocationContext表示当前函数调用的上下文对象FuncFunctionInvocationContext, CancellationToken, ValueTaskobject?表示一个委托用来执行原始的函数调用逻辑CancellationToken表示一个取消标记重写的InvokeCoreAsync方法们首先从FunctionInvokingChatClient的CurrentContext静态属性中获取当前的FunctionInvocationContext对象如果获取不到的话会创建一个新的FunctionInvocationContext对象。接着将内层AIAgent、FunctionInvocationContext对象、用于执行原始AIFunction的委托和CancellationToken作为参数传递给next委托来执行拦截操作。FunctionInvocationDelegatingAgent的实现逻辑非常简单在重写的RunCoreAsync/RunCoreStreamingAsync方法中它会从options参数表示的AgentRunOptions中提取出所有注册的AIFunction对象然后全部装饰上MiddlewareEnabledFunction中间件。构建MiddlewareEnabledFunction提供的这个定义冗长的委托来源于FunctionInvocationDelegatingAgent的第三个构造函数。internalsealedclassFunctionInvocationDelegatingAgent:DelegatingAIAgent{internalFunctionInvocationDelegatingAgent(AIAgentinnerAgent,FuncAIAgent,FunctionInvocationContext,FuncFunctionInvocationContext,CancellationToken,ValueTaskobject?,CancellationToken,ValueTaskobject?delegateFunc);protectedoverrideTaskAgentResponseRunCoreAsync(IEnumerableChatMessagemessages,AgentSession?sessionnull,AgentRunOptions?optionsnull,CancellationTokencancellationTokendefault);protectedoverrideIAsyncEnumerableAgentResponseUpdateRunCoreStreamingAsync(IEnumerableChatMessagemessages,AgentSession?sessionnull,AgentRunOptions?optionsnull,CancellationTokencancellationTokendefault);}FunctionInvocationDelegatingAgent中间件通过针对AIAgentBuilder的如下这个Use扩展方法进行注册。随便吐槽一下这个定义冗长的委托类型实在是让人头疼稍微正常的设计者都会将它定义成一个单独的委托类型。publicstaticclassFunctionInvocationDelegatingAgentBuilderExtensions{publicstaticAIAgentBuilderUse(thisAIAgentBuilderbuilder,FuncAIAgent,FunctionInvocationContext,FuncFunctionInvocationContext,CancellationToken,ValueTaskobject?,CancellationToken,ValueTaskobject?callback)builder.Use((innerAgent,_)newFunctionInvocationDelegatingAgent(innerAgent,callback));}