ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

.NET + SK + MCP:构建AI Agent工具调用能力层实战

.NET + SK + MCP:构建AI Agent工具调用能力层实战 这两年大模型工具链里最热的一个词恐怕就是 MCPModel Context Protocol模型上下文协议了。如果你在 .NET 生态里做 AI 应用又对 Semantic KernelSK不陌生那应该已经感受到了一个趋势MCP 正在成为 Agent 连接外部工具和数据的标准插座而 SK 作为编排层天生就是承接这波能力的好位置。我最近基于 .NET 8 SK 完整搭了一套 MCP 能力层把工具调用、数据源访问、Agent 编排整个链路理了一遍跑通了从 MCP Server 注册到动态工具调用的全流程。这篇文章不跟你讲 PPT 上的概念直接把我的系统方案、关键设计取舍、核心代码以及踩过的坑摊开来讲。无论你是刚接触 MCP 的新手还是已经在用 SK 做 Agent 的开发者这篇文章都能帮你节省大量试错时间。1. 整体设计思路为什么是 .NET SK MCP1.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 就装对应的 ConnectorModelContextProtocolSK 的 MCP 集成包项目结构我分成了三个独立项目避免所有代码堆在一个工程里Mcp.CapabilityLayer.sln ├── src/AgentHost // SK 编排层Kernel 配置 对话流程 ├── src/WeatherMcpServer // 示例 MCP ServerASP.NET Core 宿主 └── src/Contract // 共享的模型与接口定义把 Server 单独拆出来有两个好处一是它可以独立部署、独立扩缩容二是多个 Agent 应用可以共用同一套能力层服务。3.2 写一个最简 MCP Server示例天气服务以天气查询为例这是最直观的 MCP 工具场景。我新建了一个 ASP.NET Core 空项目然后添加ModelContextProtocol.AspNetCore包。这个包提供了把 MCP 端点挂到 ASP.NET Core 管线的辅助方法。先定义一个气象数据服务类它内部放着模拟数据和真实 HTTP 请求逻辑的替换点public interface IWeatherService { TaskWeatherResult GetCurrentWeatherAsync(string city, string unit celsius, CancellationToken ct default); TaskIReadOnlyListWeatherForecast 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() .GetRequiredServiceIWeatherService(); options.WithTools(weather); });WithTools会自动扫描服务类的公开方法并结合 XML 注释或特性生成对应的 JSON Schema。这就是裸方法秒变 MCP 工具的核心入口。为了在工具说明里给大模型更准确的提示我给方法补充 XML 注释这些注释最终会成为 schema 的 descriptionpublic sealed class WeatherService : IWeatherService { /// summary /// 查询指定城市的当前实时天气情况。适合用户询问现在冷不冷、今天天气如何等场景。 /// 城市字段支持中文城市名如北京、上海或拼音如beijing、shanghai。 /// 温度单位默认为 celsius可选 fahrenheit。 /// /summary public TaskWeatherResult 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 Taskstring 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 暴露一个健康检查端点/healthAgentHost 启动时先做一次全量健康检查任何一个 Server 不健康就告警而不是盲目调用。日志追踪。MCP 调用链路在排查问题时候非常需要 trace_id 串联。我在 AgentHost 的 HTTP 请求头里注入 trace_idMCP Server 端记录同样的 trace_id方便全链路追踪。4. 常见问题与排查技巧实录4.1 MCP Server 连接失败网络议题反复出现我在本地联调时最常遇到的就是连不上的问题。排查思路我整理成了固定套路先确认 Server 进程是否真的在监听端口netstat -ano | findstr 端口再确认客户端连的地址是否匹配特别注意http和https别混用最后用 Postman 直接请求 MCP 端点做协议级测试。如果你在本地用 http线上用了 httpsSK 端没有配置对应的证书信任连接就会静默失败这类问题排查时优先检查 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又能减少模型被无关字段干扰的概率。这个小改动在长对话场景下的体验提升非常明显。大概的代码框架和思路就是这些。真正动手搭的时候你可能会发现细节比文章里写的多得多但核心链路跑通之后剩下的都是熟能生巧的事。祝你顺利。
返回列表