news 2026/10/20 2:55:47

.NET中构建AI智能体:Microsoft Agent Framework实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET中构建AI智能体:Microsoft Agent Framework实战指南

不知道你有没有遇到过这种场景:公司技术栈明明以 .NET 为主,结果一聊到 AI 智能体,团队里默认的潜台词就是“这东西得用 Python 写”。我一开始也是这么想的,直到我把一个内部业务流程的智能体原型用 .NET 写完、跑通,才发现这个刻板印象耽误了不少事。现在.NET 生态里已经有了微软官方的 Agent Framework(现在一般叫 Microsoft Agent Framework,简称 MAF),它设计思路很新,而且跟 .NET 的异步模型、依赖注入、日志体系能比较自然地融合。如果你也想在 .NET 里搭一个 AI 智能体,或者只是想多了解一种可落地的智能体编排方案,这篇文章值得你花几分钟顺着我的实际操作思路看一遍。

这篇不是官方文档复述,而是我实打实从安装包、写代码、调 bug、重新设计拓扑一路走下来的记录。我会把框架里最容易卡人的几个概念用大白话讲清楚,再给一套可以直接抄作业的多智能体示例代码,最后把调试和排查经验也整理出来。不管你是做后端、系统集成还是架构设计,只要吃过“想上 AI 但被语言栈绑住”的亏,应该都能从里面捞到点有用的东西。

1. 为什么是 .NET,为什么是现在

1.1 .NET 做智能体到底缺什么

智能体这个词这几年被用得特别泛滥,但核心诉求其实就一句话:让程序不再只是“处理请求”,而是能自己拆解任务、调用工具、调用模型、根据结果决定下一步动作。要支撑这种动态流程,框架层面至少得解决四件事:怎么跟大模型对话、怎么管理多步执行状态、怎么让多个智能体协作、怎么把上面这些东西接进现有业务系统。

.NET 在“跟大模型对话”这件事上毫不含糊,官方 SDK 提供的流式接口、可观测性、重试机制都很成熟。真正缺的是后面三件事的统一抽象。以前我们想在 .NET 里做多智能体协作,往往要自己定义消息格式、自己实现调度循环、自己维护一个状态机。这些东西不是做不到,而是每个项目都从零造一遍轮子,最后长得都不一样,维护成本奇高。

微软解决这个问题的思路很直接。它把智能体拆成几个基础抽象,加上一个叫“拓扑”(Topology)的编排层,再用本地 Runner 把整个流程跑起来。不需要天然地依赖某个云服务,你想接什么模型就接什么模型,传递信息靠的是统一的“Activity”(活动)。我刚开始看到 Activity 这个概念的时候就觉得挺亲切的,因为它就像一个强类型版本的消息包,比较像我们做通讯协议时定义的报文结构。

1.2 Agent Framework 能带来的最大价值

这个框架给我的核心印象是:它并不指望你写多少“魔法代码”,而是逼着你把所有逻辑沉淀成清晰的模块。一个智能体在 MAF 里就是一个普通类,继承AgentBase,重写OnActivityAsync方法。任何一个外部事件、命令、消息,都可以通过 Activity 进入智能体。你是不是联想起什么了?对,有点像我们写 MVC 里的控制器路由,只不过这次处理的不光是 HTTP 请求,还有模型输出。

它的第二个价值点在于“干净结构”。用 MAF 写出来的智能体,业务逻辑和模型调用是解耦的,模型只是智能体内部的一个执行部件。这意味着你可以很自信地把智能体逻辑交给单元测试跑,不需要真的调外部 API。我后来把核心流程的逻辑抽取成纯计算模块之后,单测覆盖率一下子就拉起来了。

第三个价值点则是多智能体编排的“拓扑”设计。MAF 允许你定义一个拓扑类,在里面声明各个 Agent 的依赖关系,然后 Runner 会根据 Activity 的流向自动路由。这个设计避免了常见的“智能体互相调用到晕头”的问题,因为协作关系是在拓扑里静态声明的,而不是运行时靠 Agent 自己临时决定。

2. 先动手:五分钟跑通一个本地智能体

