1. 项目概述:这不是一次简单的工具替换,而是一场底层思维的迁移
“.NET AI 生态系统的重构与演进:Microsoft Agent Framework 对 Semantic Kernel 的继承、超越与战略整合”——这个标题里藏着三个关键动作:“重构”、“演进”、“整合”。它不是在说“又出了个新库”,而是在宣告:.NET 平台上的 AI 开发范式,正在从“调用模型 API”的辅助阶段,正式迈入“构建自主智能体(Agent)系统”的工程化阶段。我接触过大量基于 Semantic Kernel(SK)的早期项目,从某高校实验室的课程 Demo 到某公司内部的知识助手原型,它们共同的特点是:逻辑清晰、结构规整,但一旦业务复杂度上升,比如要同时处理用户多轮模糊提问、调用多个异步外部服务、在失败时自动回退并重试、还要把整个决策链路可审计地记录下来——代码就会迅速膨胀成难以维护的“状态机迷宫”。而 Microsoft Agent Framework(MAF)的出现,恰恰就是为了解决这个痛点。它没有抛弃 SK 的核心价值,比如对 LLM 编排、函数调用(Function Calling)、记忆管理(Memory)等能力的抽象,而是把这些能力重新组织进一个更贴近真实世界任务流的模型里:Agent 不再是被动响应请求的函数集合,而是能主动规划(Plan)、执行(Act)、反思(Reflect)、协作(Collaborate)的软件实体。这背后是微软对 AI 应用开发本质的一次深刻重估:开发者真正需要的,不是更灵活的 Prompt 工具,而是更健壮的“AI 系统架构”。所以,如果你现在还在用 SK 写一个需要调用天气、日历、邮件三个插件来帮用户安排行程的 Demo,那 MAF 就是你下一步必须认真研究的演进路径;如果你正准备启动一个面向企业客户的智能客服中台项目,那么 MAF 提供的内置可观测性、生命周期管理、以及与 Azure AI Studio 的深度集成,就不是锦上添花,而是降低交付风险的刚需。它面向的不是单点功能的实现者,而是 AI 系统的架构师与交付负责人。
2. 核心设计思路拆解:从“编排引擎”到“智能体操作系统”
2.1 为什么必须重构?Semantic Kernel 的历史功绩与时代局限
要理解 MAF 的价值,得先看清 SK 的底色。Semantic Kernel 诞生于大模型爆发初期,它的核心使命是“让 .NET 开发者能快速上手 LLM”。因此,它的设计哲学是“最小可行抽象”:提供一个轻量级的 Kernel 对象,封装了 Prompt 渲染、插件(Plugin)加载、函数调用(Function Calling)的调度逻辑。这种设计极其成功,它让无数 .NET 工程师在一周内就能做出一个能调用本地函数的聊天机器人。但它的局限也根植于此——它本质上是一个增强版的 Prompt 编排器。我们来看一个典型 SK 项目的代码骨架:
var kernel = new KernelBuilder() .WithOpenAIChatCompletion("gpt-4", "your-key") .Build(); // 加载插件 kernel.ImportPluginFromType<WeatherPlugin>("weather"); kernel.ImportPluginFromType<CalendarPlugin>("calendar"); // 执行一个复合任务:先查天气,再根据天气建议日程 var result = await kernel.InvokeAsync( "You are a helpful assistant. Check today's weather in Beijing, then suggest one outdoor activity if it's sunny, or an indoor activity if it's rainy.", new KernelArguments { ["location"] = "Beijing" } );这段代码的问题不在于写法,而在于责任边界模糊。InvokeAsync这一行,承担了意图识别、工具选择、参数提取、调用顺序、错误处理、结果聚合等全部职责。当业务变复杂,比如要求“如果天气API超时,则降级使用缓存数据,并通知用户”,这个逻辑就得硬编码在 Prompt 里或 Kernel 的回调中,很快就会失控。SK 的 Plugin 模型也缺乏对“状态”的原生支持——一个需要记住用户偏好、历史对话上下文、当前任务进度的智能体,其状态散落在Kernel、Memory、ContextVariables等多个对象中,没有统一的生命周期管理。这就像给一辆自行车加装了涡轮增压,动力是有了,但底盘、悬挂、刹车系统都没升级,高速行驶时风险陡增。MAF 的重构,正是要打造一套完整的“智能体底盘”。
2.2 MAF 的核心范式:以 Agent 为中心的分层架构
MAF 的设计思想非常清晰:把“智能体”(Agent)本身,作为一等公民(First-Class Citizen)来建模和管理。它不再围绕“一次调用”展开,而是围绕“一个 Agent 的完整生命周期”展开。这个生命周期被明确划分为四个核心层级,每一层都解决一个特定维度的问题:
Agent 层(The Agent Itself):这是最顶层的抽象。一个
Agent不再是一个函数集合,而是一个拥有自己身份(ID)、角色(Role)、目标(Goal)、能力(Capabilities)和记忆(Memory)的独立实体。你可以创建ResearchAgent、WritingAgent、CodeReviewAgent,它们各自封装了领域知识和行为模式。Agent类型本身就是一个可序列化、可持久化、可监控的对象。Orchestration 层(编排层):这是 MAF 最具革命性的部分。它引入了
Orchestrator的概念,一个专门负责“如何让多个 Agent 协同工作”的调度器。Orchestrator不再是 SK 那种隐式的、基于 Prompt 的调度,而是显式的、可编程的、支持多种策略的控制器。它内置了三种标准策略:- Sequential: 严格按顺序执行 Agent 链。
- Parallel: 同时启动多个 Agent,等待全部完成。
- Router: 基于输入内容,动态路由到最合适的 Agent。 更重要的是,
Orchestrator是可扩展的。你可以轻松实现自己的CustomOrchestrator,比如一个基于强化学习的动态路由器,或者一个能根据 SLA(服务等级协议)自动选择模型供应商的智能调度器。这彻底解耦了“业务逻辑”与“调度逻辑”。
Runtime 层(运行时层):这是 MAF 的“操作系统内核”。它提供了 Agent 运行所必需的基础设施:
- State Management(状态管理): 每个 Agent 都有一个强类型的
State对象,其生命周期与 Agent 绑定。你无需再手动管理ContextVariables,所有中间状态、临时变量、任务进度都自然地存储在State中,并且可以被序列化到 Redis 或 Cosmos DB 中,实现跨请求、跨实例的状态恢复。 - Observability(可观测性): MAF 内置了完整的 OpenTelemetry 支持。每一次 Agent 的
Plan、Act、Think、Observe操作,都会自动生成结构化的 Trace 和 Log。你不需要额外埋点,就能在 Azure Monitor 或任何兼容的 APM 工具中,看到一个复杂任务是如何被分解、由哪个 Agent 执行、耗时多少、调用了哪些外部服务、是否发生了重试。这对生产环境的故障排查和性能优化是无价的。 - Lifecycle Management(生命周期管理): Agent 的创建、初始化、执行、暂停、恢复、销毁,都有明确的 Hook(钩子)方法。你可以在
OnInitialized中加载专属配置,在OnExecuting中进行权限校验,在OnCompleted中触发后续的 Webhook 通知。这使得 Agent 成为了一个真正可管理、可治理的软件组件。
- State Management(状态管理): 每个 Agent 都有一个强类型的
Foundation Layer(基础层):这一层是 MAF 与 SK 的“继承”所在。它复用了 SK 最成熟、最稳定的部分:
Kernel、PromptTemplate、FunctionCalling、Memory等核心能力。这意味着,你过去为 SK 编写的高质量插件(Plugin),几乎可以 100% 无缝迁移到 MAF 中,作为某个 Agent 的“能力”(Capability)被调用。MAF 并没有推翻重来,而是在 SK 的坚实地基上,盖起了一座更高、更稳固的智能体大厦。
2.3 战略整合:为何不是取代,而是升维?
将 MAF 定位为“对 SK 的取代”,是一种严重的误读。微软的战略意图非常明确:SK 是“AI 能力的乐高积木”,而 MAF 是“用这些积木搭建智能城市的蓝图与施工队”。它们的关系是互补与升维,而非替代。这种整合体现在三个层面:
技术栈整合:MAF 的 NuGet 包
Microsoft.Extensions.AI是一个统一的、面向未来的 AI 抽象层。它定义了IChatClient、IEmbeddingClient、IRerankerClient等接口,这些接口既被 SK 实现,也被 MAF 的底层 Runtime 所依赖。这意味着,无论你用 SK 写一个简单的文本摘要工具,还是用 MAF 构建一个复杂的多 Agent 客服系统,你所依赖的底层模型客户端、向量数据库客户端,都是同一套标准化的接口。这极大地降低了技术栈碎片化带来的维护成本。工具链整合:MAF 与 Azure AI Studio 的集成是深度的、原生的。在 AI Studio 的可视化画布中,你不仅可以像以前一样调试一个 Prompt,现在还能直接拖拽出一个
ResearchAgent、一个SummarizeAgent,并用连线定义它们之间的数据流和控制流。AI Studio 会自动生成对应的 MAF 代码框架,并为你预置好 Azure Monitor 的监控仪表盘。这种“所见即所得”的低代码体验,是 SK 时代无法想象的。它把 AI 系统的开发,从纯代码编写,提升到了“系统架构设计”的层面。生态位整合:微软清晰地划分了两者的适用场景。SK 依然是快速原型(Rapid Prototyping)、小型工具(Small Utilities)、以及需要极致轻量级控制的场景(如嵌入到一个已有 Web API 的中间件中)的首选。而 MAF 则是构建生产级、可扩展、可运维的 AI 应用(Production-Grade AI Applications)的官方推荐框架。这种“双轨并行”的策略,确保了 .NET AI 生态既能保持敏捷创新,又能支撑企业级的严肃交付。
3. 核心细节解析与实操要点:从概念到代码的关键跃迁
3.1 Agent 的定义:不只是一个类,而是一个有生命的组件
在 MAF 中,定义一个 Agent 远不止是写一个继承Agent的类。它是一个包含身份、目标、能力和状态的完整契约。我们以一个DocumentReviewerAgent为例,看其核心构成:
// 1. 定义强类型 State(这是 MAF 的核心创新) public class DocumentReviewerState { public string? DocumentId { get; set; } public string? ReviewerName { get; set; } public List<string> Findings { get; set; } = new(); public int CurrentPage { get; set; } = 1; public bool IsApproved { get; set; } = false; } // 2. 定义 Agent(继承自泛型 Agent<TState>) public class DocumentReviewerAgent : Agent<DocumentReviewerState> { // 构造函数注入依赖(遵循 .NET Core 最佳实践) public DocumentReviewerAgent( IChatClient chatClient, IDocumentService documentService, ILogger<DocumentReviewerAgent> logger) : base(chatClient, logger) { // 注册该 Agent 的“能力”(Capabilities) // 这些能力最终会被 Orchestrator 调用 RegisterCapability(new ReviewDocumentCapability(documentService)); RegisterCapability(new ApproveDocumentCapability()); RegisterCapability(new RequestChangesCapability()); } // 3. 定义 Agent 的核心行为:Plan(规划) // 这是 Agent 的“大脑”,决定接下来要做什么 protected override async Task<Plan> OnPlanAsync( Plan plan, CancellationToken cancellationToken = default) { // 基于当前 State 和输入,生成一个执行计划 var prompt = $""" You are a senior document reviewer. Current document: {State.DocumentId}, Page {State.CurrentPage}. Your goal is to review and approve this document. Based on the findings so far ({string.Join(", ", State.Findings)}), what is the next best action? Choose from: ReviewNextPage, Approve, RequestChanges. Output only the action name. """; var response = await ChatClient.GetChatMessageContentAsync( new ChatHistory(prompt), cancellationToken); var nextAction = response.Content.Trim(); switch (nextAction) { case "ReviewNextPage": State.CurrentPage++; break; case "Approve": State.IsApproved = true; break; } return plan; // 返回更新后的 Plan } // 4. 定义 Agent 的核心行为:Act(执行) // 这是 Agent 的“手脚”,调用具体能力 protected override async Task OnActAsync( Plan plan, CancellationToken cancellationToken = default) { // 根据 Plan 中的指令,调用注册的能力 foreach (var step in plan.Steps) { if (step.Name == "ReviewNextPage") { await GetCapability<ReviewDocumentCapability>() .ExecuteAsync(State.DocumentId, State.CurrentPage, cancellationToken); } } } }这段代码揭示了几个关键实操要点:
State 是第一公民:
DocumentReviewerState不是一个 DTO,而是一个承载着 Agent “灵魂”的对象。它的每一个属性,都代表了 Agent 在执行任务过程中的一个关键状态。State的变更会自动触发序列化,为后续的断点续传、状态快照、审计追踪打下基础。实操心得:我强烈建议为State类添加[JsonObject]特性,并为其所有属性提供默认值。这能极大避免在反序列化时因字段缺失导致的NullReferenceException,这是我在早期测试中踩过的一个深坑。Capability 是原子能力单元:
ReviewDocumentCapability是一个独立的、可测试的类,它只做一件事:调用文档服务获取某一页的内容并进行分析。它不关心上下文、不维护状态,这完美符合单一职责原则。实操心得:不要把业务逻辑塞进 Capability 里。Capability 应该是纯粹的“胶水代码”,负责与外部系统(API、DB、文件系统)交互。真正的业务规则判断,应该放在OnPlanAsync中。这样,你的 Capability 可以被多个不同的 Agent 复用,大大提升了代码的复用率。Plan/Act 分离是心智模型的转变:
OnPlanAsync负责“想”,OnActAsync负责“做”。这种分离强制开发者思考“决策”与“执行”的边界。它让代码逻辑更清晰,也更容易进行单元测试——你可以 MockChatClient来测试OnPlanAsync的决策逻辑,而无需真正调用 LLM。注意事项:OnPlanAsync的返回值Plan对象,其Steps属性是一个List<PlanStep>。每个PlanStep都有一个Name和Parameters。OnActAsync的职责,就是遍历这个列表,并根据Name去调用对应的能力。这是一个非常明确的契约,杜绝了 SK 时代那种“一个 InvokeAsync 调用里混杂了所有逻辑”的混乱。
3.2 Orchestrator 的实战:让多个 Agent 协同工作的指挥官
定义好单个 Agent 后,真正的挑战在于如何让它们协同。MAF 的Orchestrator就是这个指挥官。下面是一个典型的CustomerSupportOrchestrator的实现,它协调IntentClassifierAgent、KnowledgeBaseAgent和ResponseGeneratorAgent三个 Agent:
public class CustomerSupportOrchestrator : Orchestrator { private readonly IntentClassifierAgent _intentAgent; private readonly KnowledgeBaseAgent _kbAgent; private readonly ResponseGeneratorAgent _responseAgent; public CustomerSupportOrchestrator( IntentClassifierAgent intentAgent, KnowledgeBaseAgent kbAgent, ResponseGeneratorAgent responseAgent) { _intentAgent = intentAgent; _kbAgent = kbAgent; _responseAgent = responseAgent; } // 重写 ExecuteAsync,定义协同流程 public override async Task<OrchestrationResult> ExecuteAsync( string input, CancellationToken cancellationToken = default) { // Step 1: 让 IntentClassifierAgent 分析用户意图 var intentResult = await _intentAgent.InvokeAsync(input, cancellationToken); var intent = intentResult.Output.ToString(); // e.g., "BillingIssue", "FeatureRequest" // Step 2: 根据意图,动态选择后续 Agent string knowledgeQuery; switch (intent) { case "BillingIssue": knowledgeQuery = $"How to dispute a charge on invoice {ExtractInvoiceNumber(input)}?"; break; case "FeatureRequest": knowledgeQuery = $"What is the roadmap for {ExtractFeatureName(input)}?"; break; default: knowledgeQuery = input; break; } // Step 3: 并行查询知识库和生成初步回复 var kbTask = _kbAgent.InvokeAsync(knowledgeQuery, cancellationToken); var responseTask = _responseAgent.InvokeAsync(input, cancellationToken); await Task.WhenAll(kbTask, responseTask); // Step 4: 聚合结果,生成最终响应 var finalResponse = await _responseAgent.InvokeAsync( $"User query: {input}. Knowledge context: {kbTask.Result.Output}. " + $"Preliminary response: {responseTask.Result.Output}. " + $"Generate a final, professional, and empathetic response.", cancellationToken); return new OrchestrationResult(finalResponse.Output.ToString()); } }这个例子展示了 MAF Orchestrator 的强大之处:
显式流程控制:整个客服流程被清晰地拆解为
Analyze -> Route -> Query & Generate -> Synthesize四个步骤。每一步的输入、输出、依赖关系都一目了然。这比在 SK 中用一个巨大的 Prompt 来描述整个流程,要可靠、可维护得多。动态路由能力:
switch (intent)语句实现了基于业务逻辑的动态路由。这不再是 Prompt 工程的“黑盒猜测”,而是确定性的、可测试的代码逻辑。实操心得:我建议将ExtractInvoiceNumber和ExtractFeatureName这样的解析逻辑,封装成独立的UtilityService,并在 Orchestrator 的构造函数中注入。这样,Orchestrator 本身保持纯净,只关注流程编排,而具体的业务规则则交由专门的服务去处理。并行与聚合:
Task.WhenAll的使用,体现了 MAF 对现代异步编程的原生支持。它允许你充分利用硬件资源,让知识库查询和初步回复生成这两个 IO 密集型操作同时进行,显著缩短了端到端的响应延迟。注意事项:在await Task.WhenAll之后,务必检查每个 Task 的IsFaulted属性。如果kbTask失败了,你不应该直接将kbTask.Result传递给ResponseGeneratorAgent,而应该捕获异常,并生成一个友好的降级响应,比如“抱歉,我暂时无法访问知识库,请稍后再试”。MAF 的OrchestrationResult类型本身就包含了IsSuccess和Error属性,你应该善用它们。
3.3 Runtime 的配置:让 Agent 在生产环境中稳健运行
MAF 的 Runtime 层是其“超越”SK 的关键。要让它在生产环境中发挥威力,正确的配置至关重要。以下是一个典型的Program.cs配置片段:
var builder = WebApplication.CreateBuilder(args); // 1. 注册 MAF 的核心服务 builder.Services .AddAgents() // 注册所有 Agent 和 Orchestrator .AddChatClient("openai", configure => configure .WithModelId("gpt-4-turbo") .WithApiKey(builder.Configuration["OpenAI:ApiKey"])) .AddMemoryStore("redis", configure => configure .WithConnectionString(builder.Configuration["Redis:ConnectionString"])) .AddObservability(); // 启用 OpenTelemetry // 2. 配置 Agent 的全局行为 builder.Services.Configure<AgentOptions>(options => { options.DefaultTimeout = TimeSpan.FromSeconds(30); // 全局超时 options.MaxRetries = 3; // 全局最大重试次数 options.RetryDelay = TimeSpan.FromMilliseconds(500); // 重试间隔 }); // 3. 配置可观测性 builder.Services.AddOpenTelemetry() .WithTracing(tracerProviderBuilder => { tracerProviderBuilder .AddSource("Microsoft.Extensions.AI") // MAF 的 trace source .AddAzureMonitorTraceExporter(); // 发送到 Azure Monitor }); var app = builder.Build(); // 4. 添加 MAF 的中间件(可选,用于 HTTP 端点) app.UseAgents(); // 这会自动为所有注册的 Agent 创建 /agents/{name} 的 REST API app.MapGet("/health", () => Results.Ok()); app.Run();这段配置代码蕴含了丰富的实操经验:
AddAgents()是魔法入口:这个扩展方法会自动扫描程序集中所有继承自Agent<TState>和Orchestrator的类型,并将它们注册为 Scoped 服务。这意味着,每次 HTTP 请求进来,你都能获得一个全新的、隔离的 Agent 实例,其State也是全新的。这天然解决了并发安全问题,是 MAF 设计的精妙之处。实操心得:我习惯在AddAgents()之后,紧接着调用builder.Services.AddHostedService<AgentWarmupService>()。这个AgentWarmupService是一个后台服务,它会在应用启动时,主动调用每个 Agent 的InitializeAsync方法,预热模型、加载缓存、建立连接。这能有效避免第一个用户请求时遭遇漫长的冷启动延迟。内存存储(Memory Store)的选择:MAF 支持多种内存后端,包括 In-Memory(仅用于开发)、Redis(推荐用于生产)、Cosmos DB(用于需要强一致性和全球分布的场景)。选择 Redis 不仅因为它速度快,更因为它支持 Pub/Sub 模式。你可以利用这一点,实现 Agent 之间的事件驱动通信。例如,当
DocumentReviewerAgent完成审批后,它可以发布一个DocumentApprovedEvent,由另一个NotificationAgent订阅并发送邮件。注意事项:在生产环境中,切勿使用AddInMemoryMemoryStore()。它会导致每个 Agent 实例都拥有自己独立的内存,无法实现跨实例的状态共享,这对于需要水平扩展的微服务架构是致命的。可观测性是生产环境的生命线:
AddObservability()和AddOpenTelemetry()的组合,是 MAF 的“眼睛和耳朵”。它会自动为每一个 Agent 的OnPlanAsync、OnActAsync、OnThinkAsync方法生成 Trace Span。你可以在 Azure Monitor 的 Application Map 中,直观地看到一个用户请求是如何被分解为多个 Agent 的调用,以及每个调用的耗时、成功率、错误堆栈。实操心得:我强烈建议在OrchestrationResult的构造函数中,手动添加一个Attributes字典,将关键的业务指标(如document_id,customer_tier,sla_met)注入到 Trace 中。这样,你就可以在 Azure Monitor 中,直接按这些业务维度进行筛选和聚合分析,而不是只能看到一堆技术指标。
4. 实操过程与核心环节实现:一个端到端的“智能会议纪要”项目
4.1 项目背景与需求拆解
我们来落地一个真实的、有代表性的项目:智能会议纪要生成系统。传统方案是,会议结束后,由助理手动整理录音、提炼要点、撰写纪要。我们的目标是:在会议进行中,实时生成一份结构化的、可编辑的会议纪要草稿,并在会后自动分发给与会者。
核心需求拆解如下:
- 实时性:需要处理流式音频转录(ASR)的文本流,而非一次性处理完整文本。
- 结构化:纪要需包含“会议基本信息”、“讨论要点”、“待办事项(Action Items)”、“决策项(Decisions)”四个核心模块。
- 可编辑性:生成的纪要应是 Markdown 格式,方便与会者在会后直接修改。
- 分发自动化:会后,系统需自动将最终版纪要通过邮件发送给所有与会者。
4.2 系统架构与 Agent 角色划分
基于 MAF,我们设计了一个四层架构:
Input Layer(输入层):一个 ASP.NET Core Minimal API,接收来自前端的 WebSocket 连接,持续接收 ASR 引擎推送的文本片段(
{"text": "Hello, everyone..."})。Orchestration Layer(编排层):
MeetingSummaryOrchestrator,作为总指挥,负责接收文本流、分发给下游 Agent、聚合结果、管理会话状态。Agent Layer(智能体层):
TranscriptAggregatorAgent: 负责将零散的文本片段聚合成连贯的会议发言记录,并识别发言人。StructureExtractorAgent: 负责从聚合后的发言记录中,抽取出“要点”、“待办事项”、“决策项”。MarkdownFormatterAgent: 负责将抽取的结构化数据,渲染成美观的 Markdown 纪要。
Output Layer(输出层):一个
EmailService,在Orchestrator完成最终纪要生成后,被调用以发送邮件。
4.3 核心代码实现:从流式输入到结构化输出
步骤 1:定义会议状态(State)
public class MeetingSummaryState { public string MeetingId { get; set; } = Guid.NewGuid().ToString(); public string Title { get; set; } = "Untitled Meeting"; public DateTime StartTime { get; set; } = DateTime.UtcNow; public List<string> Attendees { get; set; } = new(); // 流式聚合的原始发言记录 public List<TranscriptSegment> TranscriptSegments { get; set; } = new(); // 结构化抽取的结果 public List<string> KeyPoints { get; set; } = new(); public List<ActionItem> ActionItems { get; set; } = new(); public List<string> Decisions { get; set; } = new(); // 最终生成的 Markdown public string FinalMarkdown { get; set; } = string.Empty; } public class TranscriptSegment { public string Speaker { get; set; } = "Unknown"; public string Text { get; set; } = string.Empty; public DateTime Timestamp { get; set; } = DateTime.UtcNow; } public class ActionItem { public string Description { get; set; } = string.Empty; public string Owner { get; set; } = string.Empty; public DateTime DueDate { get; set; } = DateTime.UtcNow.AddDays(7); }步骤 2:实现TranscriptAggregatorAgent
public class TranscriptAggregatorAgent : Agent<MeetingSummaryState> { private readonly ISpeakerDiarizationService _diarizationService; public TranscriptAggregatorAgent( IChatClient chatClient, ISpeakerDiarizationService diarizationService, ILogger<TranscriptAggregatorAgent> logger) : base(chatClient, logger) { _diarizationService = diarizationService; } protected override async Task<Plan> OnPlanAsync( Plan plan, CancellationToken cancellationToken = default) { // 如果是第一次调用,初始化会议基本信息 if (!State.TranscriptSegments.Any()) { State.Title = "New Meeting"; State.Attendees = new List<string> { "You", "John", "Sarah" }; } // 基于最新的文本片段,决定是否需要进行发言者识别 var latestText = plan.Input; if (latestText.Length > 50 && !State.TranscriptSegments.Any(s => s.Text.Contains(latestText))) { // 调用外部服务进行说话人分离 var speakers = await _diarizationService.IdentifySpeakersAsync( latestText, cancellationToken); State.TranscriptSegments.Add(new TranscriptSegment { Speaker = speakers.FirstOrDefault() ?? "Unknown", Text = latestText, Timestamp = DateTime.UtcNow }); } return plan; } }步骤 3:实现MeetingSummaryOrchestrator
public class MeetingSummaryOrchestrator : Orchestrator { private readonly TranscriptAggregatorAgent _aggregator; private readonly StructureExtractorAgent _extractor; private readonly MarkdownFormatterAgent _formatter; private readonly EmailService _emailService; public MeetingSummaryOrchestrator( TranscriptAggregatorAgent aggregator, StructureExtractorAgent extractor, MarkdownFormatterAgent formatter, EmailService emailService) { _aggregator = aggregator; _extractor = extractor; _formatter = formatter; _emailService = emailService; } public override async Task<OrchestrationResult> ExecuteAsync( string input, CancellationToken cancellationToken = default) { // Step 1: 聚合文本流 await _aggregator.InvokeAsync(input, cancellationToken); // Step 2: 当累积到一定长度(例如 200 字),触发结构化抽取 if (_aggregator.State.TranscriptSegments.Count >= 5) { var fullTranscript = string.Join("\n", _aggregator.State.TranscriptSegments.Select(s => $"{s.Speaker}: {s.Text}")); var extractionResult = await _extractor.InvokeAsync(fullTranscript, cancellationToken); // 更新 State _aggregator.State.KeyPoints = extractionResult.KeyPoints; _aggregator.State.ActionItems = extractionResult.ActionItems; _aggregator.State.Decisions = extractionResult.Decisions; } // Step 3: 生成 Markdown var markdownResult = await _formatter.InvokeAsync( JsonSerializer.Serialize(_aggregator.State), cancellationToken); _aggregator.State.FinalMarkdown = markdownResult.Output.ToString(); // Step 4: 如果会议结束(由外部信号触发),发送邮件 if (input.Contains("[END_OF_MEETING]")) { await _emailService.SendSummaryEmailAsync( _aggregator.State.MeetingId, _aggregator.State.Attendees, _aggregator.State.FinalMarkdown, cancellationToken); } return new OrchestrationResult(_aggregator.State.FinalMarkdown); } }步骤 4:WebSocket API 的实现
app.Map("/ws/meeting", async (HttpContext context) => { if (!context.WebSockets.IsWebSocketRequest) { context.Response.StatusCode = StatusCodes.Status400BadRequest; return; } using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); var orchestrator = context.RequestServices.GetRequiredService<MeetingSummaryOrchestrator>(); try { var buffer = new byte[1024]; while (webSocket.State == WebSocketState.Open) { var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); if (result.MessageType == WebSocketMessageType.Text) { var text = Encoding.UTF8.GetString(buffer, 0, result.Count); // 将接收到的文本交给 Orchestrator 处理 var orchestrationResult = await orchestrator.ExecuteAsync(text); // 将生成的 Markdown 实时推送给前端 var responseBytes = Encoding.UTF8.GetBytes(orchestrationResult.Output.ToString()); await webSocket.SendAsync( new ArraySegment<byte>(responseBytes), WebSocketMessageType.Text, true, CancellationToken.None); } } } catch (Exception ex) { // 记录错误 context.RequestServices.GetRequiredService<ILogger<Program>>() .LogError(ex, "WebSocket error"); } });这个端到端的实现,完美展现了 MAF 的核心优势:流式处理、状态持久、模块解耦、可观测性强。每一个 Agent 都职责单一,可以独立开发、测试和部署。Orchestrator则像一个精密的瑞士钟表,将所有齿轮(Agent)严丝合缝地咬合在一起,共同驱动整个系统运转。
5. 常见问题与排查技巧实录:来自生产环境的第一手经验
5.1 问题速查表:高频故障与解决方案
| 问题现象 | 可能原因 | 排查与解决技巧 |
|---|---|---|
Agent 执行超时,抛出OperationCanceledException | 1. LLM 响应慢;2.AgentOptions.DefaultTimeout设置过短;3.Orchestrator中的Task.WhenAll某个子任务卡死。 | 排查:首先查看 Azure Monitor 的 Trace,定位是哪个 Agent 的OnPlanAsync或OnActAsync耗时最长。解决:1. 为关键 Agent 单独设置更长的超时agent.WithOptions(new AgentOptions { DefaultTimeout = TimeSpan.FromMinutes(2) });2. 在OnActAsync中,为每个Capability的调用都加上CancellationToken,并确保外部服务(如 HTTP Client)也尊重该 Token;3. 使用Task.WhenAny替代Task.WhenAll,对慢任务进行降级处理。 |
| State 状态丢失,每次调用 Agent 都是全新状态 | 1. 错误地使用了AddSingleton注册 Agent;2.Orchestrator没有正确地将State传递给下游 Agent;3.State类未标记[JsonObject],反序列化失败。 | 排查:在OnPlanAsync开头打日志Logger.LogInformation("Current State: {@State}", State),观察State的MeetingId是否每次都变化。解决:1.绝对禁止AddSingleton<YourAgent>,必须使用AddScoped;2. 确保Orchestrator在调用agent.InvokeAsync(...)时,传入的是同一个State的引用;3. 为State类添加[JsonObject(MemberSerialization.OptIn)],并为所有需要序列化的属性添加[JsonProperty]。 |
Orchestrator 的 Trace 在 Azure Monitor 中看不到Plan和Act的详细 Span | 1.AddObservability()未被调用;2.IChatClient的实现未正确集成 OpenTelemetry;3 |