☰
.NET AI Agent实战:用Agent Skills为智能体装上执行能力
2026/10/20 11:41:42 网站建设 项目流程

做.NET的人现在聊AI,绕不开一个痛点:拿大模型接口写个聊天Demo很容易,但要让Agent真正“干活”,调个数据库、算笔账、对接个内部系统,就得在Prompt里堆工具描述、自己维护Function Calling的schema,工具一多就乱成一团。最近我在用Microsoft Agent Framework做 Agent 项目时,把Agent Skills这一整套机制从头到尾踩了一遍,发现它把“让Agent具备执行能力”这件事做得很顺手。这篇就用一步步实操的方式,讲清楚 Skills 是什么、为什么需要它、怎么在十分钟内把自定义技能挂到 Agent 上,以及怎么从 MCP 服务把外部能力导入成自己的技能。

如果你正打算用 .NET 构建AI应用,或者已经用 Semantic Kernel / Aspire 跑通了基础对话,想让 Agent 摆脱“只会聊天”的尴尬,这篇文章能给你一份直接照着抄的配置和代码。读完你不仅能快速集成 Skills,还能避开几个我刚上手时踩过的坑。

1. 内容整体设计与思路拆解:给Agent“装上双手”

1.1 Agent Skills的本质:从“会说话”到“会做事”

先讲清楚一个概念:Agent Skills是 Microsoft Agent Framework 里用来封装和注册工具能力的标准单位。Agent 本身是一个能理解用户意图、进行多轮对话的实体,但只有对话能力,它本质上还是一个聊天机器人。要让 Agent 真正完成复杂任务,它需要调用外部能力——查数据、跑代码、调接口。Skills 就是这套框架里承担这个职责的模块。

我用一个生活化的类比:你把 Agent 理解成一个新入职的助理。助理脑子很聪明(模型能力强),但刚到岗时没技能,不会看报表、不会发邮件、不会写代码。Skills 就像你逐个给助理发“上岗证书”,每拿到一个技能证书,助理就能独立处理一类任务。框架里的 Skills 集合就是这个助理的技能清单,模型在对话过程中会根据任务自动挑出合适的技能来调用。

这套设计最核心的价值,是把“模型的理解能力”和“工具的执行能力”彻底解耦。你不需要为了加一个工具去改 Prompt,不需要每次对话都手动拼接 Function Calling 的参数。你唯一要做的是把技能注册到 Agent 上,剩下的触发时机、参数解析、结果返回,都交给框架和模型协作完成。

1.2 为什么用“技能化”而不是把工具全塞进Prompt

早年间做AI应用,大家习惯把所有工具函数直接写进 System Prompt 里让模型调用。小项目里工具三五个,这种写法确实简单粗暴还能跑。但一旦工具数量超过十个,问题就接踵而至:Prompt 长度膨胀,模型选择工具的准确率肉眼可见地下降,每加一个工具就要动一遍 Prompt,迭代成本高得离谱。

Skills 方案的关键优势,在于把工具的“元信息”(名称、描述、入参出参)和“执行逻辑”打包成一个独立单元。注册到 Agent 之后,模型根据用户请求动态决定要不要调用技能、调用哪个。它天然和模型上下文分离——技能再多,也不是每轮对话都把所有技能细节塞进上下文,而是需要时才加载相关信息。这种设计在企业级 Agent 项目里特别有意义,因为企业里的技能数量和复杂度远超个人 Demo。

1.3 两种技能来源:本地定制技能与MCP外部技能

Microsoft Agent Framework 默认给了两条引入技能的路径,可以混合使用。

第一条是本地定制技能。继承 AgentSkill 基类(不同预览版本命名可能略有差异),重写执行逻辑,适合封装项目内部的业务能力,比如查订单、算运费、做数据校验。这种方式灵活度最高,技能内部可以调用任何 .NET 代码。

第二条是从 MCP 服务器加载技能。MCP(Model Context Protocol)是一个把远程工具服务标准化的协议,你的 Agent 可以连上某个 MCP Server,把服务器上暴露的工具自动导入为本地技能。这就像给浏览器装插件——装上就能用,卸载也干净,不污染核心逻辑。

从职责划分的角度说,这套框架把“技能发现”和“技能执行”分得很清楚。你不需要知道技能内部怎么实现的,只需要知道它有名字、有描述、有入参出参,Agent 会自动决定什么时候调用。我在企业项目里推荐用这个模型,原因就是职责边界清晰:不同团队各自维护自己的技能模块,互不干扰,集成成本低。

