如果你最近也在折腾 Microsoft Agent Framework,八成会有跟我一样的困惑:模型明明很聪明,但让它干正事的时候总差一口气——要么嘴上答应,做一步停一步,要么干脆给你一段永远跑不起来的"伪代码"。后来我把注意力从"换更强的模型"转到"给它一套更好用的 Skills"上,问题立刻改观。在这套机制里,把"执行 Scripts"这种能力封装成 Skill 是最值得先做的一件事,因为不管是跑数据脚本、触发前端构建、批量处理文件,还是做运维巡检,最后都得落到一行行真实可运行的脚本上。这篇指南就是我从零把一个 script-runner Skill 跑通的全过程,包括结构设计、代码实现、安装排错,还有几个让我印象深刻的坑。适合正在做 Agent 自动化、或者想给助手加"动手能力"的开发者参考。
1. 先想清楚:Agent Skills 到底解决了什么问题
1.1 从 Prompt 到 Skills:Agent 的"肌肉记忆"
很多人一上来就写几千字的 system prompt,指望模型记住所有规则,但实际效果往往很虚。Prompt 本质上是一次性输入,塞得越多,模型越容易"忘记"和"跑偏",而且每次对话都要重复消耗上下文 token。Skills 则完全换了一个思路:把某一类能力打包成独立的技能模块,Agent 在需要的时候才加载它,平时不占内存、不烧 token。
可以这样理解:Prompt 像是告诉新同事"有问题你会查资料吧",而 Skill 是直接递给新同事一本操作手册加一个工具箱。真正干活的时候,他打开手册、拿出对应的工具,照着固定流程走完,效率和准确率完全不一样。OpenAI 和 Anthropic 后来把 Agent Skills 标准推成了行业通用约定,Microsoft Agent Framework 也沿用了这套思路:一个 Skill 就是一个文件夹,里面必须有一个入口描述文件(SKILL.md),其余资源按需放在子目录里。这里最妙的一点是,Agent 启动时并不会把所有 Skill 都装进上下文,它只会"看到"每个 Skill 的 description,等对话任务和某个 description 对上了,才把那个 Skill 的资源加载进来。
这也解释了为什么社区里越来越多人在分享"skills 包":前端开发 skills、数学建模 skills、专利写作 skills、公众号文章技能包、结构图 skills……这些东西本质上是可复用的"能力封装"。你不需要懂前端、不需要懂建模仿法,把对应的 Skill 装进去,Agent 就能在需要时调用它来完成任务。不过话说回来,这些现成的技能包大多解决的是"知识/生成"类问题,真正让 Agent 有"手脚"去操作系统和文件的,还是脚本执行类 Skills。
1.2 为什么"执行 Scripts"是第一个要掌握的 Skill
我自己的项目里,Agent 干得最多的其实不是写文章,而是跑脚本。举个例子:前端项目里有一堆待办,我让 Agent 清理 dist 目录、跑一下 lint、把散落的资源文件批量重命名,这些操作没有一个能靠"对话"完成,必须实际执行命令行工具或者脚本。再比如数据处理场景,让 Agent 从 CSV 里筛数据、生成统计报表;或者是构建发布场景,触发构建命令、收集产物和日志——说到底都是脚本在做实事。
如果 Agent 不掌握"执行 Scripts"这一类 Skill,自动化闭环就永远是断的。模型说得再好听,最终还是要有一个安全可控的通道,让它把命令真正丢进 shell、把脚本真正跑起来、再把结果拿回来分析。这也是我建议你先动手写 script-runner 的原因:它是 Agent 能力的"地基",后续不管是写数据处理 Skill、前端工程化 Skill 还是运维类 Skill,底层大都要依赖脚本执行器。
2. Skill 包的正确打开方式:目录结构和运行原理
2.1 一个标准 Skills 包长什么样
先看一个最标准的目录结构,这是我目前比较习惯的布局:
my-skill/ ├── SKILL.md ├── scripts/ │ ├── run_script.py │ └── helper.sh ├── resources/ │ └── template.txt └── requirements.txtSKILL.md 是整个技能包的入口,它的 frontmatter 里包含了 name、description、版本号、工具声明等元信息。正文部分则是给 Agent 看的"操作手册",告诉它这个 Skill 该怎么用、输入什么、输出什么、有哪些边界。scripts 目录放实际可执行的脚本,resources 目录放非执行的静态资源,比如模板、配置文件样例。requirements.txt 用来声明脚本依赖。
这里有个关键点:description 字段是整个 Skill 的"门面"。Agent 判断要不要调用一个 Skill,靠的主要就是这个描述。如果你把 description 写得模棱两可,比如只说"脚本执行工具",Agent 在遇到具体任务时经常想不到用它。我个人的经验是,description 至少要包含四类信息:这个 Skill 能做什么、在什么场景下用、典型输入是什么、输出格式是什么。写的时候甚至可以带上几个触发词,让 Agent 更容易命中。
2.2 scripts 目录:Agent 的执行工具箱
scripts 目录里放什么、不放什么,是有讲究的。很多新手容易把它当成普通项目代码目录,一口气塞几百行业务逻辑进去。但实际上,这里的每一个脚本都应该坚持"单一职责":一个脚本解决一个问题,输入输出干净利落。Agent 不是靠读源码来理解程序的,它是靠命令行参数和结果输出跟脚本打交道的,所以脚本的 CLI 参数要友好、健壮,最好支持 --help;输出格式要结构化,能输出 JSON 就尽量输出 JSON;错误提示要明确,别一直抛一个裸栈信息。
我在设计脚本接口时基本遵循三个原则:第一,所有参数尽量用命令行参数传递,不要依赖交互式输入,因为 Agent 没法跟你玩交互;第二,输出统一走标准输出,错误信息走标准错误,并且最后给一个明确退出码;第三,任何可能长时间运行的操作都要设置超时上限,防止脚本把整个 Agent 进程拖死。之前我写过一个批处理脚本,因为没设超时,某次遇到一个网络请求一直不返回,Agent 直接卡了十分钟。后来所有脚本入口我都加了 timeout 参数,这种问题才算根治。
2.3 执行脚本时,Agent 的环境怎么隔离
Agent 执行脚本最大的噩梦就是环境冲突:你本机 Python 是 3.10,某个 Skill 依赖 3.11;你全局 Node 装了某个包的旧版本,脚本里用的新 API 全都不认识。这种问题在人工操作时不算大事,但让 Agent 自己去处理就很费劲,它很可能改一堆乱七八糟的环境变量,最后把开发机搞得一塌糊涂。
我的习惯是给每个 Skill 建独立的虚拟环境,Python 的用 venv,Node 的用 nvm + workspace,有条件的直接丢 Docker 里跑。一个 Skill 依赖什么版本,就在它自己的环境里解决,绝不污染宿主环境。比如上面那个目录结构里的 requirements.txt,我会配合一个 bootstrap 脚本,在首次安装 Skill 的时候自动创建虚拟环境并安装依赖。虽然前期多花了一点配置时间,但后面 Agent 跑任务时特别省心,基本不会出现"跑着跑着发现缺个依赖"的尴尬局面。
3. 实战:从零写一个通用 script-runner Skill
3.1 需求拆解:这个 Skill 到底要接收什么、返回什么
动手写代码之前,先想清楚接口。我设计的 script-runner 要支持的输入包括:执行什么命令(命令行字符串)、执行哪个脚本文件、工作目录在哪儿、超时上限是多少、需要注入哪些环境变量。输出则统一返回一段 JSON,包含执行成功与否、退出码、标准输出截断、标准错误截断、实际耗时。
这几个字段的选择是有原因的。标准输出往往很长,全量返回会占用大量上下文,所以我在脚本里做了截断;但截断太多又会丢关键信息,所以截断长度要够用。退出码是判断命令成功与否的最可靠信号,比去正则匹配输出文本靠谱一百倍。耗时信息则是给 Agent 一个判断依据:如果任务花了很久,说明可能有性能瓶颈,Agent 后续可以给出更合理的建议。
3.2 SKILL.md 怎么写才不会被 Agent 无视
SKILL.md 的正文部分太重要了。它不是给人看的 README,而是给 Agent 看的"使用规范"。我从多个项目里总结出的经验是:写明支持范围、输入参数、输出格式,同时一定要写清"什么时候不要用这个 Skill"。这最后一点很多人会忽略,但不写清楚,Agent 就可能在你让它写个文案的时候莫名其妙去执行一个命令。
下面是我目前用下来觉得比较舒服的 SKILL.md 写法:
--- name: script-runner description: 通用脚本执行器。当需要运行 Python、Node、Shell 脚本,或需要执行带参数的命令行任务(如数据转换、前端构建、自动化批处理、系统维护)时使用。 version: 1.0.0 tools: - python3 - bash - node --- # script-runner 通用脚本执行器,用于在受控环境中执行指定命令或脚本。 ## 支持范围 - Python / Node / Shell 脚本 - 带参数的命令行工具 - 指定工作目录和自定义环境变量 ## 使用规则 1. 调用前必须确认脚本来源可信。 2. 涉及删除、覆盖、外部请求的命令,应先向用户摘要将要执行的操作并等待确认。 3. 执行结果统一返回 JSON,包含 ok、returncode、stdout、stderr、elapsed。注意 description 里我写了"数据转换、前端构建、自动化批处理、系统维护"这些具体场景,而不是只说"执行命令"。这样 Agent 在遇到相关任务时,更容易联想到这个 Skill。类似于你在搜索引擎里用长尾关键词,命中率才高。
3.3 核心执行脚本实现
接下来是核心的脚本执行器。我用 Python 写了一个跨平台的实现,核心思路是把命令传给 subprocess,捕获输出并转成 JSON。
#!/usr/bin/env python3 """通用脚本执行器,Agent 执行 Scripts 的运行后端。""" import argparse import json import os import subprocess import time def execute(cmd: str, cwd: str, timeout: int, env: dict) -> dict: merged_env = os.environ.copy() merged_env.update(env or {}) start = time.monotonic() try: proc = subprocess.run( cmd, shell=True, cwd=cwd, env=merged_env, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, ) result = { "ok": proc.returncode == 0, "returncode": proc.returncode, "stdout": proc.stdout[-8000:], "stderr": proc.stderr[-4000:], "elapsed": round(time.monotonic() - start, 2), } except subprocess.TimeoutExpired as e: stdout = "" if e.stdout: stdout = e.stdout.decode("utf-8", "replace")[-2000:] result = { "ok": False, "error": f"timeout {timeout}s", "stdout": stdout, "stderr": "command killed by timeout", "elapsed": timeout, } except Exception as e: result = { "ok": False, "error": str(e), "elapsed": round(time.monotonic() - start, 2), } return result def main(): parser = argparse.ArgumentParser(description="Unified script runner for Agent") parser.add_argument("--cmd", help="要执行的命令") parser.add_argument("--file", help="要执行的脚本文件路径") parser.add_argument("--cwd", default=".", help="工作目录,默认为当前目录") parser.add_argument("--timeout", type=int, default=60, help="超时秒数") parser.add_argument("--env", action="append", default=[], help="KEY=VALUE 格式的环境变量") args = parser.parse_args() if args.file: script = os.path.abspath(args.file) if not os.path.exists(script): print(json.dumps({"ok": False, "error": f"script not found: {script}"})) return 1 cmd = f'cd "{os.path.dirname(script)}" && "{script}"' else: cmd = args.cmd if not cmd: print(json.dumps({"ok": False, "error": "no command or file provided"})) return 1 env = {} for item in args.env: if "=" in item: k, v = item.split("=", 1) env[k.strip()] = v.strip() result = execute(cmd, args.cwd, args.timeout, env) print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if result.get("ok") else 1 if __name__ == "__main__": raise SystemExit(main())几个实现细节解释一下。shell=True 是为了支持管道、重定向和带参数的复合命令,这在 Agent 自动化场景里非常实用,但也意味着要更谨慎地控制传入内容,我会在后面安全边界部分展开。capture_output=True 把输出抓回 Python 进程,不直接打到终端,这样 Agent 能拿到干净的结果。utf-8 编码加 errors="replace",是为了防止脚本在 Windows 上输出 GBK 编码导致解码崩溃。stdout 截断 8000 字符、stderr 截断 4000 字符,是我试下来比较平衡的配置,上下文消耗可控,关键信息也不容易丢。
JSON 输出是这套脚本的核心设计。因为 Agent 天然适合解析结构化数据,拿到 JSON 之后它可以非常稳定地判断下一步行动。之前我用纯文本输出的时候,Agent 经常因为输出格式变化判断失败,改成 JSON 之后这类问题几乎绝迹。这也是为什么我在脚本的每个分支里都保证输出 JSON。
3.4 在 Microsoft Agent Framework 里注册并跑通
脚本文件本身不会自动被 Agent 使用,你需要在 Agent 的配置里把这个 Skill 注册进去。Microsoft Agent Framework 的 SDK 本身支持把技能包作为 Agent 的工具链,不同版本对"工具"和"技能"的叫法略有差异,但核心逻辑是一致的:注册条目、写明描述、指定调用命令。
# 示意代码:把 script-runner 注册为 Agent 可调用能力 agent = AgentFrameworkAgent( name="ops-bot", skills=[ { "name": "script-runner", "description": "在受控环境执行脚本或命令,返回结构化 JSON 结果", "command": "python3 skills/script-runner/scripts/run_script.py", } ], )注册完成之后,你就可以在对话里直接给 Agent 下指令了。我实际测试过这样一个流程:跟 Agent 说"统计项目里所有 .ts 文件总行数,按目录汇总"。Agent 做了三件事:判断这个任务属于 script-runner 的适用范围,构造对应的命令行参数,调起脚本执行,然后把 JSON 结果转成一份可读的汇总反馈给我。整个过程大概十几秒,中途我没有手动介入一次。
如果你所在的项目里 Skill 加载机制是把 SKILL.md 自动扫描的,那更简单:把目录放进 Agent 指定的 skills 目录,重启或刷新配置即可。装完之后建议先用一句"你现在有哪些技能"验证一下 Agent 是否真的索引到了这个 Skill,如果没有,多半是路径或配置没生效。
4. 安装、分发与管理 Skills 的生态玩法
4.1 重新认识 npx skills 一类的脚手架命令
现在的 Skills 生态已经比一年前成熟太多了。社区里出现了一批类似npx skills add这样的脚手架命令,目的就是让你一条命令把一个技能包装进本地 Agent。像前端开发 skills、Superpowers 技能集合、Codex skills、OpenCode 支持的能力包,都在用这种分发方式。我最初觉得这些命令有点花哨,但实际用下来发现确实省事:工具会负责解析仓库结构、写入正确的配置目录、把依赖装好。
热词里有一条命令很典型:
npx skills add sandai-org/vidmuse-skills --agent claude-code -g -y简单拆解一下:npx skills add 是装包命令,后面跟的是 GitHub 仓库地址,--agent 指定目标 Agent 类型,-g 表示全局安装,-y 表示跳过交互确认。这里claude-code是目前社区里对 Agent 类型最通用的叫法,如果你的框架也是兼容 Agent Skills 标准的,通过同样的逻辑就能装上。Microsoft Agent Framework 里如果自带了 explore 或清单命令,建议优先用官方渠道,同时这类社区工具依然值得装在旁边作为补充。
4.2 把别人的 Skill 装进自己的 Agent
安装一个别人的 Skill,看起来很简单,但有几个细节要留意。第一步是先搞清楚目标 Agent 的 skills 目录在哪里,不同框架默认路径差别很大,有的在项目目录下,有的在用户主目录下。第二步是执行安装命令并观察输出,看看它到底把文件放到了哪个目录。第三步是验证,装完之后立刻问 Agent 一个问题,确认它真的能看到新技能。
有一次我装了一个视频处理相关的技能包,安装命令输出显示成功,但我让 Agent 处理视频时它完全没反应。排查了半天,发现是工具把它安装到了另一个 Agent 的配置目录,我这个项目里的 Agent 根本没加载到。所以我现在都会在装完包之后手动检查一遍目标目录,确认 SKILL.md 确实存在,再跑一个最小化的调用测试。如果安装工具有--dry-run之类的参数,先跑一遍看清楚路径再实际安装,能省不少事。
4.3 源码安装 Skill,以及我踩过的坑
热词里有人问"npx skills 怎么源码安装 skill",其实源码安装没那么神秘。本质就是把它仓库里的 Skill 目录复制到你的 Agent skills 目录,或者用工具提供的本地路径参数安装。我常用的方式是这样:
git clone https://github.com/someone/awesome-skill.git # 方式一:直接用本地路径安装 npx skills add ./awesome-skill --agent <agent-name> -g -y # 方式二:手动复制到 agents 的 skills 目录 cp -r awesome-skill /your/agent/skills/如果安装工具不支持本地路径,直接用方式二手动复制也是一样的效果,关键是目录名字和 SKILL.md 别放错层级。
这里我要特别推荐一个技巧:开发自己的 Skill 时,别用复制文件的方式反复装,改成在 skills 目录里建软链,指向你的源码目录。比如ln -s ~/dev/my-skill /your/agent/skills/my-skill。这样你在源码里改一行,Agent 下一次调用就能感知到,不用反复安装、卸载。我最初不知道这个办法,每次改脚本都要重新复制一遍,改了十几次之后整个人都麻了。
5. 真实排坑:执行 Scripts 时最常见的崩溃现场
5.1 被忽略的 build scripts 到底怎么回事
如果你用 npm 装过带原生模块的依赖,一定见过这类日志:
npm warn install-scripts 1 package has install scripts not yet covered by allowlist ignored build scripts: cpu-features@0.0.10, esbuild@0.21.5, ssh2@1.17.0第一次见到时我以为是命令写错了,搜了半天才发现这是新版 npm 出于供应链安全考虑默认的行为:不会再无条件执行包里的安装脚本。像 esbuild、ssh2、cpu-features 这类包含原生二进制模块的包,安装脚本会在装包时下载或编译对应平台的二进制文件,一旦被跳过,后面运行的时候就会报类似"esbuild binary not found"的错误。
解决方式分几步走。首先,看清楚是哪些包被忽略,确认这些包确实是你依赖链里需要的;然后,可以单独重建某个包的原生二进制,比如npm rebuild esbuild;如果确认安全,再按 npm 终端提示的方式把这些包加进允许执行安装脚本的名单。这里要注意,不同 npm 版本的具体设置字段不太一致,直接看当前版本给出的提示就行。最不建议的做法是全局把 ignore-scripts 改成 false,那样等于把所有依赖的安装脚本都放行了,一旦某个依赖被供应链投毒,风险非常大。我自己的原则是:能单独放行就单独放行,能不放开就不放开。
5.2 Agent 不执行我的脚本,怎么办
这个问题我前前后后遇到不下二十次,归纳起来就三类原因。
第一个原因是 SKILL.md 里的 description 写得不够具体,Agent 根本不知道什么时候该调用它。我处理的办法是给 description 加"场景触发词",把各种可能触发它的说法都列进去,比如"跑一下"、"执行"、"批处理"、"汇总"、"构建"。改完之后命中率明显提升。
第二个原因是权限和工具列表没放开。有些框架默认只允许 Agent 调用白名单里的工具,你的脚本执行器没被加进去,即使 description 再准确也白搭。检查配置里是否允许 exec、shell、script 这类工具,并确认对应路径有可执行权限。
第三个原因最琐碎:依赖缺失、脚本路径写死、Python 环境不对。这种问题排查起来很费时间,所以我后来在脚本里加了日志输出,每步操作都打印当前工作目录、Python 版本、关键路径,Agent 拿到这些信息之后能快速自我纠正。
下面是我整理的一个速查表,方便你对照排查:
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| Agent 对调用请求无反应 | description 不准确 | 重写描述,加入场景触发词 |
| 调用时报权限错误 | 工具列表未放行 | 在 Agent 配置中放行脚本执行工具 |
| 命令报 command not found | 环境变量未继承 | 在脚本中显式设置 PATH 或注入环境变量 |
| 中文乱码 | 编码不匹配 | 统一使用 UTF-8,加 errors=replace |
| 命令长时间卡住 | 缺少超时控制 | 所有脚本入口加 timeout 参数 |
5.3 脚本执行的安全边界
让 Agent 能执行脚本,等于给了一个"手脚",但手脚不能乱伸。我在项目里踩过坑之后,给自己的 script-runner 订了几条硬性规则。
第一,危险操作必须二次确认。涉及到删除、覆盖、格式化、往外部服务写数据这类命令,Skill 在调用前必须把将要执行的命令原文展示给用户,等用户确认之后再执行。第二,超时必须设置上限。我一般默认 60 秒,复杂任务最多给到 300 秒,绝不允许无限期运行。第三,工作目录必须明确,防止相对路径导致的误删误写。第四,密钥和敏感信息不能以普通环境变量传给脚本,要用框架提供的密钥管理机制注入,避免在执行日志或 debug 输出里泄露。
注意:安全不是限制功能,而是让 Agent 的能力可控。一个"什么都能跑"的脚本执行器,和一个"知道什么不该跑"的脚本执行器,后者才是生产环境里能长期用的东西。
这条安全边界想明白之后,整个 Skill 的可信度直接提升一个档次。团队里其他同事参与到这个项目时,看到这套规则也会更放心。
最后再分享一个小技巧:我在脚本里强制要求每个脚本在开始运行时打印一行"运行开始 + 参数摘要",Agent 拿到结果后能确认这次执行确实是它想要的。这个习惯是从一次生产事故里学来的——当时脚本跑错了目录,输出看起来却正常,加了一行起始日志才把问题暴露出来。脚本执行类 Skill 的调试,教训往往就藏在这种看起来最不起眼的细节里。