2.1 环境准备和包介绍

我建议直接用最新的 .NET (比如 .NET 8 或更新版本) 作为宿主环境。建好一个空的控制台项目后,需要加入几个 NuGet 包。注意,这个框架的包名有一个“名不副实”的坑——核心包目前叫Microsoft.Agent.Framework,不是 “Semantic Kernel” 也不是 “Agent SDK”。我最早就在这一步找错了方向,把熊搞成大白菜了。你需要装的基础组合是:

  • 核心抽象包:Microsoft.Agent.Framework
  • 跑测试用的本地执行器:Microsoft.Agent.Framework.Runner
  • 如果要用 Azure OpenAI 或者 OpenAI,再装对应的模型封装包(比如Microsoft.Agent.Framework.Connector或官方模型接入包,具体名字看当前版本)

配置层面比我想象的简单,连接字符串和模型信息塞进appsettings.json即可。MAF 内部自带配置绑定,你不用自己写一堆IOptions模板代码,直接用框架的AgentApplicationBuilder就能完成服务注册和配置读取。

注意: 不同预览版本的包名和命名空间可能会微调,建议看你当前 NuGet 页面的最新示例。如果遇到编译错误,优先检查是不是把“Framework”和“Runtime”两套包混用了。

2.2 第一个 Agent:Hello World

先用最直白的方式感受一下智能体的骨架。新建一个类,继承AgentBase:

using Microsoft.Agent.Framework; using Microsoft.Agent.Framework.Activities; public class GreetingAgent : AgentBase { protected override async Task OnActivityAsync(IContext context, CancellationToken ct) { if (context.Activity.TryGetMessagePayload(out var message)) { var reply = $"收到你的消息了: {message.Text}。我还能继续做事,不只是打招呼。"; context.Transmit(reply.ToAgentActivity()); } } }

这段代码看起来是不是像普通的事件处理器?框架里最关键的一个设计就是:传入的 Activity 是一个多态容器。你不需要用if...else分辨一堆子类,而是用TryGetMessagePayload、TryGetCommandPayload、TryGetComponentPayload这类方法去尝试提取不同业务载荷。这比判断字符串类型可靠得多,也满足开闭原则。

然后是注册和运行。MAF 里的本地 Runner 很适合调试,它甚至会配对拓扑文件,让你直接看到 Activity 在哪些 Agent 之间的传播路径。

using Microsoft.Extensions.Hosting; using Microsoft.Agent.Framework; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddAgentFramework(agentBuilder => { agentBuilder.AddAgent<GreetingAgent>("greeter"); }); var app = builder.Build(); var runner = app.Services.GetRequiredService<RunnerBase>(); await runner.RunAsync(app.Services, ct);

运行起来之后,往控制台输入任意一句文本,智能体就能基于注册好的模型返回结果。第一次跑通的时候,我最大的感觉是“轻”。没有繁琐的中间件配置,没有在启动配置里堆一长串 Prompt,也没有各种神秘的环境变量。

2.3 Activity 的三种载荷:Message、Command、Component

想用好 MAF,必须理解 Activity 里三种最常见的载荷形态,否则写出来的 Agent 很容易变成“只能聊天的机器人”,而不是干活的工作流。

第一种是Message,也就是自然语言消息。这种载荷最常见,适合把所有不确定信息一股脑扔给大模型处理。第二种是Command,它常用于表达明确的意图。比方说“查询订单号 #12345” 这种指令,你用 Command 结构解析,就能绕开模型变成确定性逻辑。第三种是Component,它更多用于承载组件化调用结果,比如负责“商品详情”组件返回的数据、工具调用的结果。大体上,你可以这样理解:Message 是“模糊输入”,Command 是“明确指令”,Component 是“结构化数据”。

在设计智能体时,我建议你先问自己这个环节到底适合哪种载荷。如果用户随便说句话就能触发,用 Message;如果必须在满足条件时才做某个动作,用 Command 解析;如果只是把一个子任务的输出交给另一个子任务,用 Component 最干净。

3. 核心设计思路:为什么 Activity 和 Topology 是灵魂