顺带提一句,Agent Skills 的相关 API 目前还在迭代,不同预览版可能命名略有差异。我这里写的是当前预览版能跑通的写法,建议你以自己项目实际引用的 NuGet 包版本为准,重点掌握设计思路和步骤,不用死记 API 名称。

2. 环境准备:把Agent基础工程跑起来

2.1 依赖包与运行环境

我本机用的是 .NET 9,实际上 .NET 8 也能跑,关键是把依赖包装齐。需要引入的项目依赖大致如下:

  • Microsoft.Agent.Framework:Agent 框架核心包
  • Microsoft.Agent.Framework.Skills:技能相关的扩展包
  • Microsoft.SemanticKernel:Agent 框架底层依赖的语义内核
  • Microsoft.Extensions.AI:AI 客户端统一抽象
  • Microsoft.Extensions.AI.AzureOpenAI:Azure OpenAI 客户端实现

如果还要接 MCP,额外装这两个:

  • Microsoft.Agent.Framework.Skills.MCP:MCP 技能接入扩展
  • ModelContextProtocol:MCP 协议客户端

每个预览版的包名和依赖关系会调整,最稳妥的装法是在 NuGet 里搜关键词“Agent Framework”,看当前版本包含哪些程序集,再按需引入。

2.2 接入模型服务:ChatClient的配置

Agent 要能对话,必须先绑定一个模型客户端。Microsoft Agent Framework 沿用了Microsoft.Extensions.AI的抽象,也就是IChatClient接口。不管底层是 Azure OpenAI、OpenAI 官方接口还是本地模型,只要实现了IChatClient都能直接接入。

我以 Azure OpenAI 为例,写一下依赖注入配置:

using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Extensions.AI; var builder = WebApplication.CreateBuilder(args); // 从配置读取连接信息 var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]; var deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]; builder.Services.AddSingleton<IChatClient>(_ => new AzureOpenAIClient( new Uri(endpoint), new DefaultAzureCredential()) .AsChatClient(deploymentName)); var app = builder.Build();

这段代码的核心两步:AzureOpenAIClient负责和 Azure OpenAI 服务建立连接,AsChatClient扩展方法把它适配成统一的IChatClient。之后创建 Agent 时只需要依赖IChatClient这个抽象,完全不用关心底层是哪个模型服务商。

如果你的密钥走环境变量,也可以换new ApiKeyCredential("密钥"),看团队的凭据管理规范来定。用DefaultAzureCredential的好处是支持托管身份和本地开发凭据链,部署到云上时不用改代码。

2.3 创建基础ChatAgent并绑定技能集合

有了IChatClient,接下来创建 Agent。ChatAgent是框架内置的标准聊天代理类型,创建时需要传一个 agent 名称和 chatClient:

using Microsoft.Agent.Framework; using Microsoft.Agent.Framework.Skills; var agent = new ChatAgent("appAgent", chatService); agent.Description = "你是一个企业智能助手,可以回答用户问题,并根据需要调用工具技能。";

Description这段文本会被模型读取,影响 Agent 后续的行为风格和技能选择倾向,建议一开始就写清楚。

创建之后最关键的一步,是把技能挂到 Agent 上。Agent 暴露了一个Skills集合,所有技能都往里面 Add:

agent.Skills.Add(new CodeInterpreterSkill());

这里CodeInterpreterSkill是框架自带的一个通用技能——代码解释器。它能让 Agent 真正执行 C# 或 Python 代码,处理数值计算、数据分析等任务。对很多场景来说,这算是开箱即用的第一个技能,相当于给 Agent 装了个“计算器”。

注册完之后,Agent 就已经具备基础对话能力和一项执行能力了。接下来真正的重头戏,是写出一个完全属于自己的自定义技能,这正是下一节的内容。

3. 核心实操:一步步集成Agent Skills

3.1 第一步:编写自定义技能类

自定义技能的写法非常规范。你需要继承AgentSkill基类,在构造函数里定义技能的名称和描述,然后重写ExecuteAsync方法实现具体逻辑。

我写一个具体的例子:汇率换算技能。Agent 在对话中遇到“100美元等于多少人民币”这类问题时,模型会判断这个任务适合用汇率换算技能,然后触发执行。

