惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Application and Cybersecurity Blog
Application and Cybersecurity Blog
A
About on SuperTechFans
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Help Net Security
Help Net Security
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
O
OpenAI News
美团技术团队
月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
Schneier on Security
Schneier on Security
Webroot Blog
Webroot Blog
Cyberwarzone
Cyberwarzone
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google Online Security Blog
Google Online Security Blog
T
Tenable Blog
S
Security Affairs
博客园_首页
S
Schneier on Security
Security Latest
Security Latest
T
Threat Research - Cisco Blogs
T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
Spread Privacy
Spread Privacy
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
K
Kaspersky official blog
Hugging Face - Blog
Hugging Face - Blog
TaoSecurity Blog
TaoSecurity Blog
博客园 - 聂微东
Vercel News
Vercel News
M
MIT News - Artificial intelligence
T
Troy Hunt's Blog
B
Blog
MongoDB | Blog
MongoDB | Blog
Martin Fowler
Martin Fowler
Attack and Defense Labs
Attack and Defense Labs
L
LINUX DO - 最新话题
D
DataBreaches.Net
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - Franky
W
WeLiveSecurity
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
F
Fortinet All Blogs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
Check Point Blog
H
Hacker News: Front Page

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
[MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型?
Artech · 2026-05-27 · via 博客园_首页

ChatClientAgent的管道具有如下的结构,整个结构从右到左大体上由三部分组成:连接LLM的IChatClient及其中间件链条;旨在实现输入输出增强的多一个AIContextProvider链条;AIAgent中间件链条。本篇文章主要关注第一部分,我们将其称为IChatClient管道。

Alternative Text

1. IChatClient

IChatClient作为Agent与LLM交互的连接器,如果将LLM比作数据库,那么IChatClient就相当于IDbConnectionIDbConnection抽象了数据库的具体实现,让我们可以采用一种编程模式操作数据库,IChatClient让我们在写代码时不需要关心背后到底是哪家的模型。IChatClient接口的GetResponseAsyncGetStreamingResponseAsync方法采用两种不同的形式与LLM交互,前者采用阻塞式调用的方式,后者采用流式调用的方式。

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

GetResponseAsyncGetStreamingResponseAsync方法的参数除了表示一段对话历史的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 IList<string>? StopSequences { get; set; }
    public bool? AllowMultipleToolCalls { get; set; }
    public ChatToolMode? ToolMode { get; set; }
    public IList<AITool>? Tools { get; set; }
    public bool? AllowBackgroundResponses{ get; set; }
    public ResponseContinuationToken? ContinuationToken{ get; set; }
    public Func<IChatClient, 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 ChatToolMode

ChatToolMode定义了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. DelegatingChatClient

IChatClient管道的构建得益于如下这个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 Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null, 
        CancellationToken cancellationToken = default)
        =>InnerClient.GetResponseAsync(messages, options, cancellationToken);

    public virtual IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null, 
        CancellationToken cancellationToken = default)
        => InnerClient.GetStreamingResponseAsync(messages, options, cancellationToken);
}

我们可以通过继承DelegatingChatClient来创建一个自定义的ChatClient,并通过重写GetResponseAsyncGetStreamingResponseAsync方法将调用请求状态给被封装的InnerClient,同时在调用前后添加一些自定义的逻辑来处理请求和响应。实际上这就是中间件的一种实现方式,这些DelegatingChatClient组成的委托链与中间件管道是一回事。通过这种方式,我们可以在不修改原有IChatClient实现的基础上,灵活地添加一些额外的功能,如日志记录、性能监控、请求修改等,从而增强整个IChatClient Pipeline的功能和可定制性。

Alternative Text

2.1 IChatClient管道执行流程