3.1 从消息总线到智能体拓扑

接触 MAF 之后,我总忍不住拿它和我们做微服务时的消息总线做类比。一个 Agent 收到的 Activity,就像是服务总线里的消息;Agent 处理完后的输出,又是新的 Activity。既然我们做微服务时特别强调服务边界,Agent 设计也同理——每个 Agent 应该只做一件清晰的事,而且对外沟通只靠 Activity。

所谓“拓扑”(Topology),就是把 Agent 之间的关系明确画出来。MAF 的拓扑类一般是一个继承自AgentTopology的类,你在里面通过属性声明各 Agent 的依赖关系。举个简单的例子:

public class MyTopology : AgentTopology { public GreetingAgent Greeter { get; set; } = new(); public WeatherAgent WeatherWorker { get; set; } = new(); public override void Configure() { AddActivityHandler<GreetingAgent, MessageActivity>(Greeter); AddActivityHandler<GreetingAgent, CommandActivity>(Greeter); AddActivityHandler<WeatherAgent, WeatherRequest>(WeatherWorker); } }

初次看起来,这个拓扑类好像就是注册表,但它隐含了一个重要的设计原则:路由关系是静态的、可由架构审查的。相比让两个 Agent 互相注入,然后在运行时随意调用,这种声明式路由能让人一眼看清整个系统的数据流,也更方便做测试替身。

我实际做项目时发现一个挺舒服的点:拓扑类和依赖注入容器很契合。你可以把各个 Agent 需要的服务通过构造函数注入进去,然后在拓扑类的构造函数里也注入这些服务,最后用拓扑来构建整个工作流。如果团队里有人对智能体内部实现不熟悉,看拓扑配置就能理解七八成。

3.2 为什么这套设计适合人机协作和自动化

另一个我特别欣赏的点是,MAF 的 Activity 机制天然适合“人机协作”和“无人值守”两种模式的切换。人工介入时,你可以通过输入文本把用户指令包成 Message 发送过去;自动化流程中,你可以在代码里生成 Component 或 Command 来驱动下一步。

比如说,一个工单处理系统。如果 AI 无法判断工单归属,它可以返回一个特殊的Activity,要求人工给出答复。然后人工拿着结果再次把 Activity 注入流程,接着往下走。这种模式不像传统的“回调函数”那样把流程拆得七零八落,而是在同一个活动的流转里保留上下文。

在处理自动化流程时,这种强类型载荷还有一个隐藏价值:可追踪。你可以在中间件或日志中记录每一个 Activity 的类型、来源、去向和耗时。调 bug 时拉出一张活动时间线,几乎能还原整个流程。我后来排查问题就全靠这个时间线,比看混乱的文本日志强太多。

4. 实战拆解:多智能体协作处理一个业务请求

4.1 模拟场景定义

为了说清楚核心用法,咱们造一个足够贴日常的应用场景。假设我们有一个“商品咨询智能体系统”,包含两个子智能体和一个汇聚控制器:

  • OrderAgent:从用户消息中抽取订单号和商品,查询业务系统拿到物流状态和库存信息。
  • PolicyAgent:判断用户的诉求属于退货、换货还是开发票,并生成政策解释。
  • MediatorAgent:汇总前两个智能体输出,生成一段友好的最终回复。

这个场景选得好不好无所谓,关键是它涵盖了“任务分割、并行处理、结果合并”三段经典模式。

4.2 代码实现和关键参数解析

先定义两个子智能体。它们各自接收一个强类型 Component,内部调用模型或查询逻辑,最后把结果发出去。

public class OrderAgent : AgentBase { private readonly IOrderService _orders; public OrderAgent(IOrderService orders) => _orders = orders; protected override async Task OnActivityAsync(IContext context, CancellationToken ct) { if (context.Activity.TryGetComponentPayload(out OrderQuery query)) { var result = await _orders.GetOrderStatusAsync(query.OrderId, ct); context.Transmit(result.ToAgentActivity()); } } } public class PolicyAgent : AgentBase { protected override async Task OnActivityAsync(IContext context, CancellationToken ct) { if (context.Activity.TryGetComponentPayload(out UserIntention intention)) { var policyText = intention.Type switch { "refund" => "退货政策:未拆封且不影响二次销售...", "exchange" => "换货政策:非质量问题需7日内...", _ => "可为您转接人工处理..." }; context.Transmit(policyText.ToAgentActivity()); } } }