using Microsoft.Agent.Framework.Skills; public sealed class CurrencyExchangeSkill : AgentSkill { public CurrencyExchangeSkill() : base("CurrencyExchange") { Description = "当用户需要把一种货币金额换算成另一种货币时使用,例如'100美元等于多少人民币'。支持USD、CNY、EUR等币种。"; } protected override async Task<string> ExecuteAsync( SkillContext skillContext, string userInput, CancellationToken cancellationToken) { // 这里简化处理:实际项目中建议调用实时汇率API,或者从配置中心读取 var rates = new Dictionary<string, decimal> { ["USD_CNY"] = 7.24m, ["EUR_CNY"] = 7.85m, ["CNY_USD"] = 0.138m }; // 从 userInput 中解析金额和币种 var amount = ParseAmount(userInput); var from = ParseCurrency(userInput, "from"); var to = ParseCurrency(userInput, "to"); var rateKey = $"{from}_{to}"; if (!rates.TryGetValue(rateKey, out var rate)) { return $"暂不支持 {from} 到 {to} 的汇率换算"; } var result = amount * rate; return $"{amount} {from} = {result:F2} {to}"; } private static decimal ParseAmount(string input) { var match = System.Text.RegularExpressions.Regex.Match(input, @"(\d+(\.\d+)?)"); return match.Success ? decimal.Parse(match.Groups[1].Value) : 0m; } private static string ParseCurrency(string input, string direction) { // 简化版:通过关键字匹配币种 foreach (var currency in new[] { "USD", "CNY", "EUR", "美元", "人民币", "欧元" }) { if (input.Contains(currency, StringComparison.OrdinalIgnoreCase)) { return currency switch { "美元" => "USD", "人民币" => "CNY", "欧元" => "EUR", _ => currency }; } } return "USD"; } }

注意几个细节。

第一,构造函数里传给基类的字符串就是技能的唯一标识。模型在决策时就是靠这个名字加Description来决定是否调用,所以名字要简短清晰,描述要写得具体,最好包含“什么时候用”的触发条件。

第二,ExecuteAsync方法接收一个SkillContext,里面封装了当前对话的上下文信息,包括历史消息、用户输入等,可以直接读取。

第三,返回类型是Task<string>。返回的结果会作为工具调用的输出送回给模型,模型再结合这个结果生成最终回复。所以返回值最好是人类可读的文本,而不是裸的 JSON。

实际项目中,我更推荐定义一个强类型的输入参数结构,让模型按结构化参数传入,而不是像示例这样从自然语言里硬解析。不过示例的方式对快速验证技能链路已经足够。

3.2 第二步:把自定义技能注册到Agent

注册技能就一行代码:

agent.Skills.Add(new CurrencyExchangeSkill());

但这一行背后有一个值得注意的机制。Add操作执行时,框架会把技能的元信息——名字、描述、参数定义——登记到 Agent 的技能注册表。当用户发起对话时,模型会先看到所有技能的名字和描述,然后根据任务相关性决定要不要调用某个技能。

这里有个实战经验:注册多个技能后,模型的每一次对话都面临“要不要用技能、用哪个技能”的决策。技能名称如果太相似,模型很可能选错。比如你注册一个“汇率查询”和一个“汇率换算”,模型大概率会混淆。所以命名要让技能边界清晰,一个技能只完成一件职责明确的事。

3.3 第三步:从MCP服务器加载外部技能

除了本地自定义技能,更高效的能力扩展方式是从 MCP 服务器直接导入技能。

现在很多平台提供了 MCP Server:数据库查询服务、在线搜索服务、企业内部系统等,都可以通过 MCP 协议暴露能力。Agent 框架提供了一个非常便捷的创建入口:

using Microsoft.Agent.Framework.Skills.MCP; using ModelContextProtocol; // 创建一个MCP客户端连接 var mcpServer = await McpServer.CreateAsync( new() { Url = "http://localhost:8080/mcp" }); // 从MCP服务器创建技能并注册到Agent var skillFromMCP = await agent.Skills.CreateFromMCPAsync(mcpServer); agent.Skills.Add(skillFromMCP);

CreateFromMCPAsync方法会去 MCP 服务器上拉取所有注册的工具列表,把每一个工具封装成 Agent 能识别的技能,底层用的是 MCP 协议标准的initialize和tools/list接口,所以跨平台、跨语言都通用。

这种方式最大的价值在于:你的 Agent 不需要为每个新工具手写适配代码。只要服务端按 MCP 标准暴露工具,Agent 就能无缝接入。类比一下,就像你的电脑装上了一个“万能驱动”,插上任何符合标准的设备都能直接识别。

实际使用中有个注意点:MCP 导入过来的技能,其描述信息由服务端提供,质量参差不齐。如果某些技能描述写得太含糊,Agent 可能不会在正确时机调用。这种情况建议回服务端修正描述,或者在导入后对技能做二次加工,把描述改写清晰。

3.4 第四步:启动服务并验证技能调用效果

技能注册完成后,整个 Agent 已经可以对外服务了。我用一个 Web API 验证一下:

app.MapPost("/chat", async (ChatTurn[] chatTurns) => { var response = await agent.GetNewResponseAsync(chatTurns); return Results.Ok(response); }); app.Run();

这里ChatTurn是 Agent 框架定义的消息结构,和通用 ChatMessage 类型基本一致。发起一次对话请求后,Agent 内部会经历这样的流程:

  1. 模型读取用户消息,结合当前注册的技能清单做决策;
  2. 如果模型判定需要调用某个技能,框架自动执行技能逻辑;
  3. 技能返回结果,模型综合结果生成最终回复返回给调用方。

我用上面的汇率技能做了一次实测。对话内容是“帮我算一下500美元能换多少人民币”。模型识别到这是汇率换算场景,自动调用了CurrencyExchange技能,最终返回“500 USD = 3620.00 CNY”。整个调用过程用户完全无感知,他只是在和一个智能助手对话,不需要自己指定工具。

这个“无感调用”的特性非常关键。Agent 框架把工具选择的决策权交给模型和系统,这是体验上和传统机器人对话框最大的区别。

4. 常见问题与排查技巧实录

4.1 技能“没被调用”怎么办:先查描述,再查命名,最后查注册时机

我踩过不少次这个坑。技能注册好了,代码没报错,但 Agent 就是不调用技能,回答全靠模型硬答。排查思路一般分三步。

第一步,检查技能的Description。模型决定是否调用技能,主要依据就是描述文本。如果描述写得太宽泛,比如就“汇率计算”四个字,模型很难判断这个技能和当前问题有什么关系。建议写成触发条件清晰的完整句子,例如“当用户需要把一种货币换算成另一种货币时使用,支持USD/CNY/EUR等币种”。这样模型能准确匹配用户意图。

第二步,检查技能名称是否和其他技能冲突。如果两个技能名字前缀相同或者功能相近,模型容易混淆。比如“GetWeather”和“GetWeatherForecast”同时存在时,模型可能选错。

第三步,检查注册时机。确认技能是在 Agent 创建之后、首次对话之前 Add 进 Skills 集合的。如果 Agent 已经在处理对话了,中途注册技能也未必能让当前会话立即感知。

4.2 技能执行报错但Agent没报错:返回的是文本而不是异常

有时候技能内部抛了异常,但框架层并没有向上传播。这是因为框架在技能执行环节做了容错处理——执行失败时会把错误信息包装成文本返回给模型,由模型决定怎么跟用户解释,而不是直接中断对话。

所以排查技能内部问题时,建议在技能代码里加好日志输出,把关键变量和执行路径打出来。比如:

Console.WriteLine($"[Skill] CurrencyExchange called, amount={amount}, from={from}, to={to}");

开发阶段这个日志特别有用。你可以在服务端日志里看到技能的实际输入参数,判断到底是模型传参错误还是执行逻辑有 bug。

4.3 多技能之间的选择冲突:描述里写清边界

一个 Agent 往往注册了多个技能,模型面对技能列表时怎么选?框架没有严格的优先级配置,主要靠描述引导。

我的经验是:描述里把“适用范围”和“典型问法”写清楚,模型就能根据语义特征做区分。举个例子,我注册过“订单查询”和“物流查询”两个技能。一开始描述都写“查询订单信息”,模型经常选错。后来把物流查询的描述改成“当用户询问包裹当前所在位置、配送进度、运单号时使用”,模型的选择准确率明显提升。

这是很值得记住的一点:技能描述本质上是你写给模型看的“使用说明书”,不是给自己写的注释。描述质量直接决定技能在复杂对话场景下的命中率。

4.4 MCP技能导入失败:先查tools/list接口,再查网络

从 MCP 服务器导入技能时,如果导入后技能列表为空,先检查服务器端是否把工具正确暴露了。MCP 协议有tools/list接口,可以直接用调试工具(比如命令行 curl)请求一下这个接口,确认返回的工具列表非空。

其次检查网络连通性,确认 Agent 服务能访问到 MCP 服务器地址。如果是跨网络的 MCP 服务,还要检查防火墙和身份认证配置。

4.5 技能执行的“黑盒”问题:善用日志和跟踪

最后说一个通用技巧:任何 Agent 框架在初期调试时都像一个黑盒——你发一句话进去,返回一句话出来,中间发生了什么完全靠日志还原。建议在项目里给IChatClient加上日志中间件,记录模型每次收到和返回的消息。

Microsoft.Extensions.AI里可以通过ChatClientBuilder加自定义的日志处理器,或者接入 OpenTelemetry。这样你就能看到模型在调用技能前后的完整决策路径,排查问题效率翻倍。

我把常见问题整理成一张速查表:

问题现象常见原因优先排查动作
技能注册了但从未被调用描述模糊、名称与其它技能冲突、注册时机太晚重写Description,检查命名,确认在首次对话前注册
技能执行报错但用户看到的是正常回复框架把异常包装成文本返回给模型在技能内增加日志,看实际输入参数和执行路径
多技能场景选错技能技能描述边界不清、功能重叠为每个技能写明典型问法,明确适用范围
MCP导入后技能列表为空服务器未暴露工具、网络不通、认证失败用curl请求tools/list接口,确认连通性和认证
Agent回答完全忽略技能模型上下文里技能信息被截断、基线模型太弱缩短技能描述,确认使用足够强的模型版本

5. 实操心得:把Skills用好的几点建议

5.1 技能粒度:宁可多而小,不要少而大

技能设计最容易犯的错误,是把一个技能写得非常大,入参又多又杂,描述也写不清楚。我见过有人把“订单管理”做成一个技能,包含查询订单、创建订单、修改订单、取消订单四个功能,结果模型经常搞混参数。

更好的做法是把每个操作拆成独立技能:QueryOrder、CreateOrder、UpdateOrder、CancelOrder。每个技能描述都很聚焦,模型选择时准确率更高,维护时各改各的互不影响。你每多拆一个技能,实际上是在给模型减少一次“选错”的机会。

5.2 技能描述:这是给模型看的文档

很多开发者把技能描述当作给自己备忘的注释,随便写一句“汇率换算”就完事。但要注意,这些描述是直接进入模型决策上下文的,质量直接影响技能被调用的概率和正确率。

我建议描述至少包含三个要素:

  1. 触发场景:什么时候该用这个技能;
  2. 输入要求:需要哪些参数;
  3. 典型示例:给一两个用户可能的问法。

好的描述大概是这样的:

Description = "当用户需要把一种货币金额换算成另一种货币时使用,例如'100美元等于多少人民币'。输入包括:金额、原始币种、目标币种,支持USD、CNY、EUR等币种。";

模型看到这种描述后,基本不会在类似场景下选错技能。你在开发阶段多花两分钟把描述写好,能省掉后面大量排查时间。

5.3 从技能到完整Agent:能力的扩展路径

最后聊一点方向性的思考。Agent Skills 不是终点,它是 Agent 能力扩展的基础设施。我在企业级 Agent 项目里的扩展路径大致是这样:

第一步,先用框架自带技能和 MCP 导入技能,把 Agent 能做的事快速铺开,覆盖典型场景。

第二步,根据业务反馈,把高频使用的 MCP 技能沉淀为本地自定义技能,补充更细致的提示词和校验逻辑。

第三步,再叠加规划能力和多 Agent 协作能力,让多个不同技能的 Agent 协同完成更大粒度的任务。

每一步都不需要重写底层逻辑,只是持续往 Skills 集合里加技能、优化技能描述。Agent 的智能化程度会随着技能数量和质量同步提升。这也是这套框架给我最直观的感受——Agent 的能力是“长”出来的,不是“写”出来的。

我不太喜欢堆砌概念的框架,但在这点上 Microsoft Agent Framework 确实做得很实在:把复杂的能力扩展做成了一套简单的注册机制,让开发者把注意力集中在业务技能本身,而不是底层工具调度的细节上。如果你正在评估如何给现有 .NET 项目引入 AI 能力,可以从 Agent Skills 入手,很快你就能感受到一个从“聊天机器人”进化到“数字员工”的完整过程。

最后再分享一个小技巧:技能里如果能返回结构化和人类可读文本兼顾的结果,后续排查和模型理解都会舒服很多。比如汇率换算技能,既返回“500 USD = 3620.00 CNY”这样的展示文本,又可以在后面附带 JSON 明细。这样模型既有可读的回复素材,你也有结构化的数据可追踪。这个小习惯,在我后来做复杂技能集成时帮了大忙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询