相面的程序很好的演示了将DelegatingChatClient作为IChatClient中间件。我们通过继承DelegatingChatClient创建了一个名为Middleware的中间件类,在这个类中我们可以通过传入两个委托来分别处理请求和响应。在这个示例中,我们创建了三个Middleware对象,并将它们按照foo、bar、baz的顺序进行嵌套。每个Middleware对象在处理请求和响应时都会打印出相应的日志信息来展示它们的调用顺序。最后我们调用GetResponseAsync方法来触发整个IChatClient Pipeline的执行,并打印出最终的响应内容。

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 Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> 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 IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
         IEnumerable<ChatMessage> messages, ChatOptions? options = null, 
         CancellationToken cancellationToken = default)
    => throw new NotImplementedException();
}

public class Middleware(IChatClient innerClient,
    Func<IEnumerable<ChatMessage>, ChatOptions, ValueTask<IEnumerable<ChatMessage>>>? preHandler = null,
    Func<ChatResponse, ChatOptions, ValueTask<ChatResponse>>? postHandler = null) : DelegatingChatClient(innerClient)
{
    private readonly Func<IEnumerable<ChatMessage>, ChatOptions, ValueTask<IEnumerable<ChatMessage>>>? _preHandler = preHandler;
    private readonly Func<ChatResponse, ChatOptions, ValueTask<ChatResponse>>? _postHandler = postHandler;

    public override async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> 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循环引入ChatClientAgent;
  • CachingChatClient/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(Func<IServiceProvider, IChatClient> innerClientFactory)

    public IChatClient Build(IServiceProvider? services = null)

    public ChatClientBuilder Use(Func<IChatClient, IChatClient> clientFactory)
    public ChatClientBuilder Use(Func<IChatClient, IServiceProvider, IChatClient> clientFactory)
    public ChatClientBuilder Use(
        Func<IEnumerable<ChatMessage>, ChatOptions?, Func<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken, Task>, CancellationToken, Task> sharedFunc)
    public ChatClientBuilder Use(
        Func<IEnumerable<ChatMessage>, ChatOptions?, IChatClient, CancellationToken, Task<ChatResponse>>? getResponseFunc, 
        Func<IEnumerable<ChatMessage>, ChatOptions?, IChatClient, CancellationToken, IAsyncEnumerable<ChatResponseUpdate>>? getStreamingResponseFunc)
}

对于四个Use方法,前两个都好理解,都是通过封装指定的IChatClient来创建作为中间件的DelegatingChatClient对象;第三个Use方法根据指定的委托来重写DelegatingChatClientGetResponseAsyncGetStreamingResponseAsync方法,它只会利用指定的委托来加工作为输入的消息列表和ChatOptions,并直接返回InnerClient的的响应结果。第四个Use方法根据指定的两个委托来重写DelegatingChatClientGetResponseAsyncGetStreamingResponseAsync方法。

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 Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> 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 IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
         IEnumerable<ChatMessage> 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, 
    Action<ReducingChatClient>? configure = null)
 public static ChatClientBuilder UseDistributedCache(
    this ChatClientBuilder builder, 
    IDistributedCache? storage = null, 
    Action<DistributedCachingChatClient>? configure = null)
 public static ChatClientBuilder UseLogging(
    this ChatClientBuilder builder, 
    ILoggerFactory? loggerFactory = null, 
    Action<LoggingChatClient>? configure = null)
 public static ChatClientBuilder UseOpenTelemetry(
    this ChatClientBuilder builder, 
    ILoggerFactory? loggerFactory = null, 
    string? sourceName = null, 
    Action<OpenTelemetryChatClient>? configure = null)
 public static ChatClientBuilder UseFunctionInvocation(
    this ChatClientBuilder builder, 
    ILoggerFactory? loggerFactory = null, 
    Action<FunctionInvokingChatClient>? configure = null)
 public static ChatClientBuilder UseImageGeneration(
    this ChatClientBuilder builder, 
    IImageGenerator? imageGenerator = null, 
    Action<ImageGeneratingChatClient>? configure = null)