也许你会觉得PolicyAgent里用 switch 分支太简单。这只是为了说清楚模式。实战中你可以把这段逻辑改成调用真实的大模型,甚至让模型决定输出结构化的政策编号。框架不限制你用什么模型推理,它只帮忙解决信息的传递和路由。

然后是MediatorAgent。它需要同时监听两个子 Agent 的输出,并把结果拼接成最终文案。实现方式是在 Activity 处理中识别来源,或者用拓扑建立映射。

public class MediatorAgent : AgentBase { private readonly IResultFormatter _formatter; public MediatorAgent(IResultFormatter formatter) => _formatter = formatter; protected override async Task OnActivityAsync(IContext context, CancellationToken ct) { if (context.Activity.TryGetComponentPayload(out OrderInfo orderInfo)) { _partials.Order = orderInfo; } else if (context.Activity.TryGetComponentPayload(out PolicyInfo policyInfo)) { _partials.Policy = policyInfo; } if (_partials.IsComplete) { var finalReply = _formatter.Format(_partials); context.Transmit(finalReply.ToAgentActivity()); } } }

这里用了个累加器_partials,但在多实例部署下,这种内存状态会出问题。如果系统需要水平扩展,建议把状态放到分布式缓存或数据库里。上面这几段代码能跑通,但只是单机演示级别的写法。

4.3 拓扑配置和运行效果

拓扑定义如下:

public class CustomerServiceTopology : AgentTopology { public OrderAgent Order { get; set; } = new(); public PolicyAgent Policy { get; set; } = new(); public MediatorAgent Mediator { get; set; } = new(); public override void Configure() { AddActivityHandler<OrderAgent, OrderQuery>(Order); AddActivityHandler<PolicyAgent, UserIntention>(Policy); AddActivityHandler<MediatorAgent, OrderInfo>(Mediator); AddActivityHandler<MediatorAgent, PolicyInfo>(Mediator); } }

当我们注入一个主 Activity(比如用户说“我订单超时了想退款”)时,Runner 会根据负载类型把消息派给能处理它的 Agent。如果这条消息不是强类型 Query,而是自由文本,你可以在入口加一个 IntentExtractionAgent,让大模型负责从自然语言里抽出OrderQuery和UserIntention。这个阶段我不推荐后台任务直接接收原始文本当作命令处理,容易把对话模型和流程逻辑搅在一起。

整个流程跑起来以后,你会看到OrderAgent写出的订单信息和PolicyAgent写出的政策信息都被送到了MediatorAgent,最终被拼成一段自然语言回复。这种“小步骤、可测试、易替换”的风格,正好是普通开发者参与 AI 应用开发最舒适的姿势。

5. 从玩具到生产:调试、测试与常见问题

5.1 本地调试技巧:日志和中间件

MAF 的本地调试体验比我想象中成熟。第一条建议:开启框架内置的 Activity 跟踪日志。你会看到类似这样的日志输出:

[AAA] OrderAgent received Component(OrderQuery) [BBB] MediatorAgent received Component(OrderInfo)

这种结构化日志能让你瞬间知道消息是否路由错了、是否卡在某个 Agent 里。如果发现某个 Agent 一直收不到任何输入,多半是拓扑里的路由条件没匹配上,或者 Activity 的载荷类型和TryGetComponentPayload的泛型不一致。

第二条建议:写一个“观察中间件”。MAF 支持在 Agent 执行管线中插入中间件,你可以在中间件里记录每个 Activity 的耗时和成败。我把这个观察中间件比喻成“手术室的监控屏”,没它就只能瞎猜内部发生了什么。具体实现也不复杂,继承框架的中间件基类,重写执行前后的钩子方法即可。

5.2 高发坑位和小抄表格

