☰
.NET + SK + MCP:构建AI Agent工具调用能力层实战
2026/9/26 16:58:03 网站建设 项目流程

这两年大模型工具链里最热的一个词,恐怕就是 MCP(Model Context Protocol,模型上下文协议)了。如果你在 .NET 生态里做 AI 应用,又对 Semantic Kernel(SK)不陌生,那应该已经感受到了一个趋势:MCP 正在成为 Agent 连接外部工具和数据的"标准插座",而 SK 作为编排层,天生就是承接这波能力的好位置。

我最近基于 .NET 8 + SK 完整搭了一套 MCP 能力层,把工具调用、数据源访问、Agent 编排整个链路理了一遍,跑通了从 MCP Server 注册到动态工具调用的全流程。这篇文章不跟你讲 PPT 上的概念,直接把我的系统方案、关键设计取舍、核心代码以及踩过的坑摊开来讲。无论你是刚接触 MCP 的新手,还是已经在用 SK 做 Agent 的开发者,这篇文章都能帮你节省大量试错时间。

1. 整体设计思路:为什么是 .NET + SK + MCP

1.1 先理解 MCP 在整套体系里到底扮演什么角色

MCP 本质上解决的是一个很朴素的问题:大模型再聪明,它也只能"想",不能"做"。它想获取实时天气、查询数据库、操作文件、调用内部 API,都必须借助外部工具。但过去每个工具都要单独对接一套接口协议,模型方要适配 N 种工具 SDK,工具方也要为每个模型写一遍集成,两边都累。

MCP 做的事情,就是把"工具"抽象成一种标准化的资源:一个 MCP Server 对外暴露工具列表和调用端点,模型侧的客户端(比如 SK、Claude Desktop、各种 Agent 框架)通过统一协议发现工具、调用工具。这就好比 USB-C 接口——以前每个设备都有自己的充电口,现在统统统一成一个口,谁都能插。

在整个 AI 应用架构里,MCP 处于最底层的能力供给层,SK 处于中间的应用编排层,大模型在最上层做决策。SK 通过 MCP 协议拿到工具清单,大模型根据用户意图决定调哪个工具,SK 再通过 MCP 协议把参数传给对应的 MCP Server 执行。这个链路清晰、解耦、可扩展,是我最终选定这套体系的核心原因。

1.2 为什么选 .NET 而不是 Python/Node

这可能是很多团队最先纠结的问题。如果你的 AI 应用跑在 .NET 系的后端基础设施上(比如企业内部系统、ERP、工业软件),那用 .NET 做 MCP Server 有天然优势:

  • 与现有 .NET 业务代码共享模型类、数据库访问层、配置中心,不需要像 Python 方案那样单独维护一套服务。
  • .NET 8 的原生 AOT 支持可以让 MCP Server 启动做到毫秒级,在边缘设备或函数计算场景下特别有用。
  • 托管成本低,部署链路和现有服务完全打通,运维不用新学一套技术栈。

当然,Python 在 AI 生态的工具链成熟度上确实更高,但那是做研究、做算法训练的场景。做企业级工程化,.NET 的稳定性和可维护性优势非常明显。

1.3 整体系统架构分层

我搭的这套系统分四层,每层职责单一,互不越权:

  • 接入层:面向最终用户或上层应用,接收自然语言请求,走 SK 的 chat completion 流程。
  • 编排层:SK Kernel 是核心,负责注册工具、管理对话历史、调用大模型决策、执行工具调用。这一层是"大脑和小脑"的连接器。
  • 能力层:也就是本文标题说的 MCP 能力层,由一组 MCP Server 组成,每个 Server 负责一类领域能力(天气、数据库、文件、企业 API 等)。
  • 基础设施层:大模型 API 网关、向量存储、日志系统、配置中心,为上层提供基础支撑。

这套分层的核心逻辑是:编排层不关心某个工具的具体实现,只认 MCP 协议这个标准接口;能力层不关心大模型怎么决策,只把自己的能力按照协议暴露出来。两边通过协议解耦,新增能力 = 新起一个 MCP Server 或注册一个新工具,不碰编排层代码。

