:理论 × 代码 × 企业案例——用 TaoToken 统一 Key 打通 OpenAI 与 MySQL 数据链路)
1. 为什么 .NET 团队需要 Agent Skills从工具调用到流程封装很多 .NET 团队在 2024 年做 AI 功能时走的都是同一条路先接一个 OpenAI 兼容的 ChatClient然后在业务代码里写一堆 if-else 判断用户意图再手动拼 SQL、调接口、格式化结果。代码能跑但每加一个业务场景就要改一次主流程测试成本高模型换个版本行为还可能漂移。Agent Skills 想解决的就是这个问题。它把「某个领域该怎么做」从主程序里抽出来变成一个独立的、可版本管理的文件夹里面有元数据叫什么、什么时候用、有指令SKILL.md用自然语言写清楚步骤和约束、有资源脚本、模板、参考文档。主程序只负责发现技能、按需加载、把技能指令塞进系统提示词剩下的交给模型去规划。对 .NET 团队来说这个模式特别契合。.NET 本身有强类型、依赖注入、配置系统天然适合做「技能注册中心」而 MySQL 这类企业数据源又正好是 Skills 最典型的落地场景——把「查员工薪资」「统计部门人数」这类高频分析需求封装成模型能自主调用的能力而不是每次都在代码里硬编码 SQL。我试过在一个内部报表项目里用这套思路重构原来 800 行的意图判断逻辑最后压缩成 3 个 Skill 文件夹加一个通用执行器。模型换版本时只需要调整 SKILL.md 里的措辞主程序一行不动。这一篇会按「理论拆解 → 可运行代码 → 企业级 MySQL 案例 → 排错」的顺序走所有配置和代码都可以直接复制。统一 Key 的部分用 TaoToken 来做避免 OpenAI、MySQL 工具、脚本执行器各自散落一套密钥。2. TaoToken 前置准备统一 Key 与 API 通道在写代码之前先把密钥管理这件事理清楚。.NET 项目里最容易失控的就是环境变量开发机一套、CI 一套、生产一套OpenAI Key、数据库密码、脚本执行权限混在一起出问题很难定位。TaoToken 在这里的角色是一个统一的 API 通道。你只需要在它那边生成一个 Key然后在 .NET 的 appsettings 或环境变量里配置 Base URL 和 Key所有走 OpenAI 兼容协议的调用都从这里出去。好处是换模型、换供应商、加限流都只改一个地方。具体操作路径打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册账号。进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建 API Key。在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 复制你的 Key格式通常是sk-开头。需要确认模型 ID 时去模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 试一下或者查接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。API 端点统一用 https://taotoken.net/api注意这个地址后面不加 UTM 参数直接作为 Base URL 填进代码。这里有个关键点TaoToken 的 Base URL 是https://taotoken.net/api而 OpenAI SDK 通常会在后面拼/v1/chat/completions。所以你在 .NET 里配置 Endpoint 时要确认最终请求路径是https://taotoken.net/api/v1/chat/completions。如果 SDK 默认拼/v1那 Endpoint 就填https://taotoken.net/api如果 SDK 要求你填完整前缀就填https://taotoken.net/api/v1。这个细节后面排错章节会专门讲。如果你打算长期做编码类 Agent可以顺带看一下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite它更适合高频调用场景。Claude Code 相关的接入说明在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite。3. 可复制配置appsettings.json 与 Skill 目录结构这一节给出完整的配置文件。.NET 8 项目里我习惯把模型配置和数据库配置分开避免混在一起。先建项目dotnet new console -f net8.0 -n SqlAgentSkills cd SqlAgentSkills dotnet add package OpenAI dotnet add package MySql.Data dotnet add package Microsoft.Extensions.Configuration.Json dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables然后是appsettings.json{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-你的TaoToken密钥, ModelId: gpt-4o-mini }, MySql: { ConnectionString: serverlocalhost;port3306;databasesqlagent;uidroot;pwd你的密码;SslModeDisabled;AllowPublicKeyRetrievalTrue; }, Agent: { SkillsDirectory: ./Skills, MaxTurns: 10, MaxRows: 1000 } }注意ModelId这一项它必须和 TaoToken 控制台里可用的模型 ID 一致。如果你不确定先去模型对话页试一次把返回里用的模型名记下来。Cline MCP 或 Codex 的 auth.json 场景下同样需要 Base URL、Key、Model ID 三件套齐全缺一个都会在请求阶段报错。Skill 目录结构按官方规范来Skills/ └── mysql-employees-analyst/ ├── skill.json ├── SKILL.md └── scripts/ └── execute_sql.pyskill.json只放元数据{ Name: mysql-employees-analyst, Description: 分析MySQL employees示例库中的员工、薪资、部门数据将中文业务问题转为SELECT语句并输出分析报告 }SKILL.md是核心内容要包含职责边界、数据库 Schema、SQL 生成规则、工具调用方式和输出格式。下面是一个精简但可用的版本--- name: mysql-employees-analyst description: 分析MySQL employees示例库将中文问题转为SELECT并输出报告 --- # MySQL Employees 数据分析 Skill 你必须完成闭环理解问题 - 生成SELECT - 调用 execute_sql 工具 - 基于真实结果输出报告。 ## 数据库背景 核心表 - employees (emp_no, first_name, last_name) - departments (dept_no, dept_name) - dept_emp (emp_no, dept_no, from_date, to_date) - salaries (emp_no, salary, from_date, to_date) - titles (emp_no, title, from_date, to_date) 当前日期是 {current_date}查询在职员工请使用 to_date 9999-01-01。 ## 职责边界 1. 只生成 SELECT 语句禁止任何写操作。 2. 每次只生成并执行一条 SQL复杂问题分多轮完成。 3. 禁止编造查询结果必须先执行再分析。 ## SQL 生成规则 - 日期过滤在职记录用 to_date 9999-01-01。 - 聚合函数必须给列起别名。 - 字符串匹配注意中文通配符。 ## 工具使用 工具名execute_sql 参数sql_query字符串要执行的SELECT语句 返回JSON格式的查询结果或错误信息。 ## 输出要求 - 查询出错时解释错误并尝试修正。 - 查询成功时用 Markdown 表格输出结构化报告并附上使用的 SQL。scripts/execute_sql.py负责真正执行 SQL接收标准输入 JSON返回标准输出 JSON。核心逻辑是只允许 SELECT、自动加 LIMIT、把 datetime 和 Decimal 转成可序列化格式。这个脚本的完整实现比较长关键点在于用mysql.connector连接用cursor(dictionaryTrue)拿字典结果然后逐行转换特殊类型。数据库初始化脚本可以直接用 employees 示例库的建表语句重点是to_date 9999-01-01这个约定它代表「当前有效记录」。4. 验证请求从 .NET 调用到端到端成功结果配置齐了接下来写 .NET 侧的 Agent 主循环。核心思路是读取 appsettings初始化 OpenAIClient加载 SKILL.md注册 execute_sql 工具然后进入多轮对话循环。先看配置加载和客户端初始化using Microsoft.Extensions.Configuration; using OpenAI; using OpenAI.Chat; using System.ClientModel; using System.Text; using System.Text.Json; Console.OutputEncoding Encoding.UTF8; Console.InputEncoding Encoding.UTF8; var config new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(appsettings.json, optional: false) .AddEnvironmentVariables() .Build(); string baseUrl config[TaoToken:BaseUrl] ?? throw new InvalidOperationException(缺少 TaoToken:BaseUrl); string apiKey config[TaoToken:ApiKey] ?? throw new InvalidOperationException(缺少 TaoToken:ApiKey); string modelId config[TaoToken:ModelId] ?? throw new InvalidOperationException(缺少 TaoToken:ModelId); string connString config[MySql:ConnectionString] ?? throw new InvalidOperationException(缺少 MySql:ConnectionString); string skillsDir config[Agent:SkillsDirectory] ?? ./Skills; int maxTurns int.Parse(config[Agent:MaxTurns] ?? 10); int maxRows int.Parse(config[Agent:MaxRows] ?? 1000); var client new OpenAIClient( new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint new Uri(baseUrl) } ); var chatClient client.GetChatClient(modelId);这里Endpoint填的是https://taotoken.net/apiSDK 会自动拼/v1/chat/completions。如果你的 SDK 版本行为不同后面排错章节会讲怎么确认。接下来加载 Skill 指令string skillPath Path.Combine(skillsDir, mysql-employees-analyst, SKILL.md); string skillInstruction await File.ReadAllTextAsync(skillPath); skillInstruction skillInstruction.Replace({current_date}, DateTime.Now.ToString(yyyy-MM-dd));注册工具并进入循环var executeSqlTool ChatTool.CreateFunctionTool( functionName: execute_sql, functionDescription: 在MySQL employees库上执行一条SELECT语句返回JSON结果。, functionParameters: BinaryData.FromObjectAsJson(new { type object, properties new { sql_query new { type string, description 要执行的SELECT语句 } }, required new[] { sql_query } }, new JsonSerializerOptions { PropertyNamingPolicy JsonNamingPolicy.CamelCase }) ); var chatOptions new ChatCompletionOptions { Tools { executeSqlTool } }; var messages new ListChatMessage { new SystemChatMessage(skillInstruction), new UserChatMessage(统计公司员工总数并列出各部门人数) }; bool requiresAction true; int turns 0; while (requiresAction turns maxTurns) { requiresAction false; ChatCompletion completion await chatClient.CompleteChatAsync(messages, chatOptions); switch (completion.FinishReason) { case ChatFinishReason.Stop: Console.WriteLine($AI: {completion.Content[0].Text}); messages.Add(new AssistantChatMessage(completion)); break; case ChatFinishReason.ToolCalls: messages.Add(new AssistantChatMessage(completion)); foreach (var toolCall in completion.ToolCalls) { if (toolCall.FunctionName ! execute_sql) continue; using JsonDocument args JsonDocument.Parse(toolCall.FunctionArguments); string sql args.RootElement.GetProperty(sql_query).GetString() ?? ; Console.WriteLine($[Agent] 执行SQL: {sql}); string resultJson await ExecuteSqlAsync(connString, sql, maxRows); messages.Add(new ToolChatMessage(toolCall.Id, resultJson)); requiresAction true; } break; default: Console.WriteLine($[Agent] 结束原因: {completion.FinishReason}); requiresAction false; break; } }ExecuteSqlAsync的实现要点先校验 SQL 必须以 SELECT 开头然后打开连接、执行、把结果序列化成 JSON。如果出错返回{ success: false, error: ... }让模型看到错误信息后自行修正。async Taskstring ExecuteSqlAsync(string connectionString, string sql, int limit) { if (!sql.TrimStart().StartsWith(SELECT, StringComparison.OrdinalIgnoreCase)) return JsonSerializer.Serialize(new { success false, error 只允许SELECT查询 }); if (!sql.ToUpper().Contains(LIMIT)) sql $ LIMIT {limit}; var rows new ListDictionarystring, object?(); try { using var conn new MySql.Data.MySqlClient.MySqlConnection(connectionString); await conn.OpenAsync(); using var cmd new MySql.Data.MySqlClient.MySqlCommand(sql, conn); using var reader await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var row new Dictionarystring, object?(); for (int i 0; i reader.FieldCount; i) row[reader.GetName(i)] reader.GetValue(i); rows.Add(row); } return JsonSerializer.Serialize(new { success true, data rows, rowCount rows.Count, sql }); } catch (Exception ex) { return JsonSerializer.Serialize(new { success false, error ex.Message, sql }); } }运行程序输入「统计公司员工总数并列出各部门人数」你会看到类似输出[Agent] 执行SQL: SELECT COUNT(*) AS total FROM employees LIMIT 1000 [Agent] 执行SQL: SELECT d.dept_name, COUNT(*) AS cnt FROM dept_emp de JOIN departments d ON de.dept_no d.dept_no WHERE de.to_date 9999-01-01 GROUP BY d.dept_name LIMIT 1000 AI: 公司员工总数为 10 人。各部门人数如下 | 部门 | 人数 | |------|------| | 技术部 | 2 | | 市场部 | 2 | | 人事部 | 2 | | 财务部 | 2 | | 运营部 | 2 |这就是一次完整的端到端验证模型读 SKILL.md自主生成 SQL调用工具拿到真实数据输出报告。整个过程主程序没有写任何业务判断。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth实际跑的时候最容易卡在几个固定位置。下面按报错原文对照排查。401 Unauthorized / invalid_api_key最常见的原因是 Key 没读到或者 Base URL 拼错了。先确认appsettings.json里的ApiKey是sk-开头且没有多余空格。然后检查Endpoint和 SDK 的拼接行为如果 SDK 自动加/v1Endpoint 填https://taotoken.net/api如果 SDK 要求完整路径填https://taotoken.net/api/v1。可以用 curl 直接验证curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的密钥 \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:hi}]}如果 curl 通、代码不通就是 SDK 配置问题如果 curl 也 401就是 Key 或模型 ID 问题。local proxy failed / connection refused这个报错通常出现在你本地配了代理但代理没启动或者 .NET 的 HttpClient 走了系统代理。检查环境变量HTTP_PROXY、HTTPS_PROXY是否指向了一个不可用的地址。在 .NET 里可以显式禁用代理var handler new HttpClientHandler { UseProxy false };如果你用的是 Cline MCP 或类似工具它可能自己维护了一套代理配置需要在工具的 settings 里单独关掉。reading choices / choices 字段为空这个报错说明请求发出去了但返回体里没有choices。常见原因有三个一是模型 ID 写错服务端返回了错误结构二是请求体里messages为空三是流式和非流式模式混用。先打印原始响应体确认var response await chatClient.CompleteChatAsync(messages, chatOptions); Console.WriteLine(response.GetRawResponse().Content.ToString());如果返回的是{error: {...}}按错误信息处理如果是空对象检查messages是否至少有一条。OAuth / auth.json 相关报错如果你在用 Codex 或 Claude Code 这类工具它们可能要求auth.json里同时有 Base URL、Key、Model ID。缺任何一个都会报 OAuth 失败。以 Codex 为例auth.json的结构大致是{ base_url: https://taotoken.net/api, api_key: sk-你的密钥, model: gpt-4o-mini }注意base_url不要带/v1让工具自己拼。如果工具文档要求带就按文档来。三件套齐全后重启工具再试。MySQL 连接报 SSL 错误SslModeDisabled和AllowPublicKeyRetrievalTrue这两个参数在本地开发时基本是必须的。如果还报错检查 MySQL 用户是否有远程访问权限以及端口是否被防火墙挡住。Python 脚本执行超时execute_sql.py如果卡住通常是数据库连接没释放。确保脚本里用了try/finally关闭连接并且 .NET 侧设置了合理的超时时间。默认 30 秒够用复杂查询可以调到 60 秒。6. 语义一致 CTA把统一 Key 用到长期编码场景走到这里你已经有了一个能跑的 .NET Agent Skills 骨架TaoToken 统一 Key 管模型调用SKILL.md 管业务规则execute_sql 管数据访问。接下来最自然的延伸是把它用到日常编码和长期 Agent 场景。如果你主要做排障和接入建议先把 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 和接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 过一遍把 Base URL、Key、Model ID 三件套固定下来。验证模型行为时用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 快速试不用每次都跑完整项目。如果你打算把 Agent 用在长期编码、代码审查、自动化报表这类高频场景Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 会更合适它的调用配额和稳定性针对这类负载做了优化。Claude Code 用户可以直接看 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite里面有完整的接入步骤。最后给一个实用建议把 SKILL.md 当成代码来管理。每次模型行为不符合预期先改 SKILL.md 里的措辞和约束而不是改主程序。我踩过的坑是一开始总想在 C# 里加判断结果越加越乱后来把规则全部下沉到 SKILL.md主程序只保留工具注册和循环维护成本直接降了一个量级。