下表列一下我踩过和预见别人很容易踩的典型问题:

症状常见原因解决思路
Agent 收不到消息拓扑里没注册路由;载荷泛型不一致检查 Topology.Configure;打印 Activity.PayloadType
回复消息没被模型总结直接把 Component 发出去,没有经过语言模型在入口 Agent 内调用模型,把结果作为 Message 发出
多 Agent 并发时状态错乱在 Agent 实例里放了可变状态改用无状态 Agent;状态存外部存储
依赖注入失效拓扑里用了new,而不是从容器获取让拓扑从构造函数注入服务,或通过服务提供者解析
日志太吵打开了全量 Activity 追踪按活动类型和 Agent 名过滤,只保留关键路由日志
模型超时导致卡死没有配置取消令牌和超时控制所有外部调用都传 CancellationToken,并设置超时时间

5.3 关于单元测试和测试替身

因为 MAF 把智能体封装成了一个可独立调用的类,所以你完全可以直接实例化一个 Agent,然后手动构造一个 Activity 来测试它的行为。关键是把模型调用抽象成可替换的接口,比如:

public interface IChatModel { Task<string> CompleteAsync(string prompt, CancellationToken ct); }

测试时注入一个 fake 实现,直接返回“模拟模型输出”,从而快速验证业务逻辑。我第一次给智能体写测试的时候,发现竟然没有比普通 Service 测试复杂多少,这种“去神秘化”的体验确实很能增强团队信心。

另外要注意,拓扑类本身也应该有测试。如果你的业务逻辑依赖“A Agent 的输出会进入 B Agent”,那么测试拓扑时就应该断言:给 A 发送一个 Activity,B 能被接收到预期载荷。手动模拟整个 Runner 毕竟繁琐,但 MAF 官方推荐还是直接把 Runner 组件建出来跑,因为 Runner 本身就是一个小型消息循环,跑起来很快。

6. 更进一步:把智能体接进真实业务系统

6.1 从本地 Runner 到宿主程序

本地 Runner 只是验证逻辑的起点。真正要部署时,你需要把 Agent 放在一个常驻进程中,可能是 ASP.NET Core 后台服务,也可能是 Worker Service。我把调试好的逻辑迁到后台服务时,几乎只改了一小段配置,核心代码没动。框架天然支持依赖注入和宿主生命周期,所以在常见“寄宿模型”下迁移很顺。

有一点需要特别留意:后台服务的并发模型。不要默认智能体里的代码是单线程跑的,如果托管环境会为多个对话创建多个执行方式,请做好并发控制,尤其在操作数据库或外部 API 时。我的做法是每次都把一个ConversationId放进 Activity 的元数据里,整个链路传递这个 ID,所有缓存和数据库操作都以它为维度隔离,这样并发对话之间就不会互相干扰了。

6.2 可观测与风险控制

把 AI 智能体真正投入生产,你的风险控制能力比模型能力更重要。这里说三个我坚持执行的原则:

第一,给所有外部模型调用加超时和重试。模型不是数据库,它可能因为峰值流量而响应缓慢,你需要一个针对“推理服务”定制的重试策略,而不是简单沿用普通 HTTP 的指数退避。

第二,对 Agent 的输出做“守卫校验”。如果MediatorAgent输出了一段明显偏离格式的结果,比如空字符串、缺失关键字段,务必在生成最终回复前拦截。可以用正则或 JSON Schema 验证,防的是模型偶尔“放飞自我”。

第三,保留每一次 Activity 的完整记录。AI 流程的问题排查非常依赖“当时模型到底看到了什么”。把进入每个 Agent 的 Prompt(或关键载荷)记录下来,一旦用户投诉或审计要求调取记录时,才能有据可查。

6.3 这个框架还能往哪扩展

做完上面的基础系统后,你完全可以继续往这几个方向扩展:一个是在拓扑中加入“并发分支”,让多个子 Agent 并行处理任务,MAF 支持在一个父 Activity 下派生出多个子 Activity,并在返回点聚合;另一个是给 Agent 加入工具调用能力,让模型在思考过程中决定调用哪些外部函数;还有一个是把可观测信息接入现有的监控系统,用结构化日志和 APM 追踪智能体全链路。