2. 核心细节解析与实操要点

2.1 SK 与 MCP 的对接方式选型

SK 官方在 1.x 版本里已经内置了 MCP 支持,通过ModelContextProtocol这个包就可以把 MCP Server 作为工具源接入 Kernel。如果你还没用过这个能力,我的建议是:优先用官方包,不要自己造轮子去解析 MCP JSON-RPC 协议。

对接方式分两种,我分别说一下适用场景:

  • 本地 stdio 方式:MCP Server 作为子进程启动,通过标准输入输出和 SK 通信。适合本机开发、工具数量少、希望零网络开销的场景。
  • HTTP (SSE) 方式:MCP Server 独立部署成 HTTP 服务,SK 通过 SSE 或 streamable HTTP 远程调用。适合多客户端共享一套工具、工具部署在独立服务器上的场景。

我最终选择的是 HTTP 方式,原因有三个:一是多环境复用(开发、测试、生产各部署一套 Server),二是方便用 Postman 直接调试工具接口,三是后续接 Web 端客户端不需要额外改造。

如果用 HTTP 方式,MCP Server 需要实现两个类别的端点:一个是协议初始化端点(处理 initialize 请求和工具列表请求),一个是消息端点(处理工具的 call 请求)。在 .NET 里,直接用 ASP.NET Core Minimal API 就能干净地实现,不需要引入太重的框架。

2.2 工具定义的 schema 规范

MCP 的工具描述走 JSON Schema。SK 拿到这些 schema 之后,会把它转成大模型能理解的 function calling 格式。所以这里有个容易被忽略的关键点:工具描述的 schema 写得好不好,直接决定大模型能不能正确调用工具。

我复盘了几个写 schema 时特别影响调用准确率的细节:

  • description字段一定要写清楚"什么时候该用这个工具",最好带上典型场景和参数示例。大模型是靠 description 做语义匹配的,写得模糊它就乱调。
  • 参数要声明required,值域约束尽量给enum。大模型对开放式参数的填充容易放飞自我,给足约束能大大减少参数校验失败。
  • 参数描述里明确单位、格式。比如查询天气的温度单位是摄氏度还是华氏度,时间参数是"yyyy-MM-dd"还是时间戳,这些必须写死在描述里。

我把 schema 的编写理解为"教大模型用工具"的过程。你教得越具体,它用错的可能性越低。如果你发现某类工具调用经常出错,先回去检查 schema 描述,大多数问题都能在这里找到答案。

2.3 能力层工具粒度的设计原则

这是整个系统设计里最容易走偏的地方。工具粒度太粗,一个大而全的工具包含一堆可选参数,大模型会困惑该填什么;粒度太细,几百个工具堆在一起,大模型在匹配时也会出现混淆。

我遵循三条经验原则:

  • 按业务动作拆,不按数据表拆。比如用户管理域,不拆"获取用户信息""获取用户列表""获取用户手机号"这样的数据型工具,而是拆"登录用户查询""用户资料编辑权限校验"这类场景型工具。
  • 单工具参数控制在 5 个以内。参数超过 5 个,大模型填参的出错率明显上升。如果确实需要很多输入,可以考虑拆成多个工具分步调用。
  • 同域工具的数量控制在 10~15 个以内。一个 MCP Server 的工具太多,工具选择的准确率会下降。超过这个数就应该拆分成多个 Server 或按子域分组。

2.4 安全与权限控制

MCP 解锁了模型调用工具的能力,但同时也引入了新的攻击面。工具一旦注册,大模型就有可能在某个对话上下文里误调用敏感操作。我在系统里做了三层防护:

第一层是工具注册白名单。不是所有方法都自动暴露成 MCP 工具,而是显式地逐个注册。默认关闭一切权限敏感操作(删除、写入、转账等),需要额外授权才打开。

第二层是调用参数校验。MCP Server 端不做"信任上游"的假设,所有参数在 Server 端重新校验一遍。特别是文件路径、SQL 片段、URL 这类注入高发参数,必须在 Server 端做严格白名单校验。

