
Microsoft Agent Framework 的 Skills 机制能让 Agent 按需加载技能包并执行 Python 脚本。原文用 meeting-notes 做演示但照着做之前有两件事没交代C# 宿主默认不会执行任何脚本Agent 的模型通道也没说从哪配。这篇把两条线并到一起先用 TaoToken 统一模型接入——打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 创建 API Key把模型 Base URL 填成 https://taotoken.net/api再按原文思路实现 PythonRunner把执行 .py 的能力暴露成 execute_python 工具。配通之后Agent 会自己完成发现技能包、读取 SKILL.md、决定调用工具、返回脚本结果这一整条链路每一次模型推理产生的消耗都统一记在 TaoToken 账号里。1. Skills 的定位先搞清楚技能包解决了什么问题1.1 技能包不是代码插件是「指令 脚本 资源」Agent Skills 的官方定义是一组可移植的指令、脚本和资源用来给 Agent 增加特定能力。听起来有点抽象实际上拆开看就三样东西一份 SKILL.md写给模型看的说明书告诉它这个技能是干什么的、该调什么工具、参数怎么传若干 Python 脚本或资源文件真正干活的载荷一个约定的目录结构让 Agent 在启动时能自动发现这个技能存在。这个设计的好处是技能包可以像文件一样复制到任意 Agent 项目里Agent 运行时通过目录扫描就能发现自己「会什么」不需要每次把能力写死在程序里。1.2 本文的作战目标让 Agent 自己决定「跑哪个脚本」原文的示例场景是会议纪要。用户输入一句「帮我获取会议纪要」Agent 需要自己完成下面的推理链发现本地存在 meeting-notes 这个技能包加载它的 SKILL.md阅读里面的操作说明根据说明判断应该调用名为 execute_python 的工具传参meeting-notes/scripts/GetMeetingNotes.py给这个工具拿到 Python 脚本打印出来的文本整理后回复用户。问题在于在 C# 下第 4 步不会自动发生。官方文档白纸黑字写着 Script execution is not yet supported in C#也就是说 Agent 在看到.py文件后不会自己去启动 Python 进程。解决办法是自己实现一个执行器再把它注册成 Tool让 SKILL.md 的指令最终落到这个 Tool 上。2. skill 目录与 SKILL.md把「技能说明书」和脚本摆到约定位置2.1 标准目录结构先按 Agent Framework 的约定建目录。技能包名字叫 meeting-notes那么目录名必须也叫 meeting-notes不能图省事改成 meetings 或者 meetingNotes否则 Agent 扫描时匹配不上。skills/ └── meeting-notes/ ├── SKILL.md └── scripts/ └── GetMeetingNotes.py这个结构本身不复杂但路径一致性会直接影响后面的工具调用。因为最终传给 execute_python 的是相对路径meeting-notes/scripts/GetMeetingNotes.py执行器要在宿主程序目录下找到 skills 子目录再做路径拼接任一层目录名对不上都会抛文件不存在。2.2 SKILL.md 的三个组成部分SKILL.md 是技能包的入口也是 Agent 唯一的「操作手册」。它由 frontmatter 和正文 Markdown 组成。frontmatter 里的 name 和 description 是用来做技能匹配的正文部分才是真正指导模型调用工具的指令。--- name: meeting-notes description: 与会议纪要相关的 Python 脚本集合包含获取纪要文本的脚本 --- # meeting-notes 使用说明 本技能提供会议纪要相关脚本。脚本清单如下 - GetMeetingNotes.py返回当前会议纪要文本 调用方式把脚本相对路径 meeting-notes/scripts/GetMeetingNotes.py 作为参数交给 execute_python 工具执行。几个关键点name 必须和目录名一致否则技能发现阶段会把文件夹和技能名当成两个东西description 是模型判断「要不要用这个技能」的主要依据太泛会导致误匹配太窄会导致该用时不用正文里尽量写清楚「用哪个工具、传什么参数」模型读到这句话才会把路径字符串和 execute_python 关联起来。2.3 GetMeetingNotes.py脚本只需要 main() 和输出Python 脚本本身保持简单。关键约定是定义main()函数脚本在被python进程执行时把main()的返回值打印出来。这样执行器只需要读取标准输出就能拿到结果。def main(): return Meeting notes: Chester finished the create user API, next step is to implement the update user API. if __name__ __main__: print(main())这段脚本写成什么样并不重要重要的是它验证了「Agent 发现技能 → 读取说明 → 调用工具 → 执行脚本 → 输出结果」的闭环。你也可以把main()里换成任何真实的业务逻辑比如读取某个报告文件、解析一份 CSV 再返回统计结果。3. C# 的坑Script execution is not yet supported自己实现 PythonRunner3.1 官方限制是什么意思Microsoft Agent Framework 目前对 C# 宿主有一个明确的限制Agent 不会自动执行 Python 脚本。也就是说即使 SKILL.md 里写了「调用 execute_python」如果项目里根本没有这个工具Agent 只会返回一句「我无法执行脚本」之类的提示。这个限制不是 bug而是设计边界。Agent 框架只负责「推理与决策」不内置「脚本宿主」。执行 Python 这件事需要开发者自己把能力包装成一个工具函数再通过 AIFunctionFactory 注册给 Agent。理解了这一点与其抱怨官方没做完不如顺着机制补上缺失的那块拼图。3.2 PythonRunner.cs一个带校验的进程执行器下面这个实现参考了原文思路但补上了几个容易踩坑的点路径标准化、超时控制、错误输出合并、空路径和扩展名校验。using System; using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; public static class PythonRunner { public static async Taskstring RunPythonScriptAsync(string pythonFilePath) { if (string.IsNullOrWhiteSpace(pythonFilePath)) throw new ArgumentException(FilePath 不能为空, nameof(pythonFilePath)); // 统一拼接 BaseDirectory 下的 skills 目录避免相对路径被工作目录影响 string rawPath Path.Combine(AppContext.BaseDirectory, skills, pythonFilePath); string fullPath Path.GetFullPath(rawPath); if (!File.Exists(fullPath)) throw new FileNotFoundException($Python 文件不存在: {fullPath}, fullPath); if (!string.Equals(Path.GetExtension(fullPath), .py, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException(文件扩展名必须是 .py, nameof(pythonFilePath)); var startInfo new ProcessStartInfo { FileName python, Arguments $\{fullPath}\, UseShellExecute false, RedirectStandardOutput true, RedirectStandardError true, CreateNoWindow true, StandardOutputEncoding Encoding.UTF8, StandardErrorEncoding Encoding.UTF8 }; using var process new Process { StartInfo startInfo }; try { process.Start(); string output await process.StandardOutput.ReadToEndAsync(); string error await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(30)); if (process.ExitCode ! 0 || !string.IsNullOrWhiteSpace(error)) throw new InvalidOperationException( $Python 脚本执行失败ExitCode{process.ExitCode}错误输出{error.Trim()}); return output.TrimEnd(); } catch (OperationCanceledException) { throw new TimeoutException(Python 脚本执行超过 30 秒已终止。); } } }3.3 三个容易被忽略的边界第一Path.GetFullPath不能省。Path.Combine只是拼接字符串如果 pythonFilePath 里带了..最终路径可能跳出 skills 目录。先转成完整路径再校验它是否真的在预期目录内比事后排查路径穿越要省心得多。第二ReadToEnd和WaitForExit的顺序。标准输出和标准错误管道如果先做同步阻塞读取遇到脚本输出量大时会死锁。用异步读取再等待退出顺序更稳。第三FileName python依赖 PATH 环境变量。如果你的机器上 Python 命令是python3或某个绝对路径需要在这个执行器里加一个配置项而不是硬编码。4. 把 PythonRunner 暴露成 execute_python 工具4.1 Program.cs 里的注册片段有了执行器下一步就是让 Agent 能调用它。Microsoft Agent Framework 底层基于 AIFunctionFactory把一个 C# 方法转成模型可感知的工具函数只需要一行注册代码// Program.cs 片段 builder.Tools.Add(AIFunctionFactory.Create( PythonRunner.RunPythonScriptAsync, name: execute_python));这里name参数特别关键。模型并不知道你的 C# 方法名叫什么它只会根据 SKILL.md 里的描述去查找工具名。SKILL.md 里写的是 execute_python那么注册名就必须是 execute_python。如果你在这里手滑改成了run_pyAgent 在读到 SKILL.md 后会四处找 execute_python 这个名字最终报出工具不存在的错误。4.2 方法签名参数名比想象中重要AIFunctionFactory 默认使用方法参数名作为工具函数的参数名。也就是说RunPythonScriptAsync(string pythonFilePath)暴露给模型的参数名是pythonFilePath。SKILL.md 里如果写的指令是「把脚本路径作为参数传入」模型会自然地把路径字符串填到这个参数上。所以参数命名要和 SKILL.md 的措辞对齐或者干脆把 SKILL.md 里的指令写细一点明确写出参数名。比如调用方式把脚本相对路径作为参数 pythonFilePath交给 execute_python 工具执行。这样模型的推理压力会小很多错误传参的概率也会下降。5. 配置模型 Key从 TaoToken 拿一把统一钥匙5.1 原文没交代的一环模型通道前面几段把 Skill 和 Tool 的代码都写完了但还有一个前置条件被很多教程默认跳过Agent 的「决策」本身就是一次模型推理。SKILL.md 写得再清楚、Tool 注册得再对模型 Key 没配好整个流程根本跑不起来。原文只教了怎么组织目录和注册工具没有交代模型通道从哪来。我在 TaoToken 上注册之后创建了一把 API Key然后把模型的 Base URL 指到 https://taotoken.net/api。这样做的直接收益是不用再为不同模型渠道分别维护 KeyAgent 的每步推理都统一记在 TaoToken 的账号里后续核对用量只需要回控制台看一个地方。5.2 创建 Key 和填 Base URL在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 完成两件事注册账号、在控制台创建 API Key。Key 的格式是一串有一定长度的随机字符复制后保存到本地配置不要直接提交进 Git 仓库。Base URL 填 https://taotoken.net/api注意末尾不要加/v1。这是最容易出错的地方很多模型服务端的地址习惯带版本号但这里不需要。填错的话Agent 启动时能正常加载一旦发起对话就会报连接错误或 404。5.3 Kernel 构建示例Agent Framework 底层走 Semantic Kernel模型连接在 Kernel 构建时指定。下面这段代码把 endpoint 指向 TaoTokenapiKey 用你刚创建的值using Microsoft.SemanticKernel; Kernel kernel Kernel.CreateBuilder() .AddOpenAIChatCompletion( modelId: YOUR_MODEL_ID, endpoint: new Uri(https://taotoken.net/api), apiKey: YOUR_API_KEY) .Build();YOUR_API_KEY替换成你在 TaoToken 创建的那把 Key。YOUR_MODEL_ID填什么以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 模型广场当时列表为准网上流传的旧模型 ID 不一定还在列表里。6. Skills Tool 协作链路与运行验证6.1 一次完整的调用链模型 Key 配好后整个链路的走向是固定的用户输入“帮我获取会议纪要” ↓ Agent 启动时扫描 skills/ 目录发现 meeting-notes ↓ 根据 description 判断与当前任务相关加载 SKILL.md ↓ 模型读取 SKILL.md 正文看到“交给 execute_python 工具执行” ↓ 模型决定调用 execute_python(pythonFilePath meeting-notes/scripts/GetMeetingNotes.py) ↓ PythonRunner 启动 python 进程执行脚本 ↓ 脚本打印结果Agent 把结果组织成回答返回给用户这条链路里真正由模型做决定的时刻有两个一是「是否加载 meeting-notes 这个技能」二是「是否调用 execute_python」。第二个决策点尤其重要因为它发生在模型读完 SKILL.md 之后属于工具调用的推理步骤消耗的 Token 会计入本次请求。6.2 运行后的预期输出用户输入帮我获取会议纪要Agent 内部行为发现技能 meeting-notes读取 SKILL.md理解需要调用 execute_python调用execute_python(meeting-notes/scripts/GetMeetingNotes.py)拿到脚本输出Meeting notes: Chester finished the create user API, next step is to implement the update user API.最终回复给用户的就是脚本打印的这段文本。如果脚本报错PythonRunner 会抛异常并携带 stderr 的内容回传Agent 会把这部分异常信息一并返回。6.3 验证与对账跑通一次后回 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 控制台对一下这次调用的 Token 消耗。你可以看到 Agent 在「读取 SKILL.md ‐ 决策调用 ‐ 解析脚本输出」这几步分别消耗了多少也能确认自己的 Key 配置是否真的生效。如果控制台显示 0 消耗大概率是请求没有走到 TaoToken 通道回去检查 Kernel 构建时用的 endpoint 是不是 https://taotoken.net/api以及有没有误加/v1。7. 设计模式与安全红线7.1 Skill 负责决策Tool 负责执行这个例子的核心模式一句话就能说清Skill 是给模型看的指令Tool 是给程序干的活。Skill 本身不包含任何执行逻辑它只是用 Markdown 写清楚「什么时候用、怎么用」。真正执行的 PythonRunner 是一个普通 C# 方法不关心调用它的模型是谁、SKILL.md 写了什么。两者通过名字约定连接起来Skill 发出指令Tool 响应调用。层级作用示例Skill告诉 Agent 做什么meeting-notes/SKILL.mdTool真正执行动作execute_python 工具这种解耦让技能包可以随意增删不需要改 C# 代码新技能只要在 SKILL.md 里声明好要调用的工具名就能被现有 Tool 接住。7.2 执行脚本的安全红线脚本执行属于高风险能力。PythonRunner 一旦暴露给 Agent就意味着模型可以通过工具在宿主机上运行任意 Python 代码所以边界必须收紧路径限制集中拼接skills/目录用Path.GetFullPath后校验防止../路径穿越扩展名白名单只允许.py文件避免被执行器调用到其他类型文件沙箱运行正式环境建议把 PythonRunner 放进 Docker 容器限制文件系统访问范围和网络权限。提示验证用本机调试可以临时放开但生产环境不要把这个 Tool 接到会访问生产库或核心业务系统的 Agent 上。脚本执行能力适合在隔离环境跑数据加工和文本处理不适合直接操作线上数据。7.3 跑通后的下一步链路通了以后建议先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息确认模型 ID 和 Base URL 没填错。随后打开 Coding Plan 看看当前用量够不够支撑你持续调试如果后续要换成其他模型直接在 控制台 API Keys 里再建一把 Key 就行不用动任何代码。每把 Key 的消耗明细都能在控制台里单独核对多人协作时给每人分一把出问题也容易排查。