1. 为什么要在本地把 DeepSeek-V3、MCP 和 SemanticKernel 串起来如果你正在找一个能落地的智能问答应用方案DeepSeek-V3 负责推理、MCP 负责接工具、SemanticKernel 负责编排技能管线这套组合基本就是当前最顺手的本地开发链路。它适合谁适合已经会写一点 C#、想让模型真正去查数据库和调搜索接口、而不是只会在聊天框里编答案的开发者。我先把三个角色说清楚。DeepSeek-V3 是推理大脑负责理解你的问题、决定要不要调工具、把工具返回的结果组织成人话。MCPModel Context Protocol是一套开放协议让 LLM 应用和外部数据源、工具之间用标准方式对接你可以把它理解成“模型和工具之间的 USB-C 接口”插上就能用不用为每个工具单独写一套胶水代码。SemanticKernel 则是编排层它把模型、插件、函数调用串成一条管线让模型能自动决定调用哪个 KernelFunction。很多人会问 MCP 和 Function Calling 到底差在哪。简单说Function Calling 是“模型输出一个函数名和参数”MCP 是在这个基础上扩展出来的更完整的协议它管的不只是调用还包括上下文获取、工具发现、客户端-服务器架构。MCP 有 Hosts想访问数据的程序、Clients和服务器 1:1 连接的协议客户端、Servers暴露具体功能的轻量程序这几个角色数据可以留在本地或受控环境里敏感信息不用往外送。这篇教程要做的是在本地构建一个能调用搜索与数据库的智能问答应用。整条链路是你用 TaoToken 的统一 Key 接入 DeepSeek-V3 作为推理模型通过 MCP 协议连接外部工具服务器再用 SemanticKernel 把 MCP 工具映射成 KernelFunction 并编排成技能管线。我会给出 TaoToken 统一 Key 的 Base URL 和 auth.json 可复制配置然后演示一次端到端调用验证确认模型、MCP 工具和 Kernel 函数都正常返回。整个过程不需要你去折腾多个平台的账号一个 Key 就能把模型侧打通。2. TaoToken 统一 Key 的前置准备与 auth.json 配置在写代码之前先把模型侧的接入准备好。TaoToken 的作用是给你一个统一的入口你不用分别去不同平台申请 Key、记不同的 Base URL一个 Key 就能调用 DeepSeek-V3。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。你需要先拿到 API Key。登录后进入控制台在 API Keys 页面创建一个新的 Key复制出来保存好。这个 Key 后面会同时用在两处一处是 SemanticKernel 的 OpenAI 兼容连接器一处是 Codex 风格的 auth.json 配置文件。如果你用的是 Claude Code 或者类似的编码工具也可以在文档页找到对应的接入说明。先看 auth.json 的写法。很多工具比如 Codex 风格的 CLI会读取这个文件来获取模型凭证路径通常在用户目录下的 .codex/auth.json 或者项目根目录的 .taotoken/auth.json。内容结构如下你可以直接复制后替换成自己的 Key{ base_url: https://taotoken.net/api, api_key: sk-你的TaoTokenKey, model: DeepSeek-V3, provider: openai-compatible }这里三个字段要写全Base URL 是 https://taotoken.net/api Key 是你刚创建的那串Model ID 写 DeepSeek-V3。注意 Base URL 不要带 UTM 参数API 调用走的是纯净地址。如果你用的是 TOML 格式的配置比如某些 Rust 工具链等价写法是[model] base_url https://taotoken.net/api api_key sk-你的TaoTokenKey model_id DeepSeek-V3如果你在 Claude Code 里接入settings 片段可以这样写放在 ~/.claude/settings.json 或者项目级 .claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoTokenKey, ANTHROPIC_MODEL: DeepSeek-V3 } }这里要提醒一句Base URL、Key、Model ID 这三件套必须同时出现且一致缺一个就会出现 401 或者模型找不到的报错。我见过有人只填了 Key 没改 Base URL结果请求打到了默认地址一直报 local proxy failed。所以配置完先别急着写业务代码用一条 curl 验证一下模型侧是否通curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的TaoTokenKey \ -H Content-Type: application/json \ -d { model: DeepSeek-V3, messages: [{role: user, content: 你好}] }如果返回里有 choices 字段和正常的 content说明模型侧已经打通。这一步很关键因为后面 SemanticKernel 报错时你要能区分是模型侧的问题还是 MCP 工具侧的问题。模型侧通了再往下走 MCP 和 Kernel 的编排。3. 可复制的 MCP SemanticKernel 配置与代码现在进入核心部分。我们要创建一个控制台项目把 MCP 工具映射成 SemanticKernel 的 KernelFunction然后用 DeepSeek-V3 驱动自动调用。先建项目dotnet new console -n McpClient cd McpClient然后编辑 csproj加入三个依赖包。版本号按你本地实际能拉到的来这里给一组可用的ItemGroup PackageReference IncludeMicrosoft.Extensions.Hosting Version9.0.3 / PackageReference IncludeMicrosoft.SemanticKernel Version1.44.0 / PackageReference IncludeModelContextProtocol Version0.1.0-preview.4 / /ItemGroupSemanticKernel 默认不认识 MCP 的工具所以要先写扩展类做转换。核心思路是MCP 服务器通过 ListToolsAsync 暴露工具列表每个工具有 JsonSchema 描述参数我们把它转成 KernelFunction 的参数元数据调用时再把 KernelArguments 转回 MCP 需要的字典格式。先定义两个描述 JSON Schema 的类internal class JsonSchema { [JsonPropertyName(type)] public string Type { get; set; } object; [JsonPropertyName(properties)] public Dictionarystring, JsonSchemaProperty? Properties { get; set; } [JsonPropertyName(required)] public Liststring? Required { get; set; } } internal class JsonSchemaProperty { [JsonPropertyName(type)] public string Type { get; set; } string.Empty; [JsonPropertyName(description)] public string? Description { get; set; } string.Empty; }然后是转换扩展类把 MCP 工具映射成 KernelFunctioninternal static class ModelContextProtocolExtensions { internal static async TaskIReadOnlyListKernelFunction MapToFunctionsAsync( this IMcpClient mcpClient, CancellationToken cancellationToken default) { var functions new ListKernelFunction(); foreach (var tool in await mcpClient.ListToolsAsync(cancellationToken).ConfigureAwait(false)) { functions.Add(tool.ToKernelFunction(mcpClient, cancellationToken)); } return functions; } private static KernelFunction ToKernelFunction(this McpClientTool tool, IMcpClient mcpClient, CancellationToken cancellationToken) { async Taskstring InvokeToolAsync(Kernel kernel, KernelFunction function, KernelArguments arguments, CancellationToken ct) { Dictionarystring, object? mcpArguments []; foreach (var arg in arguments) { if (arg.Value is not null) { mcpArguments[arg.Key] function.ToArgumentValue(arg.Key, arg.Value); } } var result await mcpClient.CallToolAsync( tool.Name, mcpArguments.AsReadOnly(), cancellationToken: ct).ConfigureAwait(false); return string.Join(\n, result.Content .Where(c c.Type text) .Select(c c.Text)); } return KernelFunctionFactory.CreateFromMethod( method: InvokeToolAsync, functionName: tool.Name, description: tool.Description, parameters: tool.ToParameters(), returnParameter: ToReturnParameter()); } private static object ToArgumentValue(this KernelFunction function, string name, object value) { var parameterType function.Metadata.Parameters .FirstOrDefault(p p.Name name)?.ParameterType; if (parameterType null) return value; if (Nullable.GetUnderlyingType(parameterType) typeof(int)) return Convert.ToInt32(value); if (Nullable.GetUnderlyingType(parameterType) typeof(double)) return Convert.ToDouble(value); if (Nullable.GetUnderlyingType(parameterType) typeof(bool)) return Convert.ToBoolean(value); return value; } private static ListKernelParameterMetadata? ToParameters(this McpClientTool tool) { var inputSchema JsonSerializer.DeserializeJsonSchema(tool.JsonSchema.GetRawText()); var properties inputSchema?.Properties; if (properties null) return null; HashSetstring requiredProperties [.. inputSchema!.Required ?? []]; return properties.Select(kvp new KernelParameterMetadata(kvp.Key) { Description kvp.Value.Description, ParameterType ConvertParameterDataType(kvp.Value, requiredProperties.Contains(kvp.Key)), IsRequired requiredProperties.Contains(kvp.Key) }).ToList(); } private static KernelReturnParameterMetadata ToReturnParameter() new() { ParameterType typeof(string) }; private static Type ConvertParameterDataType(JsonSchemaProperty property, bool required) { var type property.Type switch { string typeof(string), integer typeof(int), number typeof(double), boolean typeof(bool), array typeof(Liststring), object typeof(Dictionarystring, object), _ typeof(object) }; return !required type.IsValueType ? typeof(Nullable).MakeGenericType(type) : type; } }接着写 Kernel 扩展把 MCP 服务器上的工具批量注册成插件。这里用 SSE 传输方式连接 MCP 服务器public static class KernelExtensions { private static readonly ConcurrentDictionarystring, IKernelBuilderPlugins SseMap new(); public static async TaskIKernelBuilderPlugins AddMcpFunctionsFromSseServerAsync( this IKernelBuilderPlugins plugins, string endpoint, string serverName, CancellationToken cancellationToken default) { var key ToSafePluginName(serverName); if (SseMap.TryGetValue(key, out var sseKernelPlugin)) return sseKernelPlugin; var mcpClient await GetClientAsync(serverName, endpoint, null, null, cancellationToken) .ConfigureAwait(false); var functions await mcpClient.MapToFunctionsAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); cancellationToken.Register(() mcpClient.DisposeAsync() .ConfigureAwait(false).GetAwaiter().GetResult()); sseKernelPlugin plugins.AddFromFunctions(key, functions); return SseMap[key] sseKernelPlugin; } private static async TaskIMcpClient GetClientAsync(string serverName, string? endpoint, Dictionarystring, string? transportOptions, ILoggerFactory? loggerFactory, CancellationToken cancellationToken) { var transportType !string.IsNullOrEmpty(endpoint) ? TransportTypes.Sse : TransportTypes.StdIo; McpClientOptions options new() { ClientInfo new() { Name ${serverName} {transportType}Client, Version 1.0.0 } }; var config new McpServerConfig { Id serverName.ToLowerInvariant(), Name serverName, Location endpoint, TransportType transportType, TransportOptions transportOptions }; return await McpClientFactory.CreateAsync(config, options, loggerFactory: loggerFactory ?? NullLoggerFactory.Instance, cancellationToken: cancellationToken); } private static string ToSafePluginName(string serverName) Regex.Replace(serverName, [^\w], _); }最后是 Program.cs把 TaoToken 的 Base URL 和 Key 填进去连接 MCP 服务器启动对话循环using McpClient; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using ChatMessageContent Microsoft.SemanticKernel.ChatMessageContent; #pragma warning disable SKEXP0010 var builder Host.CreateEmptyApplicationBuilder(settings: null); builder.Configuration.AddEnvironmentVariables().AddUserSecretsProgram(); var kernelBuilder builder.Services.AddKernel() .AddOpenAIChatCompletion( DeepSeek-V3, new Uri(https://taotoken.net/api), sk-你的TaoTokenKey); await kernelBuilder.Plugins.AddMcpFunctionsFromSseServerAsync( http://你的MCPServerIP:端口/sse, token); Console.ForegroundColor ConsoleColor.Green; Console.WriteLine(MCP Client Started!); Console.ResetColor(); var app builder.Build(); var kernel app.Services.GetServiceKernel(); var chatCompletion app.Services.GetServiceIChatCompletionService(); PromptForInput(); while (Console.ReadLine() is string query !exit.Equals(query, StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrWhiteSpace(query)) { PromptForInput(); continue; } var history new ChatHistory { new ChatMessageContent(AuthorRole.System, 下面如果需要计算两个数的和请使用我提供的工具。), new ChatMessageContent(AuthorRole.User, query) }; await foreach (var message in chatCompletion?.GetStreamingChatMessageContentsAsync( history, new OpenAIPromptExecutionSettings() { ToolCallBehavior ToolCallBehavior.AutoInvokeKernelFunctions, }, kernel)) { Console.Write(message.Content); } Console.WriteLine(); PromptForInput(); } static void PromptForInput() { Console.WriteLine(Enter a command (or exit to quit):); Console.ForegroundColor ConsoleColor.Cyan; Console.Write( ); Console.ResetColor(); }注意这里 Base URL 写的是 https://taotoken.net/api Model ID 是 DeepSeek-V3Key 换成你自己的。这三件套和前面 auth.json 里保持一致不要一个地方写 DeepSeek-V3 另一个地方写别的模型名。4. 端到端验证确认模型、MCP 工具与 Kernel 函数都正常返回代码写完了现在做一次完整的端到端验证。验证的目标是确认三件事DeepSeek-V3 能正常推理、MCP 工具能被发现并调用、Kernel 函数能正确执行并返回结果。第一步先启动你的 MCP 服务器。假设你有一个暴露了“两数相加”工具的 MCP 服务器它监听在 http://127.0.0.1:3001/sse 。启动后你会看到它打印出已注册的工具列表。如果你还没有 MCP 服务器可以用官方示例或者自己写一个最简单的只要它通过 SSE 暴露一个 add 工具即可。第二步在 MCP 服务器的 add 函数里打个断点或者加一行日志。这样当客户端调用时你能在服务器侧看到请求进来确认调用链路真的走通了而不是模型自己编了个答案。第三步运行 MCP 客户端dotnet run你会看到绿色的 “MCP Client Started!” 和提示符。输入11?预期结果是客户端先把问题发给 DeepSeek-V3模型判断需要调用工具SemanticKernel 通过 AutoInvokeKernelFunctions 自动调用 MCP 的 add 工具MCP 服务器执行加法并返回结果模型再把结果组织成自然语言输出。你会在控制台看到类似“11 等于 2”的回复同时在 MCP 服务器侧看到 add 被调用的日志。如果一切正常你还可以试一个更复杂的查询比如“帮我查一下数据库里用户表有多少条记录”前提是你的 MCP 服务器暴露了对应的数据库查询工具。模型会自动选择正确的工具SemanticKernel 负责把参数传过去MCP 服务器执行查询并返回。这就是整条链路的价值模型负责决策MCP 负责标准化工具接入Kernel 负责编排。验证时建议按这个顺序排查先确认模型侧通用前面的 curl再确认 MCP 服务器单独能跑用 MCP 官方的 inspector 工具最后确认客户端能连上。三层都通了端到端就不会有问题。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth实际跑的时候报错基本集中在这几类。我按真实遇到的顺序列一下你对照着看。401 Unauthorized 是最常见的。原因通常是 Key 没填对、Key 过期、或者 Base URL 和 Key 不匹配。检查 auth.json 和 Program.cs 里的 Key 是否一致Base URL 是否是 https://taotoken.net/api 。注意不要有多余空格也不要漏掉 Bearer 前缀curl 里需要SDK 里通常自动加。如果用的是环境变量确认变量名和代码里读的一致。local proxy failed 这个报错通常出现在你本地配了代理但代理没启动或者地址不对。解决办法是检查系统代理设置或者在代码里显式指定 HttpClient 不走代理。如果你在 auth.json 里配了 proxy 字段先去掉试试。这个报错和模型本身无关是网络层的问题。reading choices 报错一般长这样“error reading choices: unexpected end of JSON input”。这说明请求发出去了但返回的不是合法 JSON。常见原因是 Base URL 写成了 https://taotoken.net/api/v1 而 SDK 又自动拼了 /v1导致路径变成 /v1/v1/chat/completions。检查你的 Base URLOpenAI 兼容连接器通常只需要写到 https://taotoken.net/api 让它自己拼 /v1。另一个原因是模型名写错服务端返回了错误页而不是 JSON。OAuth 相关报错出现在你用 Claude Code 或类似工具时。如果你在 settings.json 里配了 ANTHROPIC_BASE_URL 和 ANTHROPIC_API_KEY但工具仍然走 OAuth 流程说明它没读到你的配置。检查配置文件路径是否正确以及环境变量是否被覆盖。有些工具需要你在启动时加 --no-oauth 或者类似的参数。确认三件套Base URL、Key、Model ID都写全了缺一个就会回退到默认的 OAuth 流程。还有一个容易忽略的MCP 连接超时。如果客户端启动后卡在 “MCP Client Started!” 之前多半是 MCP 服务器地址不对或者没启动。检查 IP 和端口确认服务器监听的地址和客户端填的一致。SSE 端点通常是 /sse不要漏掉。排查时记住一个原则先隔离模型侧再隔离工具侧。模型侧用 curl 验证工具侧用 MCP inspector 验证两边都通了再合起来跑。这样能快速定位问题在哪一层。6. 把这条链路用起来从验证到长期编码与 Agent走到这里你已经完成了 DeepSeek-V3 MCP SemanticKernel 的端到端验证。模型能推理MCP 工具能被调用Kernel 函数能正常返回这三件事都确认过了。接下来你可以把这条链路用到实际场景里。如果你只是想做模型对话验证可以直接用模型对话页面快速试不同 prompt 的效果不用每次都跑本地客户端。如果你要长期做编码或者构建 Agent建议用 Coding Plan它更适合持续性的开发任务省去反复配置的麻烦。接入过程中遇到 Key 或者配置问题去 API Keys 页面管理你的凭证接入细节看接入文档。我自己的习惯是把 auth.json 和 Program.cs 里的配置抽成环境变量这样切换环境时不用改代码。另外 MCP 服务器的工具描述要写清楚模型能不能选对工具很大程度上取决于 description 写得好不好。工具参数的类型也要和 JsonSchema 里声明的一致否则转换时会出错。最后留一个实用技巧在 MCP 服务器的每个工具入口加一行日志记录调用时间和参数。这样当模型行为不符合预期时你能快速判断是模型没选对工具还是工具执行出了问题。这条链路一旦跑通后面加搜索、加数据库、加自定义技能都是往 MCP 服务器里加工具的事客户端和 Kernel 层基本不用动。
阅读完成 · 觉得有帮助?