Skill 这个名字最近在 AI 圈子里出现频率特别高,尤其是在 Codex、Claude 这类编程助手陆续支持之后。很多人第一次接触 Skill,是从"下载一个别人做好的 Skill"开始的,真正自己动手写一个的人其实不多。这个事儿看起来神秘,拆开之后无非是三个部分:一份 SKILL.md 说明文档、一串 scripts 脚本、一堆 references 参考资料。这篇博文就围绕这三件套,把从零开发一个测试 Skill 的完整思路和步骤讲清楚,适合想给 AI 编程助手定制专属能力的开发者、做智能体落地的工程师,以及所有对"如何把零散经验固化成 AI 可复用的能力包"这件事感兴趣的人。
1. Skill 的本质认知:它不是提示词,也不是 Agent
1.1 Skill、Prompt、Agent 三者到底差在哪
网上关于 "Skill 是什么" 的问题特别多,很多人拿它跟 Prompt 比,又拿它跟 Agent 比。我的理解是:Prompt 是一段文字,告诉 AI"这一次"该怎么做;Agent 是一个完整的决策和执行循环,能自己规划多步动作;而 Skill 介于两者之间,它是一套结构化的"能力包",核心作用是让 AI 在需要的时候,能像翻开一本操作手册一样,快速获得某一类任务的标准处理流程、脚本工具和领域知识。
打个比方,Prompt 相当于你口头跟同事说"帮我修个 bug",Agent 相当于你把整个修 bug 的流程、工具链、决策逻辑全部固化成一个自动化系统,而 Skill 相当于你递给同事一份图文并茂的《故障排查 SOP 手册》,他按照手册里写的步骤、调用手册里附带的脚本,就能完成工作。这个类比能解释为什么 Skill 值得单独搞一个文件结构而不是直接写在 System Prompt 里——因为它要承载的东西太多了,包括可执行的代码、可查询的参考资料、可复用的步骤模板,这些东西塞在 Prompt 里既混乱又浪费 token。
1.2 为什么说"三步走"是最合理的开发路线
我最初写 Skill 的时候也走过弯路,一股脑把所有东西塞进一个 SKILL.md,结果文档冗长、AI 加载慢、执行准确率也不高。后来参考社区的成熟项目,发现几乎所有好用的 Skill 都遵循同一种结构:用 SKILL.md 描述"怎么做",用 scripts 解决"做什么",用 references 提供"依据什么"。三者各司其职,刚好对应开发一个能力包最自然的三个阶段。
SKILL.md 是入口,AI 首先读它来决定"这个场景该不该用、用的话按什么顺序操作"。scripts 是执行层,凡是需要确定性计算、文件读写、网络请求、数据解析的环节,都不应该让 AI 自由发挥,而应该由预制的脚本兜底。references 是知识底座,存放领域术语表、API 文档、历史案例、设计模式等,供 AI 在操作过程中查阅。这样拆分的直接好处是:每一部分都能独立维护、独立测试,SKILL.md 更新了不影响 scripts 的稳定性,scripts 修了 bug 也不需要改动文档,references 增删资料更是不动核心逻辑。
2. SKILL.md 编写实战:一份让 AI 能稳定执行的文档怎么写
2.1 核心结构设计
SKILL.md 本质上是给 AI 看的开发文档,它跟给人看的 README 有本质区别。人看 README 喜欢看原理和背景,AI 执行文档则更看重"什么时候启用、按什么顺序做什么、触发条件是什么"。我建议采用五段式结构:元信息(frontmatter)、场景定义、操作步骤、输入输出规范、注意事项。
frontmatter 部分用 YAML 格式,至少包含 name 和 description 两个字段。description 非常关键,它是 Skill 被检索和触发的依据,要写得像搜索引擎的索引词,把触发场景、任务类型、典型用户请求都覆盖进去。比如一个日志分析 Skill 的描述,不要只写"analysis log",要写成"使用 Python 脚本分析 Nginx/Java 应用日志,定位 5xx 错误与慢查询,输出统计报告"。场景定义部分要写清楚"什么时候不适用",这往往是新手最容易漏掉的,AI 没有明确的否定条件就容易乱触发。
2.2 步骤设计的颗粒度把握
SKILL.md 的主体是操作步骤,这里的关键问题是:步骤写到多细才合适?写太粗,AI 拿到文档不知道具体怎么落地;写太细,文档冗长不说,还容易把 AI 的灵活性锁死。我的经验是:用"三步法"划分颗粒度。
顶层只写 3 到 5 个大步骤,比如"数据采集 → 数据清洗 → 统计分析 → 报告生成",每个大步骤下面再用简短清单描述关键操作和信息来源。真正的执行细节要么放到 scripts 里封装,要么放到 references 里备份,SKILL.md 本身只保留索引性质的内容。这样做的好处是,AI 读完 SKILL.md 能在脑海(或者说上下文窗口)中快速建立任务地图,需要细节时按图索骥去 references 查,而不是被文档里铺天盖地的细节淹没。
2.3 让"注意事项"真正发挥作用
SKILL.md 里的注意事项不是给人看的,是给 AI 看的执行约束。写的时候要具体到"不要做什么、必须做什么、遇到什么情况停下来"。以脚本开发为例,我会在原稿中写清"不要使用未声明的第三方库""路径读取必须基于项目根目录,不能使用绝对路径""输入文件不存在时立即报错而非自动创建空文件"。这些边界条件如果用自然语言堆在正文里,AI 很容易混淆优先级,把它们提炼到独立的"执行约束"小节,效果明显更好。
3. scripts 脚本的准备:把确定性交给代码
3.1 什么时候该写脚本,什么时候该让 AI 自由发挥
Skill 里放 scripts,核心目的是兜住 AI 的"幻觉区间"。AI 在生成代码、计算结果、处理复杂逻辑时存在不确定性,凡是结果必须精确的环节(比如读取 JSON 文件中的特定字段、计算两个时间戳之间的差值、调用 API 解析响应),都应该提前写好 Python 脚本,让 AI 通过命令行调用来完成任务,而不是让 AI 现场手写代码。技能目标是一个"测试 Skill",那么测试用例生成、结果比对、覆盖统计这类环节就是天然的脚本化场景。
从工程习惯的角度,我把 scripts 分成三类:核心工具脚本(完成主要计算或处理)、辅助工具脚本(准备环境、格式化数据、生成测试报告)、适配器脚本(对接外部系统或 API)。在目录结构上分别放在 scripts/ 根目录、scripts/utils/ 和 scripts/adapters/ 下,避免一锅烩。
3.2 脚本设计的关键原则:输入输出必须标准化
Skill 中的脚本跟普通脚本最大的区别在于调用者不是人而是 AI,所以脚本的输入输出设计要尽量"直白"。输入方面,用命令行参数传递而不是交互式 input(),参数顺序要固定、短参数和长参数尽量同时支持,能使用环境变量的就不要在脚本里硬编码。输出方面,执行成功时直接输出简洁的结构化结果(JSON 或纯文本表格),不要打印多余的日志;失败时必须输出非零退出码和一行明确的错误提示,这样 AI 才能根据退出码判断后续操作分支。
以我写的这个测试 Skill 为例,核心脚本run_tests.py接收一个测试模块路径作为入参,返回 JSON 格式的测试统计结果(总用例数、通过数、失败数、失败列表),这样 AI 拿到输出后无需二次解析就能组织汇报内容。对比一下:如果脚本输出的是大段文本日志,AI 还要想办法从里面抠信息,既浪费 token 又容易出错。
3.3 依赖管理:宁缺毋滥
Skill 是分发给不同用户、不同环境使用的,脚本依赖越重,安装门槛越高,失败概率也越大。我第一次分发 Skill 的时候直接在 requirements.txt 里列了十几个依赖库,结果用户环境装不上报错一堆,严重影响体验。后来学到的经验是:能用 Python 标准库解决的,绝不用第三方库;必须用第三方库的场景(比如请求网络、解析复杂格式),也要在 SKILL.md 里写清楚安装命令和版本要求。
用户热搜词里那个 "[err_pnpm_ignored_builds] ignored build scripts: core-js@3.45.1" 就是典型例子——现代包管理器(npm/pnpm)出于安全考虑默认阻止第三方依赖执行 postinstall 脚本,这在安装 node-sass 这类依赖时必然报错,同样的问题在 Python 生态里就是系统级依赖缺失。所以在 Skill 的脚本初始化部分,我不推荐用包管理器自动安装依赖,而是提供一个check_deps.py或setup.sh脚本来检测当前环境缺什么并给出手动安装指引,让每个人在自己环境里可控地补齐依赖。
4. references 参考资料:AI 工作时的知识底座
4.1 什么时候用 references,而不是直接写在 SKILL.md 里
references 目录里放的一定是"引用频率高、但没必要每次执行都完整读一遍"的资料。如果一段信息在每次执行 Skill 时都必须用到,那它应该放在 SKILL.md 正文里;如果只是特定分支才会用到,或者内容很长、AI 只在需要时才去翻阅,就适合放 references。判断标准很简单:这段内容你希望 AI 每次执行任务时都从头到尾读一遍吗?如果是,放 SKILL.md;如果不是,放 references。
实际开发中,我经常用 references 存放:领域术语对照表、API 接口文档片段、历史问题复盘、典型测试数据样例、代码模板。以测试 Skill 为例,references 目录下可以放 pytest 常用断言速查手册、一套标准化的测试报告模板、一个典型的被测函数样例文件,这些内容不需要每次执行都读一遍,但当 AI 遇到具体问题时,按需查一下马上就能进入工作状态。
4.2 资料组织与格式选型
references 里文件的格式建议优先使用 Markdown 和纯文本。Markdown 适合结构化文档,AI 解析效率高;纯文本适合配置文件、日志样例。要避免使用 PDF、Word、HTML 等格式,AI 读取这些格式的代价更高,容易乱码或解析失败。每个文件内部要有清晰的小标题和索引目录,方便 AI 在长文档中快速定位需要的内容。
目录结构上,我习惯在 references 下按主题建一层子目录,并在根目录放一个 README.md 全局索引,说明每个子目录里有什么资料、什么情况下该去查哪个文件。这个 README 相当于给 AI 用的"图书索引",能显著减少 AI 在 references 目录里东翻西找的次数。
4.3 定期复盘,删除无效信息
参考资料最忌讳"只加不减"。运行一段时间后,Skill 的调用记录能告诉你哪些资料被频繁访问,哪些资料从未被读取,那些长期"冷门"的文件说明它要么没有被触达,要么本身就不该放在 Skill 里。我一般每两三个月从代码里调取一次"AI 执行日志",统计 references 目录下每个文件的打开频率,高频文档持续维护,低频文档要么精简进 SKILL.md、要么直接归档掉。这个习惯能避免 Skill 体积无限膨胀,始终保持轻量高效。
5. 完整实操:从零搭建一个测试 Skill
5.1 工程目录设计与初始化
开发一个新的 Skill,首先是搭目录骨架。我按照 Skill 社区比较通行的结构来组织,这里给出可直接抄的目录树:
my-test-skill/ ├── SKILL.md ├── scripts/ │ ├── run_tests.py │ ├── check_deps.py │ └── utils/ │ └── html_report.py └── references/ ├── README.md └── pytest-cheatsheet.md初始化时先在根目录创建SKILL.md,把 frontmatter 写好,然后为两个空目录分别创建布局说明。这里有个经验:不要等所有内容都准备齐全才开始写 SKILL.md,而是先写一版最简可用版本,把结构跑通,再逐步往里面补充内容。原因很简单,Skill 是一个需要实测迭代的产物,前期写得太完整反而会让 AI 对文档的理解负担变重,影响测试验证。
5.2 编写核心脚本 run_tests.py
测试 Skill 的核心是自动化测试执行脚本。我用 Python 标准库 + pytest 实现一个精简版,关键代码结构如下:
"""Run pytest and generate structured result output.""" import argparse import json import sys import tempfile import subprocess from pathlib import Path def parse_args(): parser = argparse.ArgumentParser(description="Run pytest for a target directory.") parser.add_argument("target", type=str, help="Path to test target directory or file") parser.add_argument("--format", default="json", choices=["json", "text"], help="Output format") return parser.parse_args() def run_pytest(path: str) -> dict: """Execute pytest with json report plugin if available, otherwise fallback to junitxml.""" if not Path(path).exists(): return {"ok": False, "error": f"Target path does not exist: {path}"} result_template = {"target": path, "total": 0, "passed": 0, "failed": 0, "errors": [], "duration": 0.0} with tempfile.TemporaryDirectory() as tmpdir: junit_xml_path = str(Path(tmpdir) / "result.xml") cmd = [sys.executable, "-m", "pytest", path, "-q", "--junitxml", junit_xml_path, "--disable-warnings", "--no-header"] try: completed = subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return {"ok": False, "error": "pytest timed out after 60s"} # parse junit xml (simplified) if Path(junit_xml_path).exists(): import xml.etree.ElementTree as ET root = ET.parse(junit_xml_path).getroot() result_template["total"] = int(root.attrib.get("tests", 0)) result_template["passed"] = int(root.attrib.get("passed", 0)) result_template["failed"] = int(root.attrib.get("failures", 0)) for tc in root.iter("testcase"): for failure in tc.findall("failure"): result_template["errors"].append({ "name": tc.attrib.get("name", ""), "message": (failure.attrib.get("message", "") or "")[:200] }) result_template["ok"] = completed.returncode == 0 return result_template def main(): args = parse_args() result = run_pytest(args.target) if args.format == "json": print(json.dumps(result, ensure_ascii=False, indent=2)) else: print(f"target: {result['target']}") print(f"total: {result['total']}, passed: {result['passed']}, failed: {result['failed']}") if result["errors"]: print("errors:") for e in result["errors"]: print(f" - {e['name']}: {e['message']}") sys.exit(0 if result.get("ok") else 1) if __name__ == "__main__": main()这个脚本设计上有几个细节值得展开说明。
脚本不直接调用 pytest 的 Python API,而是用subprocess调起新的 pytest 进程并解析 JUnit XML 报告。这么做的好处是:测试环境中的 pytest 插件配置、faulthandler、覆盖率钩子都不会干扰主进程的状态,隔离性更好。如果直接import pytest后在主进程内执行,一旦被测模块抛出 SystemExit 或修改了全局环境变量,Skill 的宿主进程也会跟着遭殃。
另一个细节是--junitxml临时目录的使用。pytest 的 JUnit XML 输出结构非常稳定,即使没有额外插件也能解析出总用例数、失败列表等核心信息,缺点是没有详细的堆栈跟踪,但足够满足 Skill 场景下的"快速判断成败 + 定位失败用例"需求。如果想获得更丰富的 JSON 输出,建议在 dependencies 里加入 pytest-json-report 插件,但这里为了降低依赖门槛,我用标准库 + xml.etree 解析,轻量且零额外安装。
5.3 编写 SKILL.md 主文件
SKILL.md的内容是灵魂部分。我写了一个精简但结构完整的版本,各位可以参考:
--- name: run-python-tests description: Run pytest tests for python project modules, collect pass/fail statistics and error messages. Use when user requests test execution, test result reporting, or test failure diagnosis. --- # Run Python Tests Run pytest against a Python module or test directory, and summarize execution results for the user. ## When to Use - User asks to "run tests" or "check test status" for a Python project. - User wants to verify a recently changed module still passes existing tests. - User reports a test failure and asks to identify failing test cases. When not to use: if user only wants to write a new test case but not execute it; if the project is not Python. ## Workflow 1. Verify the current directory contains a Python project (has *.py or pyproject.toml). 2. Check Python environment dependencies using `python scripts/check_deps.py --name pytest`. 3. If dependencies are missing, install pytest first; if installation fails, stop and report the error. 4. Run `python scripts/run_tests.py <target> --format json`. 5. Parse the JSON output. Report total/passed/failed numbers to the user. 6. If failures exist, list failing test names and error messages. Optionally re-run the failed tests with `pytest <target> --tb=short -x`. ## Execution Constraints - Do not modify user source code while running tests. - Do not send test targets to any external service. - If the target path does not exist, state so explicitly and do not attempt to create it. - Use relative paths consistently; never use absolute paths outside the project root. ## Input / Output - Input: path to a test directory or test file (relative to project root). - Output: JSON object with fields: target, total, passed, failed, errors, duration.几个关键点说一下:
description 字段写得非常"触发友好",它把"用户什么请求可能触发这个技能"直接铺开表述,这样无论用户说"run tests"还是"帮我看看测试挂了没",AI 都能识别到这个 Skill 与当前需求匹配。场景定义部分单独列了"When not to use",这是很多早期 Skill 缺失的部分,没有它 AI 就可能在用户只想写测试的时候误触发执行。
执行约束部分特意补充了"不发外部服务""不自动创建不存在的目标",这层边界意义重大。Skill 会运行在用户本地环境,开发者无法预知终端用户的工程上下文,这种可能涉及安全边界的约束,需要在 SKILL.md 里明确写出来。
5.4 补齐 references 资料并测试
references 目录的内容比较灵活,一个好的起点是准备三样东西:pytest 常用命令速查、测试报告模板、项目说明样例。
references/README.md写成这样:
# References Index This directory provides additional context for running and diagnosing Python tests. ## Files - `pytest-cheatsheet.md`: common pytest command line options and assertion tips. - `report-template.md`: a reusable test report template for summarizing results to the user. ## When to Consult - Consult pytest-cheatsheet when user asks about specific pytest options or flags. - Consult report-template when preparing a formal test report for delivery.首次写完后,强烈建议走一遍完整的"三层验证"流程。
第一层,手动在终端依次执行 SKILL.md 里描述的所有命令,确认脚本在干净环境下能跑通。这一步看似简单,实际能过滤掉大量路径错、依赖缺失、权限问题。第二层,在支持 Skill 的 AI 编程助手中加载整个 Skill 目录,问它"帮我运行项目中 tests 目录下的测试",观察 AI 是否成功触发并走完流程。第三层,故意设计一个测试失败场景(比如在测试文件中插入一个必然失败的断言),然后让 AI 执行 Skill,看它能否准确识别失败用例并给出有用的错误信息。
我当时开发时第三层就抓到过一个问题:脚本输出的 JSON 中,failed 字段来源于 JUnit XML 的 failures 属性,但如果被测项目配置了--continue-on-collection-errors,实际失败数可能与 failures 不同。针对这类边界情况,我后续在脚本里加了双重校验,一个是从 XML 汇总失败数,一个是从错误列表长度反推,两个不一致时优先输出 error 列表并标记ok: false。
6. 常见问题与排查技巧实录
6.1 Skill 加载不生效,AI 完全无视它
这是新手遇到最频繁也最容易困惑的问题。大多数情况不是 SKILL.md 写错了,而是 description 没写好,AI 在意图匹配时根本没意识到这个 Skill 跟用户问题相关。排查顺序建议为:第一步,确认 SKILL.md 是否放在 Skill 目录根路径,模型系统对文件路径有严格期待,放错一层就不会被加载;第二步,检查 description 里的触发词是否覆盖用户可能的问法,对比用户原话,"跑一下测试"和"执行 pytest"都该被覆盖;第三步,在终端里手动测试 Skill 是否正常工作,排除加载问题后看执行环节本身是否有 bug。
有些平台要求 Skill 目录里只能有 SKILL.md 作为入口描述,其他文件必须放到子目录,如果 SKILL.md 意外放到 scripts 或 references 里,同样不会被识别到。
6.2 pnpm ignored build scripts 报错
用户热搜词里频繁出现的[err_pnpm_ignored_builds] ignored build scripts: core-js@3.45.1也在 Skill 工程里遇到过。pnpm 从 v9 开始默认阻止第三方依赖执行 postinstall 脚本,npm 生态里多个库(常见如 esbuild、core-js、cloudflared)依赖 postinstall 下载二进制或打补丁,一旦被阻止就会在运行时崩掉。
这不是 Skill 开发本身的问题,而是 Skill 分发到用户环境后由用户项目自身的 pnpm 版本导致的。解决办法分两步:如果项目是自己的,在.npmrc里加enable-pre-post-scripts=true临时放行;如果是别人项目的报错,在 SKILL.md 的依赖检查环节加上对这种报错的识别,输出"检测到 pnpm ignored build scripts,建议运行 pnpm rebuild 或调整 .npmrc"这类修复指引。实际处理中,pnpm rebuild <包名>能解决大部分已阻止依赖的场景,但要记得在 SKILL.md 里把这一分支写好,免得 AI 遇到没见过的问题就卡死。
6.3 脚本执行路径错误
Skill 在用户机器上执行时,脚本的工作目录不一定是 Skill 目录所在位置。比如用户从终端命令中心发起任务时,当前目录可能是任意位置,脚本用相对路径读 references 或写报告就会失败。我在开发初期反复踩这个坑,后续统一在脚本开头自动定位 Skill 根目录:
SKILL_ROOT = Path(__file__).resolve().parent.parent这样无论从哪个目录调用脚本,都能正确找到 Skill 下的文件和目录。另外,与用户项目交互时,脚本应该把用户项目根作为工作目录,避免把报告文件写到 Skill 目录内造成环境污染。
我在编写 Skill 的 scripts 时规定:临时文件和报告文件一律输出到用户项目下的.skill-output/目录,Skill 本体只读可执行,不写任何状态。这样既保证 Skill 的可重复分发,也避免了多任务并行时文件互相覆盖。
6.4 AI 在执行 Skill 时跳过 references
不少人在 Skill 里放了大量参考资料,结果 AI 从不主动去查,最后看了一眼 SKILL.md 就开始自由发挥。问题往往出在 SKILL.md 里的"工作流"没有明确指出"步骤 X 需要先查阅 references/Y.md"。给 AI 的指令要像指挥新手员工一样,不写清楚"该查手册时查手册",你就别指望他自觉查。
所以我会在 SKILL.md 的每个工作流步骤后面加一个斜体提示,比如"见 references/report-template.md"或"Consult pytest-cheatsheet for options",让 AI 在对应节点有一个自然的动作指向。这种显式指引对触发率提升非常明显。
6.5 依赖冲突与 Python 版本兼容性
Skill 脚本运行在用户环境中,最常见的问题就是 Python 版本差异。如果脚本用了 3.10+ 的语法(如match语句、|类型联合操作符),而用户环境还是 3.8,那脚本直接报 SyntaxError,AI 看到错误连怎么修都无从下手。
我建议脚本内部尽量兼容 Python 3.8+。使用新特性的地方通过sys.version_info判断来代替,而不是直接覆盖。另外,脚本内部不要尝试创建虚拟环境后再跑,这会显著增加执行时间和不确定性;直接在当前解释器下运行,代码层面做好版本兼容。如果确实需要指定依赖版本,在 SKILL.md 的依赖检查环节明确校验并终止后续步骤,给出修复指引而不是带病执行。
7. 关于 Skill 开发的延伸思考
写 Skill 说到底是在做"知识工程",本质是把人的经验、流程封装成 AI 能随时调用的结构化资产。它跟写普通代码的区别在于:代码的调用方也是代码,但 Skill 的调用方是一个大模型,模型的执行习惯、信息偏好、错误恢复能力跟传统程序完全不同,所以开发调试思路也要跟着调整。
我在维护自己那批 Skill 的过程中,最重要的一个经验是"小步快跑、持续迭代"。第一版永远不可能完美,你只要把"主流程能跑通、失败时能报错、信息组织清晰"这三个底限守好,就能先发布给真实用户用起来。后续根据使用日志和用户反馈一点点打磨,比闭门造车试图一次性写出完美版本高效得多。
最后分享一个小技巧:开发完 Skill 后,建议在 references 里放一份CHANGELOG.md,记录每次版本的改动点和动机。这个文件对用户来说价值不大,但对你自己三个月后回来看代码时非常有帮助。Skill 这种项目太容易"越改越乱",有了一份清晰的变更记录,你才能大胆重构而不必担心改坏以前验证通过的内容。