摘要: 作为《ASP.NET Core 3框架揭秘》的升级版,《ASP.NET Core 6框架揭秘》提供了很多新的章节,同时对现有的内容进行大量的修改。虽然本书旨在对ASP.NET Core框架的架构设计和实现原理进行剖析,但是其中提供的258个实例演示却可以作为入门材料,这个系列会将这些演示实例单独提取 阅读全文
posted @ 2022-02-22 08:23 Artech 阅读(29959) 评论(8) 推荐(64)
摘要: `ChatHistoryMemoryProvider`利用我们提供的向量数据库,对每次调用产生的消息针对指定的Scope维度进行存储,并将当前消息作为查询文本,结合设定的Scope维度检索历史消息作为上下文的一部分来参与LLM的推理。除了这种需要我们们自己搭建和维护的基于向量数据库的解决方案之外,我们还可以利用如下两个预定义的`AIContextProvider`调用来实现长期记忆的功能 阅读全文
posted @ 2026-06-22 08:06 Artech 阅读(167) 评论(0) 推荐(1)
摘要: LLM具有固化的知识,而且针对LLM的调用是完全无状态,永远只做一锤子买卖。但是交给Agent的任务基本上不可能一蹴而就,而且还希望Agent具有学习进化的能力。所以你会发现,很多的Harness手段的目的就是为了弥合两者之间的鸿沟。解决这个问题的基本的前提是:需要赋予Agent记忆。 阅读全文
posted @ 2026-06-20 08:09 Artech 阅读(113) 评论(0) 推荐(1)
摘要: Skills针对Agent的重要性是不言而喻的。从本质上讲,Agent Skills就是随着用户与LLM对话的推进,动态加载被称为Skill作为提示词的一种机制。在大部分实现中,Skill的内容会被封装成角色为Tool的消息被添加到对话历史中,因为这样可以借助针对对话历史的压缩实现对老旧Skill的卸载。Agent Skills依然是输入增强的一种形式,所以Agent Skills在MAF中是被`AgentSkillsProvider`的`AIContextProvider`引入的。 阅读全文
posted @ 2026-06-18 08:33 Artech 阅读(106) 评论(1) 推荐(1)
摘要: 作为最核心的AIAgent,`ChatClientAgent`构建了一个管道与LLM交互。为了让管道的输出更符合我们的需求,有两个主要的途径:输入增强(Input Enhancement)和输出增强(Output Enhancement),前者通过通过改变输入让LLM返回更高质量的内容,后者则直接对LLM的输出进行加工处理。个注册的`AIContextProvider`组成的管道位于`ChatClientAgent`管道的中间件部分(前后分别是`AIAgent`中间件管道和`ChatClient`管道),是专门为输入和输出增强设计的。RAG(Retrieval-Augmented Generation)的本质是根据当前上下文检索相关内容来丰富LLM的输入,是典型的输入增强的典型应用场景,它通过`TextSearchProvider`这个预定义的`AIContextProvider`实现。 阅读全文
posted @ 2026-06-17 08:40 Artech 阅读(138) 评论(0) 推荐(3)
摘要: 当工具在执行过程借助注入对话历史的消息来描述当前的情况,以辅助LLM后续能够更加精准的推理,这是非常有价值的。比如工具在执行过程中发现验证的风控风险,可以注入一条`Assistant`消息模拟LLM的回复来提示用户风险的存在。 阅读全文
posted @ 2026-06-15 08:57 Artech 阅读(120) 评论(0) 推荐(2)
摘要: 赋予部署的应用和服务可观测性已经是一个基本的需求,在这方面,`OpenTelemetry`无疑已经称为了事实上的标准。`OpenTelemetryChatClient`是一个预定义的`IChatClient`中间件,它利用重写的`GetResponseAsync`和`GetResponseStreamAsync`方法,为LLM的调用添加了对于的链路和性能计数的输出。结合`OpenTelemetry`框架,开发者可以轻松地将这些数据发送到各种后端系统,如`Prometheus`、`Jaeger`等,以实现对LLM调用的深入分析和监控。本篇文件通过一个简单的例子在本地搭建一个这样的监控环境,展示针对Agent调用的链路跟踪和性能指标。 阅读全文
posted @ 2026-06-12 08:25 Artech 阅读(124) 评论(0) 推荐(3)
摘要: 在默认的情况下,`ChatHistoryProvider`基于**调用**对产生的请求和消息进行存档。如果一次调用涉及多轮ReAct循环,意味着每次调用可能会很多条消息,但是它们只会在ReAct循环结束之后才会被存档一次。如果最后存单失败,意味着这些消息将全部丢失,所以有时候我们ReAct循环的每次迭代都存档一次。这种细粒度的存档方式可以通过注册`PerServiceCallChatHistoryPersistingChatClient`中间件来实现。 阅读全文
posted @ 2026-06-11 08:53 Artech 阅读(157) 评论(0) 推荐(6)
摘要: 我们目前已经有相当专业的图片生成的模型,它可以利用我们提供的文本提示来生成高质量的图片,但是由于我们对文字的驾驭能力不够,写不出迎合LLM的提示词。ImageGeneratingChatClient中间件结合我们注册的ImageGenerator将两者结合在一起:我们通过与Agent对话的方式说出我们对生成图片的描述,LLM根据我们的描述返回专业的提示词文本。注册的ImageGenerator将提示词提交给专门负责图片生成的模型来生成图片。 阅读全文
posted @ 2026-06-10 08:24 Artech 阅读(168) 评论(0) 推荐(2)
摘要: 调用`IChatClient`的`GetResponseAsync`或者`GetStreamingResponseAsync`方法时,我们通常会传入一个`ChatOptions`对象来控制运行行为。`ConfigureOptionsChatClient`利用指定的委托对象来动态设置`ChatOptions`对象。而另一个`AIContextProviderChatClient`中间件则可以利用注册的`AIContextProvider`对象来动态地为每次调用生成一个`AIContext`对象,该对象可以提供请求消息和`ChatOptions`工具和系统指令。 阅读全文
posted @ 2026-06-09 08:53 Artech 阅读(148) 评论(0) 推荐(6)
摘要: 绝大部分的Agent都采用对话的方式来和用户进行交互,所以对话的内容就成了Agent决策的基础,对话历史也成为占据LLM上下文窗口的主要内容。LLM推理的质量并非与上下文的丰富程度成正向关系,有时候过多的上下文信息反而会干扰Agent的判断,导致它做出错误的决策。`ReducingChatClient`就是为了解决这个问题而设计的一个中间件,它通过精减对话内容来帮助Agent更好地理解用户的意图,从而做出更准确的决策。为上下文窗口腾出更多空间也是保证可靠性的一种基本的手段。 阅读全文
posted @ 2026-06-08 07:50 Artech 阅读(173) 评论(1) 推荐(5)
摘要: 我们知道LLM的调用不仅仅是一个耗时的操作,还会产生一定的费用,所以我们希望能够尽可能地减少不必要的调用。`CachingChatClient`就是为此而生的一个中间件实现,它通过在内存中维护一个缓存来存储之前调用LLM的输入和输出,从而避免了对相同输入的重复调用。当我们调用`GetResponseAsync`方法时,`CachingChatClient`会先检查缓存中是否已经存在针对相同输入的响应,如果存在就直接返回缓存中的响应,而不需要再次调用LLM;如果不存在,那么它就会调用LLM来获取响应,并将输入和响应一起存储到缓存中,以便下次使用 阅读全文
posted @ 2026-06-05 08:38 Artech 阅读(206) 评论(0) 推荐(4)
摘要: 在众多预定义的`IChatClient`中间件中,`FunctionInvokingChatClient`无疑是最重要的一个,以至于没有它整个Agent就无法工作了。原因在于驱动Agent执行的核心机制的ReAct循环就是通过`FunctionInvokingChatClient`实现的,我们注册的工具函数最终由它来调用。对于相对敏感的工具函数,我们还需要通过人机交互引入审批流程,这也是通过`FunctionInvokingChatClient`来实现的。 阅读全文
posted @ 2026-06-04 08:45 Artech 阅读(214) 评论(0) 推荐(6)
摘要: `LoggingChatClient`是一个预定义的`IChatClient`中间件,它在调用前后输出日志,帮助我们更好地了解Agent的执行过程。它会记录每次调用的输入和输出,以及调用的时间戳等信息。这对于调试和监控Agent的行为非常有用。 阅读全文
posted @ 2026-06-03 08:53 Artech 阅读(224) 评论(1) 推荐(6)
摘要: 与采用`DelegatingChatClient`中间件装饰`IChatClient`对象并构成`IChatClient`管道的方式类似,我们可以使用`DelegatingAIAgent`代表的`AIAgent`中间件来装饰一个`AIAgent`对象,并构成一个`AIAgent`管道。通过在不同的阶段插入不同的`AIAgent`中间件,我们就可以实现对Agent调用的全方位控制和增强。`DelegatingAIAgent`中间件链条位于整个`AIAgent`管道的最前端。 阅读全文
posted @ 2026-06-02 08:52 Artech 阅读(231) 评论(0) 推荐(1)
摘要: 上面我们介绍了与LLM交互的`IChatClient`管道、持久化对话消息的`ChatHistoryProvider`、以及实现输入和输出增强的`AIContextProvider`,接下来我们来看看`ChatClientAgent`是如何将它们整合在一起的。 阅读全文
posted @ 2026-06-01 13:45 Artech 阅读(190) 评论(0) 推荐(2)
摘要: 在不考虑LLM自身差异的前提下,LLM响应内容的质量和准确性取决于作为输入提供给LLM的消息列表和配置选项,如果能否提供一种灵活的机制动态地定制输入给LLM的消息列表和配置选项,无疑是非常有价值的。另一方面,LLM返回的结果往往也需要经过一些定制化的处理才能满足我们的需求,如果上述的这种机制还能对LLM返回的结果进行定制化处理,那就更加完美了。 阅读全文
posted @ 2026-05-30 09:13 Artech 阅读(170) 评论(0) 推荐(5)
摘要: 针对`IChatClient`的结构化输出可以通过调用如下这些重载的`GetResponseAsync 阅读全文
posted @ 2026-05-29 08:30 Artech 阅读(203) 评论(1) 推荐(3)
摘要: 在`IChatClient`管道的最末端是一个与LLM进行交互的`IChatClient`对象,这个对象负责将最终的请求发送给LLM并返回响应结果。这个`IChatClient`对象的具体类型取决于我们使用的是什么模型以及模型的部署方式。系统提供了很多这样的`IChatClient`实现来支持不同的模型和部署方式。对于目前主流的LLM,我们都可以直接利用其客户端来创建一个对应的`IChatClient`对象. 阅读全文
posted @ 2026-05-28 08:52 Artech 阅读(147) 评论(0) 推荐(3)
摘要: `ChatClientAgent`的管道具有如下的结构,整个结构从右到左大体上由三部分组成:连接LLM的`IChatClient`及其中间件链条;旨在实现输入输出增强的多一个`AIContextProvider`链条;`AIAgent`中间件链条。本篇文章主要关注第一部分,我们将其称为`IChatClient`管道。 阅读全文
posted @ 2026-05-27 09:15 Artech 阅读(325) 评论(0) 推荐(7)
摘要: 和LangChain**万法归一**的设计哲学不同,MAF在设计上采用了**多态**的设计哲学,提供了一个Agent基类,通过继承这个基类来创建不同类型的Agent。虽然MAF的Agent类型多种多样,但最重要的莫过于`ChatClientAgent`,MAF语境下的Agent基本上指的就是这个对象。`ChatClientAgent`采用管道式设计,它利用一些列可扩展的组件构建了Agent和LLM消息交换的通道,还实现ReAct循环。这个管道之于MAF的重要性,可能比中间件管道对于ASP.NET Core的还重要. 阅读全文
posted @ 2026-05-26 08:57 Artech 阅读(344) 评论(0) 推荐(4)
摘要: 基于对话的Chat Agent是目前最主流的Agent类型,它采用的**基于角色的消息**是一种结构化对话机制,它通过将对话内容划分为不同的预设身份(Roles)来引导模型理解其职责和当前上下文。LangChain和MAF针对基于角色的消息设计了一个完整的消息体系,但是它们的设计思路和实现方式却有所不同。 阅读全文
posted @ 2026-05-25 09:00 Artech 阅读(278) 评论(0) 推荐(3)
摘要: Agent是一个能够自主决策和执行任务的Agent,它可以根据用户的输入和上下文信息来规划自己的行动,并利用工具来完成任务。LangChain和MAF针对Agent采用了完全不同的设计哲学和实现方式。虽然LangChain提供了针对Agent的不同创建方式,但是通过这些方式创建的Agent本质却没有什么不同。而MAF的设计则是,为Agent定义一个基类,通过继承该基类来创建不同类型的Agent,这些Agent在执行流程、状态管理、工具调用等方面都有可能有不同的实现。 阅读全文
posted @ 2026-05-22 08:56 Artech 阅读(315) 评论(0) 推荐(5)
摘要: 上篇我们介绍了LangChain和MAF的基本编程模式,包括如何创建Agent、如何注册工具、以及**阻塞式调用**和**流式响应**的编程方式。接下我们就来看看会话保持和流程编排在LangChain和MAF中的实现方式有什么差异。 阅读全文
posted @ 2026-05-21 08:24 Artech 阅读(297) 评论(1) 推荐(6)
摘要: 作为一个资深的.NET开发者,当初决定转移到Agent赛道时,我首先选择的是微软的Semantic Kernel、AutoGen以及后来将二者统一在一起的MAF(Microsoft Agent Framework),但是我总觉得这个框架在设计上差点意思。再加上既然都决定决定投入到AI领域,就没有必要继续把自己困在.NET的茧房,于是选择了LangChain。经过对LangChain深入学习和使用后,再回头看看MAF,会有不一样的体会。通过对比,我觉得我现在对MAF有了更深的理解,而且我发现通过对比学习能够有效了理解MAF的精髓所在。 阅读全文
posted @ 2026-05-20 08:52 Artech 阅读(662) 评论(9) 推荐(12)
摘要: 虽然Python作为AI领域的第一语言,但是作为一个C#的深度使用者,对于Python这门编程语言确实有太多值得吐槽的地方。但是我觉得Python在设计上有一个绝对的亮点,也是我最喜欢的地方,那就是基于元类的元模型,在这篇文章中我会聊聊我对Python元模型理解。 阅读全文
posted @ 2026-04-11 09:04 Artech 阅读(468) 评论(4) 推荐(2)
摘要: 参数在方法种具有按“值(by value)”和“引用(by ref)”两种传递方式,这是每个.NET程序员深入骨髓得基本概念。但是我若告诉你,.NET规定的参数传递形式其实是三种,会不会颠覆你的认知。 阅读全文
posted @ 2024-08-23 08:35 Artech 阅读(2377) 评论(10) 推荐(11)
摘要: 在《可以调用Null的实例方法吗?》一文中,我谈到.NET方法的三种调用形式,现在我们就来着重聊聊这个话题。具体来说,这里所谓的三种方法调用形式对应着三种IL指令:Call、CallVirt和Calli。 阅读全文
posted @ 2024-08-20 08:46 Artech 阅读(3089) 评论(21) 推荐(19)
摘要: 前几天有个网友问我一个问题:调用实例方法的时候为什么目标对象不能为Null。看似一个简单的问题,还真不是一句话就能说清楚的。而且这个结论也不对,当我们调用定义在某个类型的实例方法时,目标对象其实可以为Null。 阅读全文
posted @ 2024-08-19 09:43 Artech 阅读(2804) 评论(7) 推荐(15)
摘要: 当我们使用System.Text.Json.JsonSerializer对一个字典对象进行序列化的时候,默认情况下字典的Key不能是一个自定义的类型,本文介绍几种解决方案。 阅读全文
posted @ 2024-03-19 08:33 Artech 阅读(2649) 评论(4) 推荐(14)
摘要: 毫不夸张地说,路由是ASP.NET Core最为核心的部分。路由的本质就是注册一系列终结点(Endpoint),每个终结点可以视为“路由模式”和“请求处理器”的组合,它们分别用来“选择”和“处理”请求。请求处理器通过RequestDelegate来表示,但是当我们在进行路由编程的时候,却可以使用任意类型的Delegate作为处理器器,这一切的背后是如何实现的呢? 阅读全文
posted @ 2024-03-18 11:51 Artech 阅读(3272) 评论(2) 推荐(20)
摘要: ASP.NET Core MVC的“模块化”设计使我们可以构成应用的基本单元Controller定义在任意的模块(程序集)中,并在运行时动态加载和卸载。《设计篇》介绍了这种为“飞行中的飞机加油”的方案的实现原理?本篇我们将演示将介绍“分散定义Controller”的N种实现方案。源代码从这里下载。 阅读全文
posted @ 2024-03-06 08:56 Artech 阅读(4393) 评论(1) 推荐(18)
摘要: ASP.NET Core MVC的“模块化”设计使我们可以构成应用的基本单元Controller定义在任意的模块(程序集)中,并在运行时动态加载和卸载。这种为“飞行中的飞机加油”的方案是如何实现的呢?该系列的两篇文章将关注于这个主题,本篇着重介绍“模块化”的总体设计,下篇我们将演示将介绍“分散定义Controller”的N种实现方案。 阅读全文
posted @ 2024-03-05 08:44 Artech 阅读(2972) 评论(6) 推荐(16)
摘要: ControllerModel类型的Actions属性包含一组描述有效Action方法的ActionModel对象。对于定义在Controller类型中的所有方法,究竟哪些方法才能成为有效的Action方法呢?所以在正式介绍ActionModel类型之前,我们先来聊聊Action方法的选择规则。 阅读全文
posted @ 2024-02-29 09:56 Artech 阅读(1814) 评论(2) 推荐(12)
摘要: 从编程的角度来看,一个MVC应用是由一系列Controller类型构建而成的,所以对于一个代表应用模型的ApplicationModel对象来说,它的核心就是Controllers属性返回的一组ControllerModel对象,每个ControllerModel对象是应用模型针对Controller类型的描述。 阅读全文
posted @ 2024-02-28 09:37 Artech 阅读(1719) 评论(1) 推荐(13)
摘要: 在对应用模型的基本构建方式具有大致的了解之后,我们来系统地认识一下描述应用模型的ApplicationModel类型。对于一个描述MVC应用模型的ApplicationModel对象来说,它承载的元数据绝大部分是由默认注册的DefaultApplicationModelProvider对象提供的,在接下来针对ApplicationModel及其相关类型(ControllerModel、ActionModel和ParameterModel等)的介绍中,我们还会着重介绍DefaultApplicationModelProvider对象采用怎样的方式提取并设置这些元数据。 阅读全文
posted @ 2024-02-27 08:25 Artech 阅读(821) 评论(2) 推荐(9)
摘要: 我个人觉得这是ASP.NET Core MVC框架体系最核心的部分。原因很简单,MVC框架建立在ASP.NET Core路由终结点上,它最终的目的就是将每个Action方法映射为一个或者多个路由终结点,路由终结点根据附加在Action上的若干元数据构建而成。为了构建描述当前应用所有Action的元数据,MVC框架会提取出定义在当前应用范围内的所有Controller类型,并进一步构建出基于Controller的应用模型。应用模型不仅仅是构建Action元数据的基础,承载API的应用还可以利用它自动生成API开发文档,一些工具甚至可以利用应用模型自动生成消费API的客户端代码。 阅读全文
posted @ 2024-02-26 08:14 Artech 阅读(1775) 评论(1) 推荐(14)
摘要: 我想很多人已经体验过GRPC提供的三种流式消息交换模式,在.NET Core上构建的GRPC应用本质上是采用HTTP2/HTTP3协议的ASP.NET Core应用,我们当然也可以在一个普通的ASP.NET Core应用实现这些流模式。不仅如此,HttpClient也提供了响应的支持,这篇文章通过一个简单的实例提供了相应的实现 阅读全文
posted @ 2024-02-20 08:13 Artech 阅读(2055) 评论(5) 推荐(12)
摘要: 针对“缓冲区”编程是一个非常注重“性能”的地方,我们应该尽可能地避免武断地创建字节数组来存储读取的内容,这样不但会导致大量的字节拷贝,临时创建的字节数组还会带来GC压力。要正确、高效地读写缓冲内容,我们应该对几个我们可能熟悉的类型具有更深的认识。 阅读全文
posted @ 2024-02-19 08:07 Artech 阅读(6614) 评论(8) 推荐(48)
摘要: 由于Memory存储的是单纯的二进制字节,所以原则上我们可以用来它作为媒介,在wasm模块和数组程序之间传递任何类型的数据。在JavaScript API中,Memory通过WebAssembly.Memory类型表示,我们一般将它内部的缓冲区映射相应类型的数组进行处理。WebAssembly也提供了相应的指令来提供针对Memory的读、写、扩容等操作 阅读全文
posted @ 2024-02-05 09:42 Artech 阅读(2168) 评论(5) 推荐(7)
摘要: WebAssembly程序总是以模块来组织,模块是基本的部署、加载和编译单元。在JavaScript编程接口中,模块通过WebAssembly.Module类型表示。WebAssembly.Module通过加载的.wasm二进制文件创建而成,它承载了描述wasm模块的元数据,类似于描述程序集的Assembly对象。WebAssembly.Module自身是只读且无状态的,有状态的是根据它结合指定的导入对象创建的模块实例,后者通过WebAssembly.Instance表示。这两个类型提供了几个核心API,解析我们就通过它们来介绍WebAssembly的这两个核心对象。 阅读全文
posted @ 2024-02-02 09:39 Artech 阅读(761) 评论(0) 推荐(3)