第三层是敏感操作的人工确认。对于删除、覆盖、转账、外发等高风险工具,在 SK 编排层做拦截,先返回一个确认请求给用户,用户确认后再放行。这个机制虽然多了一步交互,但在生产环境里非常必要。

3. 实操过程与核心环节实现

3.1 前期环境准备与项目结构

我用的开发环境是 .NET 8 SDK + Visual Studio 2022(或 Rider),需要安装的 NuGet 包有:

  • Microsoft.SemanticKernel(最新的稳定版本)
  • Microsoft.SemanticKernel.Connectors.OpenAI(或你用 Azure OpenAI 就装对应的 Connector)
  • ModelContextProtocol(SK 的 MCP 集成包)

项目结构我分成了三个独立项目,避免所有代码堆在一个工程里:

Mcp.CapabilityLayer.sln ├── src/AgentHost // SK 编排层,Kernel 配置 + 对话流程 ├── src/WeatherMcpServer // 示例 MCP Server(ASP.NET Core 宿主) └── src/Contract // 共享的模型与接口定义

把 Server 单独拆出来有两个好处:一是它可以独立部署、独立扩缩容,二是多个 Agent 应用可以共用同一套能力层服务。

3.2 写一个最简 MCP Server(示例:天气服务)

以天气查询为例,这是最直观的 MCP 工具场景。我新建了一个 ASP.NET Core 空项目,然后添加ModelContextProtocol.AspNetCore包。这个包提供了把 MCP 端点挂到 ASP.NET Core 管线的辅助方法。

先定义一个气象数据服务类,它内部放着模拟数据和真实 HTTP 请求逻辑的替换点:

public interface IWeatherService { Task<WeatherResult> GetCurrentWeatherAsync(string city, string unit = "celsius", CancellationToken ct = default); Task<IReadOnlyList<WeatherForecast>> GetForecastAsync(string city, int days = 3, CancellationToken ct = default); } public sealed record WeatherResult(string City, double Temperature, string Unit, string Condition, string UpdatedAt); public sealed record WeatherForecast(string Date, double High, double Low, string Condition);

实现类里可以先做一个简单的内存数据版本,方便本地联调。真要接真实天气源,把这里换成 HTTP 调用或者第三方 SDK 就行。重点在于工具注册部分:

builder.MapMcpServer("weather-server", options => { options.WithHttpTransport(); var weather = builder.Services.BuildServiceProvider() .GetRequiredService<IWeatherService>(); options.WithTools(weather); });

WithTools会自动扫描服务类的公开方法,并结合 XML 注释或特性生成对应的 JSON Schema。这就是"裸方法秒变 MCP 工具"的核心入口。

为了在工具说明里给大模型更准确的提示,我给方法补充 XML 注释,这些注释最终会成为 schema 的 description:

public sealed class WeatherService : IWeatherService { /// <summary> /// 查询指定城市的当前实时天气情况。适合用户询问"现在冷不冷"、"今天天气如何"等场景。 /// 城市字段支持中文城市名(如"北京"、"上海")或拼音(如"beijing"、"shanghai")。 /// 温度单位默认为 celsius,可选 fahrenheit。 /// </summary> public Task<WeatherResult> GetCurrentWeatherAsync(string city, string unit = "celsius", CancellationToken ct = default) { // 实现略,返回模拟数据或调用第三方API } }

把服务类注册好后,一个具备工具暴露能力的 MCP Server 就已经跑起来了。通过dotnet run启动后,访问/mcp端点就是协议入口。

3.3 在 SK Kernel 里接入 MCP Server

接下来是另一端:在 SK 的 AgentHost 项目里,用ModelContextProtocol包连接 MCP Server,把工具接入 Kernel。

关键代码如下:

using Microsoft.SemanticKernel; using ModelContextProtocol; var builder = Kernel.CreateBuilder(); builder.AddOpenAIChatCompletion("gpt-4o", apiKey); // 接入 MCP 工具 await using var mcpClient = await McpClient.CreateAsync( new McpClientOptions { ClientName = "AgentHost", ProtocolVersion = "2025-03-26", }, McpTransportType.Http, new Uri("http://localhost:5298/mcp")); // 获取工具并加入到 Kernel await foreach (var tool in mcpClient.ListToolsAsync()) { var skTool = tool.AsSKFunction(); builder.Plugins.Add(skTool); } var kernel = builder.Build();

这段代码执行了三个动作:建立 MCP 客户端连接、拉取 Server 端工具清单、把每个工具包装成 SK 插件函数。至此,SK Kernel 就能像调用本地函数一样调用远程 MCP 工具了。

如果你需要并发调用多个 MCP Server,就循环这段逻辑,往builder.Plugins里添加多组插件。每个 Server 对应一组插件,完成插件隔离。

3.4 对话编排:让大模型自主决定调用哪个 MCP 工具

工具接入之后,剩下就是典型的 Agent 循环逻辑。我封装了一个简单的对话执行器:

public class AgentRunner { private readonly Kernel _kernel; public async Task<string> RunAsync(string userPrompt) { var history = new ChatHistory(); history.AddUserMessage(userPrompt); var settings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; var result = await _kernel.InvokePromptAsync( userPrompt, new KernelArguments(new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }), kernel: _kernel); return result.ToString(); } }

