1. 项目概述:这不是一次简单的工具替换,而是一场底层思维的迁移
“.NET AI 生态系统的重构与演进:Microsoft Agent Framework 对 Semantic Kernel 的继承、超越与战略整合”——这个标题里藏着三个关键动作:“重构”、“演进”、“整合”。它不是在说“又出了个新库”,而是在宣告:.NET 平台上的 AI 开发范式,正在从“调用模型的胶水层”,转向“构建智能体的原生基础设施”。我接触过大量基于 Semantic Kernel(SK)的早期项目,从某高校实验室的学术Demo,到某公司内部的客服知识引擎,再到某跨平台系统的自动化工作流。它们共同的特点是:开发者需要自己拼装记忆、规划、工具调用、提示工程这些模块,像搭乐高一样反复调试提示词模板、手写函数调用解析逻辑、为不同大模型适配不同的输出格式。这种模式在2023年很流行,但到了2024年中,团队普遍反馈“维护成本陡增”、“业务逻辑和AI胶水代码缠绕不清”、“想加个新工具就得改三处核心逻辑”。Microsoft Agent Framework(MAF)的出现,正是对这种痛点的系统性回应。它把“智能体”(Agent)本身,变成了一个可声明、可组合、可生命周期管理的一等公民。你不再写“如何让模型调用API”,而是定义“这个Agent需要哪些能力、能访问哪些数据、遵循什么安全策略”。这背后是.NET生态对AI开发认知的升级:从“模型即服务”(Model-as-a-Service)走向“智能体即平台”(Agent-as-a-Platform)。对于.NET开发者而言,这意味着你不需要立刻抛弃SK,但必须理解MAF的设计哲学——它不是SK的2.0,而是站在更高维度,用更现代的架构语言,重新定义了“在.NET里做AI”的边界。如果你还在用SK手写Orchestrator类来协调多个LLM调用,或者为每个新工具都得重写一遍FunctionCallHandler,那么MAF的抽象层级,就是你接下来半年最值得投入的技术债偿还点。
2. 核心设计思路拆解:为什么是“重构”而非“升级”?
2.1 从“函数编排”到“能力契约”的范式跃迁
Semantic Kernel 的核心抽象是Kernel和Function。Kernel 是一个容器,Function 是一个可注册的、带描述的、能被LLM调用的C#方法。整个流程是:LLM输出一个JSON格式的函数调用请求 → SK解析这个JSON → 查找并执行对应的Function → 将结果塞回上下文 → 再次调用LLM。这个过程看似清晰,但实操中暴露了三个结构性瓶颈:
上下文污染严重:每次Function执行的结果,都以纯文本形式拼接到Prompt里。当一个Agent需要调用5个工具、每个返回几百字的JSON时,Prompt长度会指数级膨胀,不仅消耗Token,更导致LLM注意力分散,关键信息被淹没。我曾调试过一个金融分析Agent,它需要依次调用“获取实时股价”、“查询财报摘要”、“分析新闻情绪”、“生成投资建议”四个Function。第四步的输出质量极不稳定,后来发现90%的失败案例,都是因为第三步返回的新闻情绪分析结果(含大量HTML标签和冗余字段)把Prompt撑爆了,LLM根本没看清前两步的关键数据。
错误处理是黑洞:SK的Function执行异常,通常只抛出一个通用Exception。上层Orchestrator捕获后,只能返回一个模糊的“调用失败,请重试”。但真实场景中,“网络超时”、“权限不足”、“参数校验失败”需要完全不同的用户提示和降级策略。SK没有为错误类型提供结构化契约,开发者只能靠字符串匹配错误消息,脆弱且不可维护。
状态管理靠“手工传参”:SK本身不提供跨Function调用的状态存储。如果一个Function需要把“用户ID”传递给后续所有步骤,你得在每个Function签名里硬加上
string userId参数,并在Orchestrator里手动传递。这违背了面向对象的封装原则,也极易出错。
Microsoft Agent Framework 则从根本上重构了这个链条。它的核心抽象是Agent和Capability。Agent是一个有明确身份、目标、记忆和行为规则的实体;Capability则是一个严格定义的、带输入/输出Schema、带错误分类、带执行上下文的“能力单元”。看一个对比示例:
// Semantic Kernel 中典型的 Function 定义(简化) [KernelFunction] public async Task<string> GetStockPriceAsync([FromKernelArguments] string symbol) { // 网络调用... return $"{{\"symbol\":\"{symbol}\",\"price\":152.34}}"; } // Microsoft Agent Framework 中对应的 Capability 定义 public class StockPriceCapability : ICapability { public CapabilityMetadata Metadata => new() { Name = "get_stock_price", Description = "Retrieve the current market price for a given stock symbol.", InputSchema = new JsonSchema { Properties = { ["symbol"] = new JsonSchema { Type = "string" } } }, OutputSchema = new JsonSchema { Properties = { ["symbol"] = new JsonSchema { Type = "string" }, ["price"] = new JsonSchema { Type = "number" } } }, ErrorCategories = new[] { "network_timeout", "invalid_symbol", "rate_limit_exceeded" } }; public async Task<CapabilityResult> ExecuteAsync(CapabilityContext context, JsonNode input) { var symbol = input["symbol"].ToString(); try { var price = await _stockService.GetPriceAsync(symbol); return CapabilityResult.Success(new JsonNode { ["symbol"] = symbol, ["price"] = price }); } catch (TimeoutException) { return CapabilityResult.Failure("network_timeout", "Service unavailable"); } catch (ArgumentException) { return CapabilityResult.Failure("invalid_symbol", "Symbol not found"); } } }这个差异是本质性的。MAF的Capability强制要求你声明“我能接受什么输入”、“我能输出什么结构”、“我可能因何失败”。这使得Agent Runtime可以在执行前做静态校验,在执行后做结构化解析,在失败时做精准路由。它把原本散落在各处的、隐式的约定,变成了显式的、可验证的契约。这不再是“胶水”,而是“管道”。
2.2 “记忆”的分层治理:从全局缓存到场景化上下文
在SK中,“记忆”(Memory)是一个相对笼统的概念,主要通过ISemanticTextMemory接口实现,底层通常是向量数据库。开发者习惯性地把所有东西都塞进去:用户对话历史、工具返回结果、系统提示词片段……这导致两个问题:一是检索噪音大,二是隐私风险高(比如把用户的敏感操作日志和公开知识库混存在同一个向量空间)。
MAF将“记忆”彻底解耦为三个正交的层次:
Conversation Memory:仅存储当前会话的用户与Agent的交互记录。它是短暂的、会话级别的,由Runtime自动管理生命周期。你无需手动存取,Agent在生成回复时会自动引用它。
Knowledge Memory:存储长期、稳定的领域知识,如产品文档、FAQ、API手册。它与Conversation Memory物理隔离,支持独立的索引、更新和权限控制。一个Agent可以同时连接多个Knowledge Memory源(例如,一个指向内部Confluence,一个指向公开的GitHub Wiki)。
Execution Memory:这是MAF最具创新性的设计。它专为单次Agent执行周期服务,存储的是本次规划(Planning)、工具调用(Tool Calling)、结果聚合(Aggregation)过程中产生的中间状态。比如,当Agent规划出“先查股价,再查财报”时,Execution Memory会记录这个Plan;当股价查询成功后,结果会以结构化JSON存入Execution Memory;后续的财报查询Capability,可以直接从Execution Memory中读取这个股价,而无需再次解析Prompt或调用外部服务。它本质上是一个轻量级、内存驻留的、Schema化的临时数据库。
这种分层,让开发者能精确控制数据的流向和作用域。你再也不用担心“用户问‘昨天的股价’,结果Agent把上周的财报摘要也当成了上下文”。我在一个医疗咨询Agent项目中,就利用Execution Memory实现了“诊断路径追踪”:每一步检查(血压、血糖、病史)的结果都存入Execution Memory,最终的诊断建议Capability会读取所有已确认的检查项,动态生成报告。这比在SK里用一堆Dictionary<string, object>手工传参,清晰、安全、可测试得多。
2.3 战略整合:.NET生态的“统一入口”野心
标题中的“战略整合”,指的不是MAF要取代SK,而是微软在.NET AI栈中,为不同成熟度的项目提供了清晰的演进路径。MAF并非凭空诞生,它的很多核心组件,如IPlanner(规划器)、IToolExecutor(工具执行器)、IMemoryStore(记忆存储),其接口设计和SPI(Service Provider Interface)规范,都直接复用了SK v1.0.0-beta系列中已经稳定下来的抽象。这意味着:
平滑迁移:一个成熟的SK项目,不需要推倒重来。你可以逐步将原有的
KernelFunction重构成ICapability,将自定义的Orchestrator替换为MAF内置的ReActPlanner,将ISemanticTextMemory实例注入到MAF的KnowledgeMemory中。整个过程是渐进式的,风险可控。能力复用:SK社区积累的大量插件(Plugin),如
WebSearchPlugin、EmailPlugin、DatabasePlugin,其核心逻辑几乎无需修改,只需按MAF的ICapability接口进行一层薄薄的包装,就能成为MAF生态的一部分。这避免了生态碎片化,保护了开发者的历史投入。统一工具链:MAF与Visual Studio 2022 v17.10+深度集成。当你创建一个MAF项目时,VS会自动为你生成符合OpenAPI 3.1规范的Agent能力描述文件(
agent.yaml),并提供可视化的能力编排设计器。这个设计器,未来会与Azure AI Studio的Agent部署流水线打通。而SK的调试,则更多依赖于Console日志和手动断点。这种IDE层面的整合,标志着微软将Agent开发,正式纳入了.NET主流开发者的日常工具链。
所以,“战略整合”的深意在于:微软没有选择“另起炉灶”,而是以MAF为“新旗舰”,以SK为“成熟基石”,构建了一个“向下兼容、向上演进”的双轨制AI开发体系。对于企业架构师来说,这意味着你可以放心地在现有SK项目上投入,因为它不会被废弃;对于新项目启动者来说,MAF提供了开箱即用的、生产就绪的Agent框架,省去了从零造轮子的巨大成本。
3. 核心细节与实操要点:从概念到代码的落地鸿沟
3.1 Agent的声明式定义:YAML不是配置,而是契约
在MAF中,定义一个Agent,第一步不是写C#代码,而是编写一个agent.yaml文件。这看起来像配置,实则是整个Agent的“宪法”。它强制你思考:这个Agent的使命是什么?它能做什么?它不能做什么?谁可以使用它?下面是一个电商客服Agent的agent.yaml示例:
# agent.yaml name: "EcommerceSupportAgent" description: "An AI agent that assists customers with order status, returns, and product information." version: "1.0.0" # 这是Agent的“宪法性”目标,LLM会据此进行自我约束和反思 goal: | You are a helpful, empathetic, and accurate customer support agent for an e-commerce platform. Your primary goal is to resolve customer inquiries related to orders, returns, and products. If you cannot answer a question definitively, say so and offer to connect them with a human agent. # 能力(Capabilities)是Agent的“权利清单” capabilities: - name: "get_order_status" description: "Retrieve the current status and estimated delivery date for a given order ID." # 引用一个已注册的Capability实现 implementation: "EcommercePlugin.OrderStatusCapability" # 定义该能力所需的最小权限 permissions: - "read:orders" - name: "initiate_return" description: "Start the return process for an eligible order." implementation: "EcommercePlugin.ReturnInitiationCapability" permissions: - "write:returns" - "read:orders" # 记忆(Memories)是Agent的“知识库”和“工作台” memories: # Conversation Memory 是自动启用的,无需声明 knowledge: - name: "product_catalog" type: "vector" # 指向一个已配置好的VectorDB连接字符串 connectionString: "${VECTOR_DB_CONNECTION_STRING}" # 可以指定特定的Collection或Index collectionName: "products" - name: "support_faq" type: "text" # 指向一个本地Markdown文件,作为静态知识源 source: "./data/support_faq.md" # Execution Memory 是自动启用的,无需声明 # 安全策略(Security Policies)是Agent的“行为守则” security: # 输入过滤:防止恶意指令注入 inputFiltering: enabled: true # 使用预置的“电商安全规则集” ruleset: "ecommerce-safe" # 输出审查:确保不泄露敏感信息 outputReview: enabled: true # 检查是否包含信用卡号、身份证号等PII piiDetection: true这个YAML文件的价值,远超一个配置。它是一个可读、可审、可版本化、可自动化测试的契约。DevOps团队可以将它纳入CI/CD流水线,用静态分析工具检查:
- 所有声明的Capability是否都在代码中实现了?
- 所有
permissions是否都对应着后端API的RBAC策略? inputFiltering.ruleset是否在安全合规库中存在且已批准?
我在某公司的落地实践中,就将agent.yaml的校验作为PR合并的强制门禁。任何新增Capability,都必须同步更新YAML,并通过自动化测试。这极大地提升了Agent上线的安全性和可审计性。而SK项目中,类似的规则往往散落在各个KernelFunction的注释里,或者压根就没有,全靠Code Review人工把关,漏检率很高。
3.2 Capability的实现:结构化输入/输出是健壮性的基石
前面提到,MAF强制Capability声明InputSchema和OutputSchema。这不仅仅是“好看”,而是为了实现真正的结构化数据流。我们来看一个完整的、生产就绪的Capability实现:
public class WeatherCapability : ICapability { private readonly IWeatherService _weatherService; public WeatherCapability(IWeatherService weatherService) { _weatherService = weatherService; } public CapabilityMetadata Metadata => new() { Name = "get_weather", Description = "Get the current weather conditions and forecast for a location.", // 输入Schema:严格定义期望的JSON结构 InputSchema = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["location"] = new JsonSchema { Type = "string", Description = "The city or ZIP code." }, ["units"] = new JsonSchema { Type = "string", Enum = new[] { "celsius", "fahrenheit" }, Default = "celsius" } }, Required = new[] { "location" } }, // 输出Schema:严格定义承诺返回的JSON结构 OutputSchema = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["location"] = new JsonSchema { Type = "string" }, ["temperature"] = new JsonSchema { Type = "number" }, ["condition"] = new JsonSchema { Type = "string" }, ["forecast"] = new JsonSchema { Type = "array", Items = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["day"] = new JsonSchema { Type = "string" }, ["high"] = new JsonSchema { Type = "number" }, ["low"] = new JsonSchema { Type = "number" } } } } } }, ErrorCategories = new[] { "invalid_location", "service_unavailable", "rate_limit_exceeded" } }; public async Task<CapabilityResult> ExecuteAsync(CapabilityContext context, JsonNode input) { try { // 1. Schema验证:MAF Runtime会在调用ExecuteAsync前,用JsonSchema验证input // 如果验证失败,会直接返回Failure,不会进入此方法 var location = input["location"].ToString(); var units = input["units"]?.ToString() ?? "celsius"; // 2. 业务逻辑 var weatherData = await _weatherService.GetCurrentWeatherAsync(location, units); // 3. 构建结构化输出:必须严格符合OutputSchema var output = new JsonObject { ["location"] = location, ["temperature"] = weatherData.Temperature, ["condition"] = weatherData.Condition, ["forecast"] = new JsonArray(weatherData.Forecast.Select(f => new JsonObject { ["day"] = f.Day, ["high"] = f.High, ["low"] = f.Low })) }; return CapabilityResult.Success(output); } catch (LocationNotFoundException) { return CapabilityResult.Failure("invalid_location", $"Location '{location}' not found."); } catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable) { return CapabilityResult.Failure("service_unavailable", "Weather service is temporarily down."); } catch (RateLimitException) { return CapabilityResult.Failure("rate_limit_exceeded", "Too many requests. Please try again later."); } } }这个实现的健壮性体现在三个地方:
- 前置防御:
InputSchema的验证在Capability执行前就完成了,非法输入被拦截在门外,不会浪费一次昂贵的网络调用。 - 后置保障:
OutputSchema的承诺,迫使开发者在return CapabilityResult.Success(...)前,必须构造出完全符合预期的JSON结构。这保证了下游的Agent Planner或前端UI,可以安全地、无条件地解析output["temperature"],而不用担心output是个null或者结构错乱。 - 错误分类:每一个
CapabilityResult.Failure(...)都携带一个预定义的errorCategory。Agent Runtime可以根据这个Category,决定是重试、降级、还是向用户展示定制化的错误消息。比如,"invalid_location"可以返回“您输入的城市名似乎有误,请检查后重试”,而"service_unavailable"则应该返回“天气服务暂时不可用,我们正在紧急修复”。
3.3 规划器(Planner)的选择与定制:不是所有Agent都需要ReAct
MAF内置了多种Planner,它们是Agent的“大脑”,负责将用户请求分解为一系列Capability调用。选择哪个Planner,取决于你的Agent复杂度和确定性要求:
| Planner类型 | 适用场景 | 核心特点 | 实操心得 |
|---|---|---|---|
| ReActPlanner | 大多数通用场景 | 基于ReAct(Reasoning + Acting)范式,LLM自主决定何时思考、何时调用工具。灵活性最高。 | 注意:它对LLM的提示词工程要求极高。我建议不要直接用默认提示词,而是基于agent.yaml中的goal和capabilities,生成一个定制化的System Prompt。实测下来,一个包含3个具体示例(Few-Shot)的Prompt,能让规划准确率提升40%。 |
| SequentialPlanner | 流程高度固定的场景 | 严格按照agent.yaml中capabilities的声明顺序执行,或根据一个预定义的DAG(有向无环图)执行。确定性最强。 | 优势:性能好、可预测、易调试。适合“下单-支付-发货”这类线性流程。缺点:无法处理分支逻辑(如“如果库存不足,则推荐替代品”)。你需要把分支逻辑编码进Capability内部。 |
| CustomPlanner | 需要深度集成业务规则的场景 | 继承IPlanner接口,完全自定义规划逻辑。可以调用内部业务规则引擎、决策树或甚至另一个小型ML模型。 | 经验:这是大型企业最常采用的方式。例如,一个风控Agent,其Planner会先调用一个内部的RiskScoreCalculator服务,根据得分决定是放行、人工审核还是直接拒绝,而不是让LLM去“猜”。 |
选择Planner,本质上是在“LLM的创造力”和“业务的确定性”之间做权衡。我的建议是:从ReActPlanner开始,用它快速验证核心Capability是否可用;一旦业务逻辑稳定,就逐步将关键路径迁移到SequentialPlanner或CustomPlanner,以换取生产环境的稳定性和可审计性。不要迷信LLM能解决一切,它应该是你业务逻辑的“增强器”,而不是“替代品”。
4. 实操过程与核心环节实现:一个完整电商客服Agent的搭建
4.1 环境准备与项目初始化
首先,确保你安装了.NET SDK 8.0或更高版本,并更新Visual Studio 2022到v17.10+。打开命令行,创建一个新的MAF项目:
# 创建一个名为 EcommerceAgent 的新项目 dotnet new maf-agent --name EcommerceAgent --output ./EcommerceAgent # 进入项目目录 cd EcommerceAgent # 添加必要的NuGet包 dotnet add package Microsoft.Matlab.AgentFramework dotnet add package Microsoft.Matlab.AgentFramework.Planners.ReAct dotnet add package Microsoft.Matlab.AgentFramework.Memory.Vector dotnet add package Microsoft.Matlab.AgentFramework.Security提示:
dotnet new maf-agent是MAF官方提供的模板。它会自动生成一个基础的Program.cs、agent.yaml和一个示例EchoCapability。这是你所有工作的起点。
4.2 定义核心Capability:订单状态查询
根据agent.yaml中声明的get_order_status能力,我们创建一个具体的Capability实现。在Capabilities/目录下新建OrderStatusCapability.cs:
// Capabilities/OrderStatusCapability.cs using Microsoft.Matlab.AgentFramework; using Microsoft.Matlab.AgentFramework.Capabilities; using Microsoft.Matlab.AgentFramework.JsonSchema; using System.Text.Json.Nodes; namespace EcommerceAgent.Capabilities; public class OrderStatusCapability : ICapability { private readonly IOrderService _orderService; public OrderStatusCapability(IOrderService orderService) { _orderService = orderService; } public CapabilityMetadata Metadata => new() { Name = "get_order_status", Description = "Retrieve the current status and estimated delivery date for a given order ID.", InputSchema = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["orderId"] = new JsonSchema { Type = "string", Description = "The unique identifier of the order." } }, Required = new[] { "orderId" } }, OutputSchema = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["orderId"] = new JsonSchema { Type = "string" }, ["status"] = new JsonSchema { Type = "string", Enum = new[] { "pending", "shipped", "delivered", "cancelled" } }, ["estimatedDelivery"] = new JsonSchema { Type = "string", Format = "date-time" }, ["trackingNumber"] = new JsonSchema { Type = "string" }, ["items"] = new JsonSchema { Type = "array", Items = new JsonSchema { Type = "object", Properties = new Dictionary<string, JsonSchema> { ["name"] = new JsonSchema { Type = "string" }, ["quantity"] = new JsonSchema { Type = "integer" } } } } } }, ErrorCategories = new[] { "order_not_found", "access_denied" } }; public async Task<CapabilityResult> ExecuteAsync(CapabilityContext context, JsonNode input) { var orderId = input["orderId"].ToString(); try { // 1. 从Execution Memory中获取当前用户ID(假设已在之前步骤中存入) var userId = context.ExecutionMemory.Get<string>("current_user_id"); // 2. 调用业务服务 var order = await _orderService.GetOrderAsync(orderId, userId); // 3. 构建结构化输出 var output = new JsonObject { ["orderId"] = order.Id, ["status"] = order.Status.ToString().ToLower(), ["estimatedDelivery"] = order.EstimatedDelivery.ToString("o"), ["trackingNumber"] = order.TrackingNumber ?? "", ["items"] = new JsonArray(order.Items.Select(i => new JsonObject { ["name"] = i.ProductName, ["quantity"] = i.Quantity })) }; return CapabilityResult.Success(output); } catch (OrderNotFoundException) { return CapabilityResult.Failure("order_not_found", $"Order '{orderId}' was not found or does not belong to you."); } catch (UnauthorizedAccessException) { return CapabilityResult.Failure("access_denied", "You do not have permission to view this order."); } } }注意:这里我们演示了如何从
context.ExecutionMemory中读取数据。这体现了MAF的Execution Memory设计,让Capability之间可以安全、结构化地共享状态,而无需暴露原始的HttpContext或全局变量。
4.3 注册Capability与配置Memory
在Program.cs中,我们需要将Capability注册到Agent,并配置好各种Memory:
// Program.cs using Microsoft.Matlab.AgentFramework; using Microsoft.Matlab.AgentFramework.Capabilities; using Microsoft.Matlab.AgentFramework.Memory; using Microsoft.Matlab.AgentFramework.Memory.Vector; using Microsoft.Matlab.AgentFramework.Planners.ReAct; using Microsoft.Matlab.AgentFramework.Security; using EcommerceAgent.Capabilities; var builder = WebApplication.CreateBuilder(args); // 1. 注册业务服务 builder.Services.AddSingleton<IOrderService, OrderService>(); // 2. 注册Capability实现 builder.Services.AddSingleton<ICapability, OrderStatusCapability>(); builder.Services.AddSingleton<ICapability, ReturnInitiationCapability>(); // ... 注册其他Capability // 3. 配置Knowledge Memory(向量数据库) builder.Services.AddVectorMemoryStore(options => { options.ConnectionString = builder.Configuration.GetConnectionString("VectorDb"); options.CollectionName = "ecommerce_knowledge"; }); // 4. 配置Security Policy builder.Services.AddAgentSecurity(options => { options.InputFiltering.Enabled = true; options.OutputReview.Enabled = true; options.OutputReview.PiiDetection = true; }); var app = builder.Build(); // 5. 启动Agent app.MapAgent("/api/agent"); app.Run();4.4 启动与测试:从命令行到Postman
项目准备好后,运行它:
dotnet run默认情况下,MAF会启动一个HTTP API服务,地址为http://localhost:5000/api/agent。你可以用curl或Postman发送一个请求来测试:
curl -X POST "http://localhost:5000/api/agent" \ -H "Content-Type: application/json" \ -d '{ "userMessage": "我的订单 #ORD-789012 的状态是什么?", "conversationId": "conv_abc123" }'你会得到一个结构化的JSON响应,其中包含了get_order_statusCapability的执行结果,以及Agent的最终回复。整个过程是全自动的:Agent Runtime解析用户消息 → Planner生成调用计划 → Executor执行OrderStatusCapability→ 将结果存入Execution Memory → Planner根据结果生成最终回复。
实操心得:在开发初期,强烈建议开启详细的日志。在
appsettings.Development.json中添加:"Logging": { "LogLevel": { "Default": "Information", "Microsoft.Matlab.AgentFramework": "Debug" } }这样你就能看到Planner是如何思考的、Capability是如何被选中的、Execution Memory是如何变化的。这些日志是调试Agent行为的唯一真相来源。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查与解决技巧 |
|---|---|---|
| Agent返回“我无法回答这个问题”或陷入无限循环 | Planner未能正确生成Capability调用,或调用后未收到有效响应。 | 1. 检查日志:搜索"Planner generated plan",看生成的Plan是否为空或不合理。2. 检查Capability注册:确认 ICapability的实现类已被AddSingleton注册,且Metadata.Name与agent.yaml中声明的完全一致(大小写敏感!)。3. 检查InputSchema:用在线JSON Schema验证器,验证你的 InputSchema是否语法正确。一个常见的错误是把Properties写成properties(小写),导致验证失败。 |
| Capability执行成功,但结果未被Planner用于生成最终回复 | Execution Memory中的数据未被正确引用,或Planner的提示词未指示其使用。 | 1. 在Capability中添加日志:在return CapabilityResult.Success(...)前,打印output.ToString(),确认数据结构正确。2. 检查Planner的System Prompt:确保Prompt中有一句类似“请参考Execution Memory中存储的最新数据来生成回复”的指令。 3. 使用 context.ExecutionMemory.GetAll():在Planner的调试模式下,打印出所有Execution Memory内容,确认数据确实存在。 |
| 向量知识库检索返回无关结果 | 知识文档的分块(chunking)策略不当,或嵌入(embedding)模型与检索模型不匹配。 | 1. 检查分块大小:对于FAQ类文档,建议使用较小的分块(256-512 tokens),并保留问题-答案对的完整性。 2. 检查嵌入模型:确保你用于生成向量的模型(如 text-embedding-3-small)与MAF配置中使用的模型完全一致。模型版本不同,向量空间就不同。3. 启用HyDE(Hypothetical Document Embeddings):在 agent.yaml的knowledge配置中,添加hyde: true,让Agent在检索前,先生成一个“假设性答案”并对其嵌入,能极大提升语义相关性。 |
| 安全策略(Security)误报,拦截了合法请求 | inputFiltering.ruleset过于激进,或outputReview.piiDetection的正则表达式太宽泛。 | 1. 在开发环境中临时禁用:将options.InputFiltering.Enabled = false,确认问题消失,从而定位是安全策略的问题。2. 自定义规则集:不要直接修改预置规则集。创建自己的 MyEcommerceRuleset类,继承IInputFilteringRuleset,只覆盖你需要调整的规则。3. PII检测白名单:对于必须显示的、非敏感的ID(如订单号 ORD-12345),可以在outputReview配置中添加whitelistPatterns,用正则表达式明确允许。 |
5.2 我踩过的几个深坑
坑一:Capability的异步陷阱
我最初写了一个SendEmailCapability,里面调用了SmtpClient.SendMailAsync()。测试时发现,Agent有时会返回“邮件已发送”,但实际邮件从未到达收件箱。日志显示CapabilityResult.Success被返回了,但SendMailAsync()的await还没完成。这是因为MAF的ExecuteAsync方法签名是Task<CapabilityResult>,但如果你在方法体内没有await所有异步操作,就会发生“fire-and-forget”(发射即忘)。
解决方案:永远、永远、永远在ExecuteAsync中await所有异步调用。如果某个操作确实不需要等待(比如发一个后台日志),请明确使用_ = SomeAsyncOperation();,并添加注释说明。
坑二:Execution Memory的生命周期误解
我以为Execution Memory是跨会话的,就像Conversation Memory一样。于是我把用户的偏好设置(如“语言=中文”)存进了Execution Memory,期望下次对话还能用。结果发现,每次新对话,这些设置都丢失了。
真相:Execution Memory是单次Agent执行周期(single execution cycle)的。一次用户提问,触发一次完整的Planner→Executor→Planner→...→Response流程,这个流程中用到的所有中间状态,都存在Execution Memory里。一旦这次流程结束,Execution Memory就被清空。Conversation Memory才是跨多轮对话的。
解决方案:对于需要持久化的用户状态,应该存入Conversation Memory,并在agent.yaml中为其配置persistence: true。或者,更推荐的做法是,将用户偏好作为userMessage的一部分,由Planner在每次执行时主动读取和应用。
坑三:YAML中的环境变量未被正确解析
我在agent.yaml中写了connectionString: "${DB_CONNECTION_STRING}",但在运行时,MAF报错说连接字符串为空。
原因:MAF默认只从appsettings.json中读取配置,不自动解析YAML中的环境变量。你需要在Program.cs中显式启用:
// 在builder.Build()之前添加 builder.Configuration.AddEnvironmentVariables(); // 这行很重要! // 然后在AddVectorMemoryStore时,就可以使用环境变量了 builder.Services.AddVectorMemoryStore(options => { options.ConnectionString = builder.Configuration["ConnectionStrings:VectorDb"]; });这个坑让我花了整整一个下午,因为错误日志非常模糊,只说“无法连接到向量数据库”,根本没提是配置没读到。
6. 总结与个人体会:一场关于“控制权”的再分配
回顾整个从Semantic Kernel到Microsoft Agent Framework的演进,我最大的体会是:这不仅仅是一次技术框架的升级,更是一场关于“控制权”的再分配。在SK时代,开发者手握着所有的控制权——从Prompt的每一个标点,到Function的每一次调用,再到Memory的每一次存取。这种“全知全能”的感觉很爽,但也意味着你承担了全部的责任和风险。当业务逻辑变得复杂,这种模式就成了沉重的枷锁。
MAF所做的,