我个人目前正在做的扩展是,把OrderAgent和PolicyAgent变成能动态清点自身能力的“插件式智能体”。具体做法是,用拓扑上的元数据描述这个 Agent 能处理哪些载荷类型,然后在运行前生成能力清单,再由另一个“分配 Agent”根据用户请求自动挑选合适的 Agent 子集参与本轮任务。这比硬编码拓扑更灵活,但要注意不能让模型拥有修改系统拓扑的权限,安全边界还是要靠代码守住。

讲真,AI 智能体这个领域每天都有新概念,但落到工程上,依然是老一套的高质量原则:清晰边界、可测试、可观测、可控制。MAF 把这些原则用一种挺自然的语言融进了 .NET. 至少对我来说,现在可以说是“谁说.NET没有智能体?我们不但有,而且能按自己的方式跑得很好”了。最后顺手分享一个小经验:如果你想在团队里推广这套方案,别急着炫代码,先把 Activity 的概念和拓扑图摆在白板上,你会发现大家理解协作流程的速度比你预期快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 2:54:55

从后端开发到Agent开发:思维重构与工程实践

1. 从传统后端开发到Agent领域的思维转变1.1 我最初对Agent模型的误判说实话&#xff0c;刚开始接触Agent大模型这个方向时&#xff0c;我觉得它无非就是在原有接口调用外面包一层逻辑&#xff0c;把模型返回的结果解析一下&#xff0c;再拼接到业务流程里而已。毕竟做了快十年…

作者头像 李华
网站建设 2026/10/20 2:54:32

Subversion svn:ignore 三层机制与企业级忽略策略

1. 为什么“svn ignore”不是加个标记就完事——从一次误删生产配置说起刚接手一个老项目时&#xff0c;我遇到过最惊险的一次操作&#xff1a;在本地调试完新功能后执行svn commit -m "feat: add user profile module"&#xff0c;结果提交记录里赫然出现config/pro…

作者头像 李华
网站建设 2026/10/20 2:53:50

STM32与VL53L0X激光测距实战:从I2C驱动到精度调校

简介&#xff1a;一套基于STM32F10vet6与VL53L0X激光测距传感器的嵌入式开发资源&#xff0c;面向需要实现非接触式高精度测距的硬件工程师与嵌入式学习者。资源完整演示了I2C总线通信与引脚映射&#xff08;如PA3/PA2、XSHUT接PA5&#xff09;&#xff0c;包含驱动代码、工程配…

作者头像 李华
网站建设 2026/10/20 2:53:30

腾讯云轻量服务器搭建小程序物体识别后端完整指南

简介&#xff1a;一份围绕微信小程序物体识别开发的完整实践资料&#xff0c;面向需要了解前端小程序与云端服务器联通的开发者。内容覆盖小程序开发基础、腾讯云轻量云服务器部署、Anaconda环境搭建、Django后端接口服务以及YOLOv3深度学习模型的运用&#xff0c;帮助读者从零…

作者头像 李华
网站建设 2026/10/20 2:53:14

STM32F103内部Flash读写实战:从寄存器操作到断电备份设计

简介&#xff1a;面向STM32F103开发者的Flash读写示例工程&#xff0c;适合正在做固件升级、配置参数掉电保存或运行日志记录等功能的嵌入式开发者参考学习。示例基于标准外设库封装了完整的内部Flash操作流程&#xff0c;包括解锁Flash控制器、扇区擦除、按字或半字写入、回读…

作者头像 李华
网站建设 2026/10/20 2:53:09

Shell函数实战指南:从定义语法到模块化设计

说明项目标题 "Shell 编程之函数" 是一个典型的 Linux/Unix 脚本开发主题&#xff0c;面向有一定命令行基础、想提升脚本质量的开发者与运维人员。下面这篇内容按从业者视角展开&#xff0c;覆盖定义、参数作用域、嵌套递归、模块化设计、调试与实战&#xff0c;并附…

作者头像 李华