当模型决定调用工具时,SK 会自动触发对应插件函数的执行,并把执行结果反馈给模型继续推理。

比如用户输入"北京现在多少度",整个流程就是:用户文本 → SK 调用大模型 → 大模型识别意图并返回工具调用请求 → SK 匹配到 MCP 注册的天气工具 → 构造参数后通过 MCP 协议发到 WeatherMcpServer → Server 执行真实逻辑并返回结果 → SK 把结果回传给大模型 → 大模型生成最终自然语言回答。

这个链路跑通之后,你会发现加一个新的能力域(比如查询订单、查数据库)就跟接插件一样简单:新写一个 MCP Server、注册工具、重启 AgentHost,不用改任何编排代码。

3.5 生产环境部署要点

开发环境跑通只是第一步。上生产之前,有几个配置项我建议提前考虑:

  • MCP Server 的地址不能硬编码。我会放到配置中心,按环境区分开发、测试、生产各自的 Server 地址。
  • 连接超时和重试策略。如果某个 MCP Server 因为网络抖动返回超时,Agent 整个对话就会卡住。我给 MCP 客户端加了超时控制和指数退避重试,默认超时 15 秒,重试 3 次。
  • 健康检查。每个 MCP Server 暴露一个健康检查端点(/health),AgentHost 启动时先做一次全量健康检查,任何一个 Server 不健康就告警而不是盲目调用。
  • 日志追踪。MCP 调用链路在排查问题时候非常需要 trace_id 串联。我在 AgentHost 的 HTTP 请求头里注入 trace_id,MCP Server 端记录同样的 trace_id,方便全链路追踪。

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

4.1 MCP Server 连接失败,网络议题反复出现

我在本地联调时最常遇到的就是"连不上"的问题。排查思路我整理成了固定套路:

先确认 Server 进程是否真的在监听端口(netstat -ano | findstr 端口),再确认客户端连的地址是否匹配(特别注意http和https别混用),最后用 Postman 直接请求 MCP 端点做协议级测试。如果你在本地用 http,线上用了 https,SK 端没有配置对应的证书信任,连接就会静默失败,这类问题排查时优先检查 TLS 配置。

4.2 SK 获取不到工具列表

这种情况通常是 MCP Server 返回的工具列表为空,或者工具 schema 生成失败。我遇到过的原因有两个:

第一个是服务类没有被正确注册到 DI 容器,导致WithTools扫描时找不到实例。第二个是方法的参数类型里有复杂对象,JSON Schema 生成器无法处理。解决方案是避免在工具方法里暴露嵌套过深的复杂参数模型,尽量用简单类型参数,必要的话定义扁平化的请求 DTO。

4.3 工具调用参数解析错误

