[MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型?

[MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型? 1. IChatClientIChatClient作为Agent与LLM交互的连接器如果将LLM比作数据库那么IChatClient就相当于IDbConnection。IDbConnection抽象了数据库的具体实现让我们可以采用一种编程模式操作数据库IChatClient让我们在写代码时不需要关心背后到底是哪家的模型。IChatClient接口的GetResponseAsync和GetStreamingResponseAsync方法采用两种不同的形式与LLM交互前者采用阻塞式调用的方式后者采用流式调用的方式。public interface IChatClient : IDisposable { TaskChatResponse GetResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default); object? GetService(Type serviceType, object? serviceKey null); }GetResponseAsync和GetStreamingResponseAsync方法的参数除了表示一段对话历史的ChatMessage集合之外还可以接受一个ChatOptions对象来设置一些与当前对话相关的选项。ChatOptions将各大模型供应商OpenAI, Anthropic, Google等常用的参数进行了标准化。public class ChatOptions { public string? ConversationId { get; set; } public string? Instructions { get; set; } public float? Temperature { get; set; } public int? MaxOutputTokens { get; set; } public float? TopP { get; set; } public int? TopK { get; set; } public float? FrequencyPenalty { get; set; } public float? PresencePenalty { get; set; } public long? Seed { get; set; } public ReasoningOptions? Reasoning { get; set; } public ChatResponseFormat? ResponseFormat { get; set; } public string? ModelId { get; set; } public IListstring? StopSequences { get; set; } public bool? AllowMultipleToolCalls { get; set; } public ChatToolMode? ToolMode { get; set; } public IListAITool? Tools { get; set; } public bool? AllowBackgroundResponses{ get; set; } public ResponseContinuationToken? ContinuationToken{ get; set; } public FuncIChatClient, object?? RawRepresentationFactory { get; set; } public AdditionalPropertiesDictionary? AdditionalProperties { get; set; } }具体配置选项说明如下ConversationId对话ID用于将多个请求关联到同一个对话中Instructions对模型的系统指令或者系统提示词用于引导模型生成符合预期的响应Temperature控制生成文本的随机程度值越大生成的文本越随机值越小生成的文本越确定MaxOutputTokens生成文本的最大Token数量用于控制生成文本的长度TopP控制生成文本的多样性值越小生成的文本越集中在概率较高的选项上值越大生成的文本越分散TopK控制生成文本的多样性值表示在生成每个Token时考虑的候选Token数量值越小生成的文本越集中在概率较高的选项上值越大生成的文本越分散FrequencyPenalty控制生成文本中重复Token的惩罚程度值越大生成的文本中重复Token越少PresencePenalty控制生成文本中已经出现过的Token的惩罚程度值越大生成的文本中已经出现过的Token越少Seed随机数种子用于控制生成文本的随机性设置相同的种子可以得到相同的生成结果Reasoning推理选项用于控制模型在生成文本时的推理过程如是否启用链式思维、推理的深度等ResponseFormat响应格式用于指定模型生成的响应的格式如纯文本、JSON等。如果设置成具有某种格式的JSON Schema可以实现结构化输出ModelId模型ID用于指定使用哪个模型来生成响应StopSequences停止序列用于指定在生成文本时遇到这些序列就停止生成AllowMultipleToolCalls是否允许在生成响应的过程中调用多个工具ToolMode工具模式用于指定在生成响应时如何使用工具Tools工具列表用于指定在生成响应时可用的工具AllowBackgroundResponses是否允许生成后台响应ContinuationToken续订令牌用于在生成响应时继续之前的对话RawRepresentationFactory原始表示工厂用于生成原始表示对象AdditionalProperties附加属性字典用于存储额外的配置信息。1.1 ReasoningOptions设置推理配置选项的ReasoningOptions类型定义如下所示。它的Effort属性用于控制推理的努力程度Output属性用于控制推理输出的详细程度。ReasoningOptions可以帮助我们更好地控制模型在生成文本时的推理过程从而得到更符合预期的响应。public sealed class ReasoningOptions { public ReasoningEffort? Effort { get; set; } public ReasoningOutput? Output { get; set; } } public enum ReasoningEffort { None, Low, Medium, High, ExtraHigh } public enum ReasoningOutput { None, Summary, Full }1.2 ChatToolModeChatToolMode定义了AI模型在对话中如何对待和选择工具。你可以把它理解为给AI下达的工具使用指令。public class ChatToolMode { public static AutoChatToolMode Auto { get; } new AutoChatToolMode(); public static NoneChatToolMode None { get; } new NoneChatToolMode(); public static RequiredChatToolMode RequireAny { get; } new RequiredChatToolMode(null); public static RequiredChatToolMode RequireSpecific(string functionName) new RequiredChatToolMode(functionName); } public sealed class AutoChatToolMode : ChatToolMode { public override bool Equals(object? obj) public override int GetHashCode() } public sealed class NoneChatToolMode : ChatToolMode { public override bool Equals(object? obj)obj is NoneChatToolMode; public override int GetHashCode()typeof(NoneChatToolMode).GetHashCode(); } public sealed class RequiredChatToolMode : ChatToolMode { public string? RequiredFunctionName { get; } public RequiredChatToolMode(string? requiredFunctionName) { if (requiredFunctionName ! null) { Throw.IfNullOrWhitespace(requiredFunctionName, requiredFunctionName); } RequiredFunctionName requiredFunctionName; } public override bool Equals(object? obj) { if (obj is RequiredChatToolMode requiredChatToolMode) { return RequiredFunctionName requiredChatToolMode.RequiredFunctionName; } return false; } public override int GetHashCode()RequiredFunctionName?.GetHashCode(StringComparison.Ordinal) ?? typeof(RequiredChatToolMode).GetHashCode(); }ChatToolMode的四个静态属性返回的四个ChatToolMode对象分别表示四种工具使用模式Auto自动模式AI模型会根据对话的上下文自动决定是否使用工具以及使用哪个工具None无工具模式AI模型在生成响应时不会使用任何工具RequireAny要求使用任意工具模式AI模型在生成响应时必须使用至少一个工具RequireSpecific要求使用特定工具模式AI模型在生成响应时必须使用指定的工具2. DelegatingChatClientIChatClient管道的构建得益于如下这个DelegatingChatClient类。DelegatingChatClient实现了IChatClient接口并且持有一个InnerClient属性来引用管道中的下一个IChatClient对象。我们可以通过继承DelegatingChatClient来创建一个个的中间件组件在这些组件中我们可以在调用InnerClient的方法前后添加一些自定义的逻辑来对请求和响应进行处理从而实现对整个管道的控制和定制。public class DelegatingChatClient : IChatClient, IDisposable { protected IChatClient InnerClient { get; } protected DelegatingChatClient(IChatClient innerClient) InnerClient Throw.IfNull(innerClient, innerClient); public virtual TaskChatResponse GetResponseAsync(IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) InnerClient.GetResponseAsync(messages, options, cancellationToken); public virtual IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) InnerClient.GetStreamingResponseAsync(messages, options, cancellationToken); }我们可以通过继承DelegatingChatClient来创建一个自定义的ChatClient并通过重写GetResponseAsync和GetStreamingResponseAsync方法将调用请求状态给被封装的InnerClient同时在调用前后添加一些自定义的逻辑来处理请求和响应。实际上这就是中间件的一种实现方式这些DelegatingChatClient组成的委托链与中间件管道是一回事。通过这种方式我们可以在不修改原有IChatClient实现的基础上灵活地添加一些额外的功能如日志记录、性能监控、请求修改等从而增强整个IChatClientPipeline的功能和可定制性。2.1 IChatClient管道执行流程相面的程序很好的演示了将DelegatingChatClient作为IChatClient中间件。我们通过继承DelegatingChatClient创建了一个名为Middleware的中间件类在这个类中我们可以通过传入两个委托来分别处理请求和响应。在这个示例中我们创建了三个Middleware对象并将它们按照foo、bar、baz的顺序进行嵌套。每个Middleware对象在处理请求和响应时都会打印出相应的日志信息来展示它们的调用顺序。最后我们调用GetResponseAsync方法来触发整个IChatClientPipeline的执行并打印出最终的响应内容。using Microsoft.Extensions.AI; IChatClient chatClient new LLMChatClient(); chatClient new Middleware(chatClient, preHandler: (messages, options) { Console.WriteLine(baz.pre-handler); return ValueTask.FromResult(messages); }, postHandler: (response, options) { Console.WriteLine(baz.post-handler); return ValueTask.FromResult(response); }); chatClient new Middleware(chatClient, preHandler: (messages, options) { Console.WriteLine(bar.pre-handler); return ValueTask.FromResult(messages); }, postHandler: (response, options) { Console.WriteLine(bar.post-handler); return ValueTask.FromResult(response); }); chatClient new Middleware(chatClient, preHandler: (messages, options) { Console.WriteLine(foo.pre-handler); return ValueTask.FromResult(messages); }, postHandler: (response, options) { Console.WriteLine(foo.post-handler); return ValueTask.FromResult(response); }); var response await chatClient.GetResponseAsync([]); Console.WriteLine($response: {response.Messages.Single().Text}); class LLMChatClient : IChatClient { public void Dispose() { } public TaskChatResponse GetResponseAsync(IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) Task.FromResult(new ChatResponse(new ChatMessage(role: ChatRole.Assistant, content: Hello world!))); public object? GetService(Type serviceType, object? serviceKey null) null; public IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) throw new NotImplementedException(); } public class Middleware(IChatClient innerClient, FuncIEnumerableChatMessage, ChatOptions, ValueTaskIEnumerableChatMessage? preHandler null, FuncChatResponse, ChatOptions, ValueTaskChatResponse? postHandler null) : DelegatingChatClient(innerClient) { private readonly FuncIEnumerableChatMessage, ChatOptions, ValueTaskIEnumerableChatMessage? _preHandler preHandler; private readonly FuncChatResponse, ChatOptions, ValueTaskChatResponse? _postHandler postHandler; public override async TaskChatResponse GetResponseAsync(IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) { messages _preHandler ! null ? await _preHandler.Invoke(messages, options!) : messages; var response await base.GetResponseAsync(messages, options, cancellationToken); if (_postHandler ! null) { response await _postHandler.Invoke(response, options!); } return response; } }输出foo.pre-handler bar.pre-handler baz.pre-handler baz.post-handler bar.post-handler foo.post-handler response: Hello world!2.2 预定义的IChatClient中间件系统通过继承DelegatingChatClient的方式预定义了很多这样的中间件我们这里随便列举了一些LoggingChatClient: 在不修改业务逻辑的前提下透明地记录所有与AI模型的交互细节;FunctionInvokingChatClient这是最强大的内置中间件。它拦截模型的回复如果模型返回的是函数调用请求Function Call由它实施最终的调用然后将结果反馈给模型直到模型给出最终文本回复。我们可以使用它实现联网搜索、查询数据库等自动化插件功能。FunctionInvokingChatClient将最重要的ReAct循环引入ChatClientAgentCachingChatClient/DistributedCachingChatClient对对话请求和响应进行缓存管理。当发送相同的对话历史时它会先检查缓存如Redis或内存。如果命中则直接返回缓存结果不再调用昂贵的AI API。有效地使用它可以节省Token成本、提高重复问题的响应速度OpenTelemetryChatClient集成分布式追踪Tracing和指标Metrics。它利用OpenTelemetry自动记录每个请求的耗时、Token消耗量、模型名称等元数据。在生产环境中监控AI服务的稳定性、性能及费用ConfigureOptionsChatClient在请求发起前动态修改ChatOptions。它接收一个回调函数允许你在不修改业务代码的情况下统一为所有请求注入特定参数如设置默认的Temperature或MaxTokens。可以利用实现全局策略控制例如根据用户等级动态限制输出长度ReducingChatClient管理超长对话上下文。当对话历史过长超过模型限制时该客户端可以执行压缩、截断或总结逻辑确保请求能成功发送给模型。使用它可以处理超长会话防止Token溢出ImageGeneratingChatClient为普通的文本聊天客户端增加了图像生成和处理的能力。它的核心逻辑可以概括为偷梁换柱”与自动翻译。它的作用是让一个原本只能处理文字的模型通过函数调用Function Calling具备生成图片的能力AIContextProviderChatClient它利用指定的一组AIContextProvider来作为上下文的AIContext并使用此上下文包含的消息列表作为输入PerServiceCallChatHistoryPersistingChatClient在每次调用AI服务时自动持久化和管理聊天历史记录确保AI模型在处理请求时能看到之前的对话背景并在请求结束时把新的对话存回去3. ChatClientBuilder为了方便用户构建一个具有多个中间件的IChatClient管道系统提供了一个ChatClientBuilder类。如下面的代码片段所示一个ChatClientBuilder对象可以通过传入一个IChatClient对象或者一个工厂方法来创建。四个Use方法会根据给定的参数创建并注册一个作为中间件的DelegatingChatClient对象Build方法返回的IChatClient对象就是由这些中间件组成的IChatClient管道的入口点了。public sealed class ChatClientBuilder { public ChatClientBuilder(IChatClient innerClient) public ChatClientBuilder(FuncIServiceProvider, IChatClient innerClientFactory) public IChatClient Build(IServiceProvider? services null) public ChatClientBuilder Use(FuncIChatClient, IChatClient clientFactory) public ChatClientBuilder Use(FuncIChatClient, IServiceProvider, IChatClient clientFactory) public ChatClientBuilder Use( FuncIEnumerableChatMessage, ChatOptions?, FuncIEnumerableChatMessage, ChatOptions?, CancellationToken, Task, CancellationToken, Task sharedFunc) public ChatClientBuilder Use( FuncIEnumerableChatMessage, ChatOptions?, IChatClient, CancellationToken, TaskChatResponse? getResponseFunc, FuncIEnumerableChatMessage, ChatOptions?, IChatClient, CancellationToken, IAsyncEnumerableChatResponseUpdate? getStreamingResponseFunc) }对于四个Use方法前两个都好理解都是通过封装指定的IChatClient来创建作为中间件的DelegatingChatClient对象第三个Use方法根据指定的委托来重写DelegatingChatClient的GetResponseAsync和GetStreamingResponseAsync方法它只会利用指定的委托来加工作为输入的消息列表和ChatOptions并直接返回InnerClient的的响应结果。第四个Use方法根据指定的两个委托来重写DelegatingChatClient的GetResponseAsync和GetStreamingResponseAsync方法。namespace Microsoft.Extensions.AI; public static class ChatClientBuilderChatClientExtensions { public static ChatClientBuilder AsBuilder(this IChatClient innerClient) new ChatClientBuilder(innerClient); }系统还为IChatClient提供了一个AsBuilder的扩展方法方便我们直接将一个IChatClient对象转换成一个ChatClientBuilder对象来进行中间件的构建。所以前面演示的实例可以改写成如下的形式:using Microsoft.Extensions.AI; var chatClient new LLMChatClient() .AsBuilder() .Use(getResponseFunc: async (messages, options, client, cancelToken) { Console.WriteLine(foo.pre-handler); var response await client.GetResponseAsync(messages, options, cancelToken); Console.WriteLine(foo.post-handler); return response; },getStreamingResponseFunc:null) .Use(getResponseFunc: async (messages, options, client, cancelToken) { Console.WriteLine(bar.pre-handler); var response await client.GetResponseAsync(messages, options, cancelToken); Console.WriteLine(bar.post-handler); return response; },getStreamingResponseFunc: null) .Use(getResponseFunc: async (messages, options, client, cancelToken) { Console.WriteLine(baz.pre-handler); var response await client.GetResponseAsync(messages, options, cancelToken); Console.WriteLine(baz.post-handler); return response; },getStreamingResponseFunc: null) .Build(); var response await chatClient.GetResponseAsync([]); Console.WriteLine($response: {response.Messages.Single().Text}); class LLMChatClient : IChatClient { public void Dispose() { } public TaskChatResponse GetResponseAsync(IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) Task.FromResult(new ChatResponse(new ChatMessage(role: ChatRole.Assistant, content: Hello world!))); public object? GetService(Type serviceType, object? serviceKey null) null; public IAsyncEnumerableChatResponseUpdate GetStreamingResponseAsync( IEnumerableChatMessage messages, ChatOptions? options null, CancellationToken cancellationToken default) throw new NotImplementedException(); }输出foo.pre-handler bar.pre-handler baz.pre-handler baz.post-handler bar.post-handler foo.post-handler response: Hello world!对于上面列出的那些系统预定义的中间件系统定义了如下的扩展方法进行注册public static ChatClientBuilder UsePerServiceCallChatHistoryPersistence(this ChatClientBuilder builder) public static ChatClientBuilder UseAIContextProviders( this ChatClientBuilder builder, params AIContextProvider[] providers) public static ChatClientBuilder UseChatReducer( this ChatClientBuilder builder, IChatReducer? reducer null, ActionReducingChatClient? configure null) public static ChatClientBuilder UseDistributedCache( this ChatClientBuilder builder, IDistributedCache? storage null, ActionDistributedCachingChatClient? configure null) public static ChatClientBuilder UseLogging( this ChatClientBuilder builder, ILoggerFactory? loggerFactory null, ActionLoggingChatClient? configure null) public static ChatClientBuilder UseOpenTelemetry( this ChatClientBuilder builder, ILoggerFactory? loggerFactory null, string? sourceName null, ActionOpenTelemetryChatClient? configure null) public static ChatClientBuilder UseFunctionInvocation( this ChatClientBuilder builder, ILoggerFactory? loggerFactory null, ActionFunctionInvokingChatClient? configure null) public static ChatClientBuilder UseImageGeneration( this ChatClientBuilder builder, IImageGenerator? imageGenerator null, ActionImageGeneratingChatClient? configure null)