模型返回的 JSON 参数偶尔会和 schema 不完全匹配,导致 Server 端强转失败。我的处理方式是用JsonElement接收参数,然后手动做宽松解析,并给必填参数提供默认值。宁可参数值不精确,也不要让调用直接抛异常,让链路中断。

但要注意,宽松解析不意味着放弃校验。对于安全敏感的参数(路径、权限指令等),宽松解析之后依然要做严格校验,这件事不能省。

4.4 大模型乱选工具或选错工具

这个问题往往不是模型能力问题,而是工具设计问题。排查步骤我建议按顺序做:

  • 检查每个工具的 description 是否足够具体、是否区分了相近场景。
  • 检查同一 MCP Server 下的工具是否存在语义重叠。
  • 尝试在描述里加上"负面提示",比如"本工具仅用于查询当前天气,不使用于查询未来天气预报"。
  • 如果工具数量很大,考虑按领域拆分成多个 MCP Server,配合 System Prompt 向模型说明何时该用哪个 Server 的工具。

我踩过的一个典型坑是:一个 Server 里同时放了"查询用户信息"和"查询用户订单"两个工具,description 都写得很泛,模型经常混淆。后来我把第二个工具的 description 改成"仅在用户明确提到订单、交易、购买记录时使用",准确率立刻上来了。

4.5 常见问题速查表

症状可能原因快速解法
MCP 客户端连接超时地址错误 / 网络不通 / Server 未启动依次检查端口监听、URL、防火墙、TLS
工具列表为空DI 注册缺失 / schema 生成失败检查服务是否加入容器 / 简化参数类型
调用工具返回 500工具内部异常未捕获在 Server 端增加全局异常中间件并记录日志
模型总是选错工具description 不够具体重写描述,加上触发场景和使用限制
并发调用时数据串了MCP Server 单例存在状态确保 Server 端逻辑无状态,依赖注入改用 Scoped/Transient
流式响应中断HTTP 传输缓冲区/代理配置问题检查反向代理的超时设置和缓冲设置

4.6 调试 MCP 协议的几个小工具

光靠 printf 式日志排查协议问题效率很低。我常用的调试方式有两种:

  • 在 Server 端加 MCP 协议日志中间件,记录每次 JSON-RPC 请求和响应。这个中间件在生产环境可以按 trace_id 采样开启。
  • 用 Postman 或者curl手动构造协议请求,直接验证 Server 行为,把 SK 链路和 Server 链路分开排查。

比如想知道 Server 到底暴露了哪些工具,手动调一下 tools/list 方法即可:

curl -X POST http://localhost:5298/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'

返回的 JSON 数组里每个对象包含name、description、inputSchema,一眼就能看出 schema 是否正确。

5. 经验总结与扩展方向

整套系统跑通之后,我最大的体会是:MCP + SK 的组合把"模型调工具"这件事的工程成本降到了一个新的量级。过去每接一个新工具,要写对接层、写参数映射、写错误处理,现在只需要写工具自身逻辑,协议层全部交给框架。

如果你接下来想在这个方向继续深挖,我建议按这几个方向扩展:

  • 多 Server 编排:尝试把不同领域的能力拆成多个 MCP Server,然后在 SK 端做 Server 分组和按需加载。
  • 动态工具发现:通过配置中心动态管理 MCP Server 列表,实现不重启 Agent 就热加载新工具。
  • MCP 网关:做一个统一的 MCP 网关服务,向上游 Agent 暴露固定入口,向下游代理多个 MCP Server,集中做鉴权、限流、审计。
  • 向量记忆衔接:把 MCP Server 返回的结构化结果写进向量存储,长期积累形成可检索的工具调用记忆,提升 Agent 在重复场景下的效率。

最后再分享一个实践细节:工具调用的结果返回后,尽量做一次"结果精炼",把大段结构化数据压缩成几句话再回传给模型。这样既省 token,又能减少模型被无关字段干扰的概率。这个小改动,在长对话场景下的体验提升非常明显。

大概的代码框架和思路就是这些。真正动手搭的时候,你可能会发现细节比文章里写的多得多,但核心链路跑通之后,剩下的都是熟能生巧的事。祝你顺利。

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

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

立即咨询