如果你已经在用 MCP 把模型接上数据库、文件系统和第三方 API,你多半会遇到下一个更棘手的问题:工具都连上了,但模型依然不知道该按什么顺序、什么标准去完成一件事。这正是 Agent Skills(智能体技能)要补的那一层。它由 Anthropic 在 2025 年 10 月提出,随后作为开放标准沉淀到 agentskills.io,到 2026 年已被 Codex、GitHub Copilot、Microsoft Agent Framework 等平台按同一格式兼容。本篇是《Agent Skills 完全指南》的上半部分,我们先把地基打牢:SKILL.md 的字段硬约束、文件夹目录约定、渐进式披露的三层加载时序、Token 预算的账本思维,以及那个最容易被忽视的核心原则——能用代码判断的事,绝不用文字去求模型。
从 MCP 到 Agent Skills:Anthropic 的第二层抽象与 2025 年 10 月的起点
理解 Skills 的定位,最好把它和 MCP 对照着看。MCP(Model Context Protocol)解决的是「怎么连上外部工具与数据源」,它关心的是通道问题:模型能不能调用一个搜索接口、能不能读到一个数据库、能不能访问某个 SaaS 的 API。但通道打通不等于任务完成。一个真实的业务动作,往往包含判断、顺序、格式、边界校验、失败重试等一连串程序性决策,这些决策 MCP 本身并不负责。
所以 Anthropic 在 2025 年 10 月提出了 Agent Skills,把它定义为继 MCP 之后的第二层抽象:MCP 回答「工具在哪里、怎么调」,Skills 回答「拿到工具之后,这件事到底该怎么做才算做好」。一个负责把食材搬进厨房,一个负责给出配方与火候。二者是互补关系,不是替代关系——Skills 官方文档里也明确提到,在代码执行的安全性、稳定性与沙箱隔离上,MCP 那种由服务端托管执行的方式依然占据优势;Skill 更擅长轻量脚本与简单逻辑。
值得一提的是节奏:提出之后大约两个月,Anthropic 就把这套规范作为开放标准发布,而不是锁在自己家的产品里。任何 Agent 平台,只要按这套格式解析,就能兼容别人写好的 Skill。这个决定直接决定了后面要讲的跨平台版图。
开放标准落地:agentskills.io 与 Codex、Copilot、Microsoft Agent Framework 的兼容版图
到 2026 年,Agent Skills 的定位已经从「Claude 的一项功能」演变为跨平台开放标准,官方规范站是 agentskills.io。目前按同一格式支持 Skill 的平台包括:Claude Code、OpenAI Codex、GitHub Copilot、Microsoft Agent Framework 等。这意味着你为 Claude Code 写的一个 Skill 文件夹,理论上可以直接丢进 Codex 或 Copilot 的 skills 目录里复用,只要不依赖某个平台独有的元数据扩展。
其中值得单独拿出来讲的是 Microsoft Agent Framework 的设计。它把 Skill 的能力拆成两个显式动作:read_skill_resource(读取资源)与 run_skill_script(执行脚本)。这个二分不是随手起的名字,而正是我们后面要反复强调的核心机制——「读」会把内容灌进上下文、消耗 Token;「跑」只执行、只拿结果,几乎不占上下文。框架把这层区别提升成一等公民,等于在运行时强制你思考:这一步到底是要 Agent 知道,还是要 Agent 执行?
| 对比维度 | MCP | Agent Skills |
|---|---|---|
| 回答的问题 | 怎么连上外部工具与数据源 | 拿到工具后,如何把事做对做好 |
| 抽象层级 | 第一层(连接层) | 第二层(程序性知识层) |
| 典型载体 | 协议 + 服务端进程 | 文件夹 + SKILL.md |
| 执行安全性 | 较强,可由服务端沙箱托管 | 较弱,脚本在本机上下文执行 |
| 适用复杂度 | 重逻辑、需强隔离的任务 | 轻量脚本与简单逻辑 |
| 上下文开销 | 工具 schema 常驻 | 元数据常驻约 100 tokens/skill |
Skill 的本质:一个文件夹加一份 SKILL.md,封装的是程序性知识
把 Skill 的物理形态说透,其实就一句话:一个文件夹 + 一个 Markdown 文件(SKILL.md)。这个文件夹里装的是程序性知识(procedural knowledge),也就是「怎么做」;它刻意不装事实性知识(declarative knowledge),也就是「是什么」。
这个区分非常关键,因为它决定了你写 Skill 时的取舍标准。事实性知识——公司的产品参数、API 的字段用法、某个领域的背景概念——应该放在检索系统、文档站点或者 references/ 里按需取用;而 Skill 要装的是流程、步骤、判断规则、常见陷阱。举例来说,「我们的退款政策是 7 天无理由」是事实性知识,适合放文档;「处理退款争议时,先查订单时间,超过 7 天则要求上传开箱视频,否则直接放行」是程序性知识,这才是 Skill 该承载的内容。
更进一步:程序性知识里,能用确定性代码表达的,就不应该用自然语言表达。这是全篇最反直觉、也最有价值的一条原则,后面会用一小节专门展开。
目录约定拆解:SKILL.md 必需,scripts/、references/、assets/ 三者可选
官方约定的目录结构一共四类内容,但只有第一类是硬性必需:
- SKILL.md(必需):Skill 的入口。没有它,这个文件夹就不是 Skill。它承担元数据声明与主指令两项职责。
- scripts/(可选):可执行代码。典型场景是校验、格式化、数据转换、生成文件。它的价值在于把「判断」变成「执行」,是确定性工程的落点。
- references/(可选):按需参考文档。API 手册、领域背景、大段规则表的去处。它会被「读」进上下文,所以属于按需加载的重资源。
- assets/(可选):模板与静态资源。文件模板、图片、字体、样例输出等,供脚本或 Agent 直接取用。
这里有一个新手最容易踩的坑:一个目录算不算 Skill 根目录,判断依据是它是否直接包含 SKILL.md。很多人从压缩包或仓库下载下来,看到外层有个同名文件夹就整包拷进去,结果 Agent 在 skills 根目录下扫到的是「子文件夹套子文件夹」,找不到 SKILL.md,Skill 直接不生效。正确做法是:进入目录树,找到那个直接包含 SKILL.md 的层级,从那一层开始拷贝。
SKILL.md 结构:YAML frontmatter 元数据 + Markdown 正文指令
SKILL.md 由两段拼接而成,中间用三条短横线分隔,且 frontmatter 必须位于文件最开头:
- 元数据层(YAML frontmatter):告诉 Agent「这个 Skill 是什么、什么时候该用它」。它始终加载在上下文里,是常驻开销。
- 指令层(Markdown 正文):告诉 Agent「具体怎么执行」。它只在 Skill 被激活后才加载。
为什么必须分开?因为二者的加载时机完全不同。元数据是「目录」,必须永远在场,才能让 Agent 在恰当的时刻发现这个 Skill 值得激活;指令是「正文」,只在需要时才展开。如果你把关键触发信息写进正文,那它永远不会被加载——因为激活之前 Agent 根本看不到正文。反过来,如果你把大段操作步骤写进元数据,那每轮对话都要为它付 Token,纯属浪费。
下面是一段可直接改用的 SKILL.md 示例,frontmatter 与正文各司其职:
---
name: csv-schema-checker
description: 校验 CSV 数据的表头、编码与列类型,发现不符合约定的数据质量问题。当用户需要检查 CSV 文件格式、清洗脏数据、或在上传前做数据质量把关时使用。
license: MIT
compatibility: 需要 Python 3.10 及以上,依赖标准库 csv 与 chardet
metadata:
version: 1.2.0
owner: data-platform
allowed-tools: Bash, Read
description_keywords: CSV, 编码, 表头, 数据类型, 数据质量
---
# CSV 数据质量校验
## 何时使用
当用户要求检查、清洗或验证 CSV 文件的格式与数据质量时启用本 Skill。
## 执行流程
1. 先运行校验脚本,不要凭肉眼判断。
- 运行方式(意图为 run,不要读取脚本内容):
`python scripts/check_csv.py --path <文件路径>`
2. 脚本退出码为 0 时,说明校验通过,直接向用户报告 PASS,不要额外发挥。
3. 脚本退出码非 0 时,读取它的标准输出,每条报错都对应一个具体问题。
4. 根据报错修正数据源(可能是编码、表头顺序或列类型),然后重新运行脚本。
5. 循环执行第 3、4 步,直到脚本输出 PASS 为止。
## 硬性约束
- 严禁在没有跑脚本的情况下声称数据已通过校验。
- 严禁修改 scripts/check_csv.py 的判定逻辑来让校验通过。
- 表头白名单与类型规则统一定义在 references/schema.md,不要在正文里重复列举。
## 常见坑(gotchas)
- 带 BOM 的 UTF-8 文件常被误判为乱码,脚本已自动处理,不要再手动 strip。
- 日期列存在 `2026/3/1` 与 `2026-03-01` 混用的情况,脚本会统一归一化后再比较。
- 若文件超过 200MB,脚本会切换为流式读取,此时不输出行号,属正常现象。
字段约束清单:name 的 64 字符与父目录同名规则、description 的 1024 字符触发器
元数据不是随便写的注释,官方字段有明确的硬性校验,写错会被直接拒绝。逐条列出:
- name(必填):最多 64 字符;只允许小写字母、数字与连字符;不得以连字符开头或结尾;不得出现连续连字符(如
my--skill非法);必须与父目录名完全一致。最后这条最容易被忽略——你把文件夹叫csv-checker,里面的 name 写csv_schema_checker,就是不合法的。 - description(必填):最多 1024 字符。它不是简介,而是触发器。写法上必须包含能让 Agent 识别任务的触发关键词,覆盖用户可能的多种说法。
- compatibility(可选):最多 500 字符,用于声明运行环境、依赖、平台要求。
- license(可选):许可声明。
- metadata(可选):自定义键值对,放版本、负责人、团队等信息。
- allowed-tools(可选):允许该 Skill 调用的工具白名单。
关于 description 的写法,一个实用对照是:不要写「这是一个用于处理 CSV 的 Skill」,而应写「校验 CSV 的表头、编码与列类型……当用户需要检查 CSV 格式、清洗脏数据、上传前做数据质量把关时使用」。前者是自我介绍,后者是触发信号。1024 字符的额度其实相当宽裕,完全够你把触发场景铺开。
| 字段 | 必填 | 上限 | 关键约束 |
|---|---|---|---|
| name | 是 | 64 字符 | 小写字母/数字/连字符;首尾不能为连字符;不能连续连字符;与父目录同名 |
| description | 是 | 1024 字符 | 必须含触发关键词,当作触发器而非简介来写 |
| compatibility | 否 | 500 字符 | 声明环境与依赖要求 |
| license | 否 | 无硬性上限 | 许可声明 |
| metadata | 否 | 按需 | 版本、负责人等自定义键值 |
| allowed-tools | 否 | 按需 | 工具白名单,用于收窄权限 |
渐进式披露三层:常驻元数据、激活指令与按需资源的加载时序
渐进式披露(Progressive Disclosure)是 Skills 架构的灵魂。它把信息按「什么时候真的需要」分成三层,逐层展开:
- 第 1 层:元数据。始终加载,约 100 tokens/skill。它相当于一本书的目录——Agent 每轮对话都能看到所有已安装 Skill 的 name 与 description,从而判断该不该激活。因为只有约 100 tokens,即使装几十个 Skill,常驻开销也在可控范围内。
- 第 2 层:指令。只有当某个 Skill 被激活时才加载,也就是 SKILL.md 的 Markdown 正文。官方建议控制在 5k tokens 以内。这一层是流程与规则的主体。
- 第 3 层:资源。references/ 与 scripts/,是「按需中的按需」。即使 Skill 已激活,也不代表这些资源都要用上,只有在流程真正走到某一步时才去取。
这个设计解决的是一个根本矛盾:Agent 需要知道「有哪些能力」,但不能把所有能力的细节都背在上下文里。目录常驻、正文按需、资源再按需,等价于把上下文从「全量加载」改造成了「懒加载」。
而第 3 层内部又有一次关键分叉,必须讲清:
- references/ 是「读」(read):内容会被读进上下文,实实在在消耗 Token。所以它适合放「需要 Agent 理解」的资料,比如规则表、字段定义、领域背景。
- scripts/ 是「跑」(run):只执行,不读取内容,几乎不占用上下文。Agent 只需要知道怎么调用它,不需要知道它内部怎么实现。一个几百行的校验脚本,对上下文的成本可能只是指令里那一行命令行。
唯一的例外是:如果 SKILL.md 里没有写清脚本的运行方法,Agent 可能会为了搞懂怎么跑而主动去读代码,这时它就退化成了「读」,上下文开销立刻飙升。所以在指令里显式写明运行命令与意图(run 还是 read),不是可选项,而是省钱的关键。
Token 账本:把上下文当预算来花,SKILL.md 控制在 500 行以内
有了三层模型,就能算一笔账。假设你装了 30 个 Skill:光是常驻元数据就要约 3000 tokens。如果其中 5 个在对话中被激活,每个正文按 5k tokens 上限算,又是 25000 tokens。此时如果 references/ 还被无脑全量读入,上下文会迅速见底,模型的注意力也会被摊薄。
这正是官方建议 SKILL.md 控制在 500 行以内的工程含义——它不是排版洁癖,而是一条预算红线。用 把上下文当预算来花的视角看,文件越长,不是「信息越全」,而是「每条规则分到的注意力越少」。这就是所谓的注意力衰减:内容越多,单条规则的权重越弱;更糟的是规则失效——当规则与模型自身的倾向冲突时,模型会权衡后选择违反,因为对模型而言,自然语言规则是「建议」而非「命令」。
应对这个问题的根本手段,是把判断权从文字手里夺回来交给代码。提示词是概率性的,代码是确定性的。与其在正文里写「请仔细检查数据格式是否符合规范」,不如写「运行 python scripts/check_csv.py,退出码非 0 就修正后重跑,直到 PASS」。前者把正确性寄托在模型的自觉上,后者把正确性锚定在一个可复现的执行结果上。
#!/usr/bin/env python3
"""scripts/check_csv.py —— 确定性 CSV 校验脚本示例。
设计原则:退出码即结论,标准输出即修复清单。
- 退出码 0:全部通过,最后一行打印 PASS。
- 退出码 1:存在数据质量问题,逐条打印问题描述。
- 退出码 2:脚本自身参数或环境错误(这不是数据问题,不要重试)。
"""
import csv
import sys
import argparse
import unicodedata
from pathlib import Path
REQUIRED_HEADERS = ["order_id", "user_id", "amount", "created_at"]
ALLOWED_AMOUNT_CHARS = set("0123456789.-\u00a5\uffe5")
STREAM_THRESHOLD_BYTES = 200 * 1024 * 1024
def normalize_header(name: str) -> str:
"""去掉 BOM、全角空白与零宽字符,统一小写并去首尾空白。"""
cleaned = unicodedata.normalize("NFKC", name)
cleaned = cleaned.replace("\ufeff", "").replace("\u200b", "")
return cleaned.strip().lower()
def check_headers(fieldnames):
problems = []
actual = [normalize_header(f) for f in (fieldnames or [])]
if actual != REQUIRED_HEADERS:
problems.append(
f"表头不匹配: 期望 {REQUIRED_HEADERS}, 实际 {actual}"
)
return problems
def check_amount(raw: str, line_no: int):
value = (raw or "").strip().replace(",", "").lstrip("\u00a5\uffe5")
if not value:
return [f"第 {line_no} 行 amount 为空"]
if not set(value) <= ALLOWED_AMOUNT_CHARS:
return [f"第 {line_no} 行 amount 含非法字符: {raw!r}"]
try:
amount = float(value)
except ValueError:
return [f"第 {line_no} 行 amount 无法解析为数字: {raw!r}"]
if amount < 0:
return [f"第 {line_no} 行 amount 为负数: {amount}"]
return []
def check_file(path: Path):
problems = []
size = path.stat().st_size
stream_mode = size > STREAM_THRESHOLD_BYTES
with path.open("r", encoding="utf-8-sig", newline="") as fh:
reader = csv.DictReader(fh)
problems.extend(check_headers(reader.fieldnames))
for offset, row in enumerate(reader):
line_no = offset + 2 # 表头占一行,且行号从 1 开始
problems.extend(check_amount(row.get("amount", ""), line_no))
if stream_mode and len(problems) >= 20:
problems.append("已达问题上限,流式模式下不再输出后续行号")
break
return problems
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--path", required=True, help="待校验的 CSV 文件路径")
args = parser.parse_args()
target = Path(args.path)
if not target.is_file():
print(f"[环境错误] 文件不存在: {target}", file=sys.stderr)
return 2
try:
problems = check_file(target)
except UnicodeDecodeError as exc:
print(f"[环境错误] 文件编码无法解析: {exc}", file=sys.stderr)
return 2
if problems:
print("校验未通过,请逐条修复后重新运行:")
for item in problems:
print(f"- {item}")
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main())
这个脚本体现的正是「脚本 FAIL → 修正 → 重跑 → 直到 PASS」的闭环。注意它的退出码语义:0 表示业务通过、1 表示数据有问题需修复、2 表示环境或参数错误不该重试。把 2 和 1 分开是工程上的重要细节——否则 Agent 会对着一个路径写错的调用反复重试,白白烧掉轮次。
顺带回到安装环节的一个安全提醒:从互联网获取 Skill,本质上等于在自己机器上运行陌生人写的软件。Skill 可以携带 scripts/,也就可能携带破坏性命令或越权访问。官方仓库 github.com/anthropics/skills 的 skills 目录下,每个子目录就是一个 Skill;社群站 skillsmp.com 与开源集合 github.com/ComposioHQ/awesome-claude-skills 也是常见来源。无论从哪来,安装前通读 SKILL.md 与 scripts/ 内容这条底线都不能省。安装动作本身很简单——把整个 Skill 文件夹放进 Agent 约定的 skills 目录即可,例如 Claude Code 的 .claude/skills、Codex 安装目录下的 skills、OpenCode 项目内的 .opencode/skills——但往哪个目录拷、拷到哪一层,取决于你是否找到了那个直接包含 SKILL.md 的根目录。
上半部分已经把 Skill 的定位、SKILL.md 的格式与字段约束、渐进式披露的加载时序和预算模型讲完了。但还有一个更尖锐的问题没有正面回答:如果 description 没写好,Skill 再完美也不会被激活;如果正文与脚本的意图没写清,确定性就会退化成概率性。接下来进入真正决定一个 Skill 好不好用的地方——资源怎么分工、文字与代码的边界在哪里、以及安装之前必须先做什么。
references 是「读」、scripts 是「跑」:两类资源的分工与上下文代价
回到目录约定:SKILL.md 必需,scripts/、references/、assets/ 三者可选。很多人把 references 和 scripts 当成同一类东西——「额外的文件」。它们在机制层面完全不同,区别只有一个字:读还是跑。
references/ 是「读」。当 Agent 判断需要参考某份文档时,它会用读取文件的动作把内容拿进来,这份内容会进入上下文窗口,按字计费地消耗 Token。这意味着 references 里放的东西和你写在 SKILL.md 正文里的东西,在成本模型上是同一类东西,只是加载时机被推迟到了「按需」。所以 references 的价值不是「省 Token」,而是「不在不需要的时候花 Token」。
scripts/ 是「跑」。Agent 调用执行动作去运行这个脚本,脚本本身的内容不进入上下文,只有它的标准输出、错误信息、退出码会回到对话里。一个 300 行的校验脚本和一个 3 行的脚本,如果输出都是同样的一行 PASS,对上下文的占用几乎一样。这就是 Skill 里最被低估的一个杠杆:把复杂度关进脚本里。
但这里有一个非常常见的工程坑:如果 SKILL.md 里没写清楚这个脚本该怎么运行、需要什么参数,Agent 很可能会先去「读」这个脚本源码,试图自己搞明白。一旦发生这种情况,scripts 的全部上下文优势当场消失,退化成一份被全文读入的 references。所以正确做法是:脚本的调用方式必须在 SKILL.md 正文里写成可直接照抄的命令行,包括解释器、参数顺序、输入输出约定、以及失败时该怎么做。
另一类坑是把脚本写成「需要人看着跑」的交互式程序——等待输入、打印进度条、依赖当前工作目录。Agent 执行脚本时是非交互的,这类脚本要么挂住,要么静默失败。约定是:脚本必须是一次性、幂等、非交互、退出码可判定。
| 维度 | references/(读) | scripts/(跑) |
|---|---|---|
| Agent 动作 | 读取文件内容 | 执行代码 |
| 是否进入上下文 | 是,全文进入 | 否,只有 stdout / stderr / 退出码返回 |
| Token 成本 | 与文档长度成正比 | 与输出长度成正比,与代码长度无关 |
| 确定性 | 低,属于给模型的参考信息 | 高,同一输入同一结果 |
| 适合放什么 | API 参考、字段字典、长篇规范、领域背景 | 格式校验、数据转换、调用外部接口、生成产物 |
| 常见误用 | 把每次都要用的核心规则塞进去 | 忘了写运行命令,导致 Agent 去读源码 |
一句话总结两者的分工:references 影响「模型怎么想」,scripts 决定「系统怎么动」。凡是需要模型理解、权衡、表达的内容,放 references;凡是答案唯一、对错可判定的内容,放 scripts。
从 Prompt 到确定性脚本:把能用代码判断的事从文字里拿出来
要理解 Skill 的工程价值,先要接受一个前提:提示词是概率性的,代码是确定性的。你写「请确保 JSON 字段完整」,模型可能检查,也可能不检查,还可能检查了却漏掉某一层嵌套。你写「请仔细检查一下」,这句话的期望收益接近于零——它没有定义什么叫「仔细」,也没有定义什么叫「通过」。
确定性脚本把这件事变成机械流程:脚本FAIL → 修正 → 重跑 → 直到 PASS。这个闭环之所以远强于一句叮嘱,原因有三:
- 它有明确的成功判据。退出码非零就是没做完,不存在「我觉得应该没问题了」。模型的自我评估天然偏乐观,而退出码不会。
- 它把注意力从「检查」转移到「修正」。模型不需要在长上下文里持续保持警觉,只需要看到具体的错误行,改掉,再跑一次。每一步都是短反馈。
- 它可复现。同一个输入跑两次结果一致,这意味着出问题时你能定位,而不是重启一次祈祷它变好。
所以好 Skill 的核心原则可以浓缩成一句:把所有能用代码判断的事,从文字里拿出来交给脚本。判断「能不能用代码判断」的标准很简单——如果两个工程师对同一个输出是否会给出相同结论,那它就该是代码的事。
下面是一份可用的 SKILL.md 示例。它封装的是「把外部数据整理成规范 JSON 并校验」这个技能,注意它如何把「判断」留给脚本、把「解释」留给文字。
---
name: json-schema-guard
description: 校验并修复 JSON 数据文件,使其符合项目 schema。当用户要求检查 JSON 格式、修复字段缺失、统一输出结构,或提到 schema 校验、字段对齐、数据清洗时使用。
license: MIT
compatibility: 需要 Python 3.10+ 与 jsonschema 包
metadata:
author: platform-team
version: 1.1.0
allowed-tools: read_file, write_file, run_skill_script
---
# JSON Schema Guard
把任意 JSON 数据规范成符合 `assets/schema.json` 的结构,并用脚本验证,直到通过为止。
## 何时使用
当任务是「数据格式不对」「字段缺失」「输出结构要和 schema 对齐」时使用本技能。
若只是读取 JSON 内容做展示,不需要本技能。
## 执行流程
1. 运行校验脚本,查看当前失败项:
`python scripts/validate.py --input data.json --schema assets/schema.json`
2. 脚本以非零退出码结束,并逐行打印 `PATH: 原因` 形式的错误。
3. 按错误行逐条修改 `data.json`,只改被指出的路径,不要重写整个文件。
4. 重新运行同一条命令,直到退出码为 0 且输出 `PASS`。
5. 只有在脚本 PASS 之后,才向用户报告完成。
## 脚本约定
- `scripts/validate.py` 是幂等只读校验,不会修改输入文件。
- 不要读取 `scripts/validate.py` 源码来推断规则,规则以 `assets/schema.json` 为准。
- 需要查看字段含义时,读 `references/fields.md`,不要猜。
## 边界
- 不修改 schema 本身。若 schema 与数据冲突,先向用户确认。
- 不处理非 UTF-8 编码文件,直接报告并停止。
## Gotchas
- `assets/schema.json` 中的 `additionalProperties` 为 false,多一个字段也会失败,删除比新增安全。
- 空数组与字段缺失是两种不同错误,不要用 `null` 兜底。
- 脚本对相对路径敏感,请始终从 Skill 根目录执行。
注意这份文件里的几个设计选择:正文没有解释 JSON Schema 的原理(那是 references 的事),没有罗列每个字段的含义(放进了 references/fields.md),但把「运行哪条命令」「失败长什么样」「什么时候才算完成」「哪些坑不能再踩」写死了。gotchas 这一节往往是整份 SKILL.md 里最值钱的部分,因为它来自真实踩坑,而不是文档复述。
第二段示例是一个真正的确定性校验脚本。它的价值不在于逻辑复杂,而在于输出对人类和 Agent 都直接可操作:
#!/usr/bin/env python3
"""幂等只读校验:检查 data.json 是否符合 schema.json。
退出码:0 = 通过;1 = 校验失败;2 = 使用方式或环境错误。
"""
import argparse
import json
import sys
from pathlib import Path
try:
from jsonschema import Draft202012Validator
except ImportError:
print("ERROR: 缺少 jsonschema,请先 pip install jsonschema", file=sys.stderr)
sys.exit(2)
def load_json(path: Path):
if not path.exists():
return None, f"文件不存在: {path}"
try:
return json.loads(path.read_text(encoding="utf-8")), None
except UnicodeDecodeError:
return None, f"不是 UTF-8 编码: {path}"
except json.JSONDecodeError as exc:
return None, f"JSON 语法错误 第 {exc.lineno} 行 第 {exc.colno} 列: {exc.msg}"
def main():
parser = argparse.ArgumentParser(description="校验 JSON 数据是否符合 schema")
parser.add_argument("--input", required=True)
parser.add_argument("--schema", required=True)
args = parser.parse_args()
data, err = load_json(Path(args.input))
if err:
print(f"FAIL\n{err}", file=sys.stderr)
sys.exit(1)
schema, err = load_json(Path(args.schema))
if err:
print(f"FAIL\nschema 无法加载: {err}", file=sys.stderr)
sys.exit(1)
try:
Draft202012Validator.check_schema(schema)
except Exception as exc:
print(f"FAIL\nschema 本身不合法: {exc}", file=sys.stderr)
sys.exit(2)
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(data), key=lambda e: list(e.path))
if not errors:
print("PASS")
sys.exit(0)
for err in errors:
path = "/".join(str(p) for p in err.path) or "(root)"
print(f"{path}: {err.message}")
print(f"共 {len(errors)} 处问题", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
这段脚本里有三个容易被忽略但很关键的细节:退出码分三档,让 Agent 能区分「数据错了,去改数据」和「环境错了,别瞎改」;错误按路径排序输出,让模型可以稳定地从第一条开始改;显式禁止非 UTF-8 输入,避免出现「模型改了半天,其实是编码问题」。这些都不是算法难点,而是把不确定性钉死的工程习惯。
注意力衰减与规则失效:纯文字 Skill 就是大号 Prompt 的两个天花板
如果把所有逻辑都写在 SKILL.md 正文里,那你得到的其实就是一个大号 Prompt。它不会因为换了文件格式就获得额外能力,反而会撞上两个天花板。
第一个是注意力衰减。上下文里的内容越多,每条规则分到的注意力就越弱。这不是模型偷懒,而是机制上的必然:十条规则各有各的位置,一百条规则里排在中间的那些就很容易被忽略。表现是——规则写在 SKILL.md 里,模型也「看见了」,但执行时只遵守了开头和结尾的几条。官方因此建议 SKILL.md 控制在 500 行以内,第二层指令控制在 5k tokens 以内,这不是美学要求,而是为了让每条规则都还能被分到足够的注意力。
第二个是规则失效。当模型的判断和规则冲突时,它会权衡,然后可能选择违反规则——因为对模型而言,规则是建议而不是命令。「不要改动这个文件」是一句建议;一个在脚本里被校验、失败就退出非零的约束,才是命令。这也是为什么把规则写进代码比写进文字更可靠:文字是被理解的,代码是被执行的。
两个天花板叠加的结果,是长 Prompt 的老问题:越多规则 ≠ 越可靠,反而可能越低。工程上的应对不是「写得更清楚」,而是分层——把「每次都要遵守的核心原则」写进精简的正文,把「特定场景才用到的细节」移入 references,把「对错可判定的检查」移入 scripts。这也正好呼应前面说的把上下文当预算。
与 MCP 的边界:MCP 供食材、Skill 给配方,互补而非替代
Skills 出现之后,最常见的困惑是「它是不是要取代 MCP」。答案是明确的:不是,两者在抽象层上管的事不同。
MCP 解决的是「连接外部工具与数据源」——把数据库、API、文件系统、SaaS 服务接进 Agent 能调用的范围。打个比方,MCP 提供食材:它让 Agent 能够拿到数据、调用远端能力,重点在「连得上、调得到」。
Skill 解决的是「如何加工这些数据」——拿到食材之后该怎么切、按什么顺序下锅、成品长什么样。Skill 给的是配方:程序性知识,重点在「做得对、做得稳」。
| 对比维度 | MCP | Agent Skills |
|---|---|---|
| 解决的问题 | 连接外部工具与数据源 | 如何加工数据、如何完成任务 |
| 类比 | 提供食材 | 提供配方 |
| 载体 | 协议与服务器 | 文件夹 + SKILL.md |
| 擅长 | 标准化接入、鉴权、远程调用 | 轻量脚本、简单逻辑、流程编排 |
| 弱项 | 承载复杂业务流程需要额外设计 | 代码执行的安全性与稳定性不及 MCP |
| 关系 | 互补而非替代。典型形态是 Skill 在流程里调用 MCP 工具 | |
要诚实地看待 Skill 的短板:它本质上是「文件 + 脚本」的轻量约定,在代码执行的安全性、隔离性和稳定性上不及 MCP。MCP 服务器可以集中做鉴权、限流、审计和沙箱,而 Skill 的脚本就是在你的环境里跑。所以合理的边界是:轻量、本地、一次性的处理交给 Skill;需要鉴权、多租户、强隔离、集中治理的能力留给 MCP。一个成熟系统里,两者通常是嵌套关系:Skill 定义流程,流程中的关键步骤调用 MCP 工具完成。
三类高频误区:SKILL.md 越长越好、资料全塞正文、忽略 description
在看过足够多的 Skill 之后,问题基本集中在三类上。
误区一:SKILL.md 越长越好。很多人下意识觉得写得越详尽,模型表现就越好,于是把 SKILL.md 写成几千行的百科。结果是稀释了重点,还白白占用了 Token——因为正文是第二层,一旦 Skill 被激活就会整份加载。规则越多单条越弱,核心步骤反而被淹没。正确姿势是:正文只保留执行流程、脚本调用方式、边界和 gotchas,其他一切移出。官方建议的 500 行上限应当视为硬约束而不是参考值。
误区二:把所有资料都塞进 SKILL.md。典型症状是把 API 全量字段表、历史背景、名词解释统统写进正文。这类内容的特点是「特定场景才用得到」,天然属于 references/。判断标准是:如果这段内容不是每次执行都需要,它就不该出现在正文里。同理,能写成检查的不要写成说明。
误区三:忽略 description。这是最隐蔽也最致命的一条。很多作者把 description 当成「简介」,写得像摘要:"一个用于数据处理的有用技能。" 但 description 不是简介,而是触发器。它是在第一层始终加载的那部分内容(约 100 tokens/skill),模型靠它来判断「这个任务要不要激活这个 Skill」。所以 description 里必须出现帮助 Agent 识别任务的触发关键词:用户可能怎么说、涉及哪些名词、什么场景该用、什么场景不该用。写摘要等于让这个 Skill 永远不被选中,即使它内部写得再好。规范上它最多 1024 字符,这个额度就是给你写触发条件的,不用是浪费。
获取、安装与根目录判定:从官方仓库到 .claude/skills、.opencode/skills
Skill 的获取渠道主要有三类:官方仓库 github.com/anthropics/skills,其中的 skills 目录下每个子目录就是一个 Skill;社群聚合站 skillsmp.com;以及开源集合仓库 github.com/ComposioHQ/awesome-claude-skills。由于 Agent Skills 已经是跨平台开放标准(规范站为 agentskills.io),同一份 Skill 在 Claude Code、OpenAI Codex、GitHub Copilot、Microsoft Agent Framework 等平台上都能被识别——例如 Microsoft Agent Framework 就把能力明确拆成「读取资源 read_skill_resource」与「执行脚本 run_skill_script」两个动作,和前面讲的「读 / 跑」分工是同一套模型。
安装方式简单到有点反直觉:把整个 Skill 文件夹原样放进 Agent 约定的 skills 目录即可,不需要构建、不需要注册。常见位置包括 Claude Code 的 .claude/skills、Codex 安装目录下的 skills、OpenCode 项目内的 .opencode/skills。
这里有一个非常实用、但文档里常常一句话带过的问题:从压缩包或仓库里拷贝时,到底该拷哪一层文件夹?判定方法只有一条——看哪个目录直接包含 SKILL.md,那个目录就是真正的 Skill 根目录。如果解压后得到的是 my-skill-main/json-schema-guard/SKILL.md,那么要拷的是 json-schema-guard 这一层,而不是最外层的 my-skill-main。拷错一层的后果是 Agent 在约定目录下扫描不到 SKILL.md,Skill 静默失效。顺带一提,这也解释了为什么规范要求 name 必须与父目录名一致——目录名本身就是定位的一部分。
顺便把元数据的硬性约束集中列一遍,因为写 Skill 时踩的坑大多在这里:name 必填,最多 64 字符,仅允许小写字母、数字和连字符,不得以连字符开头或结尾,不得出现连续连字符,且必须与父目录名一致;description 必填,最多 1024 字符,应包含帮助 Agent 识别任务的触发关键词;compatibility 可选,最多 500 字符;license 与 metadata 可选;allowed-tools 可选,用于声明该 Skill 运行时需要哪些工具权限。
安装前先审计:装一个 Skill 等于在本机运行陌生人的软件
最后这一条没有商量余地。Skill 不是一个纯配置文件,它可以携带脚本,而脚本会在你的环境里被执行。所以从互联网安装一个 Skill,本质上等于在自己机器上运行陌生人的软件——只是它的分发形式看起来像几个 Markdown 文件,容易让人放松警惕。
这正是工程界总结的第五条准则「运行之前先审计」所针对的场景。落到操作上,安装前至少要做这几件事:
- 通读 SKILL.md 全文,特别是执行流程和脚本调用部分,确认真实会执行什么命令。
- 逐行审查 scripts/ 下的所有代码,不要因为「只是个校验脚本」就跳过。重点看是否存在删除、覆盖、批量改写文件的操作。
- 排查破坏性命令:递归删除、格式化磁盘、清空目录、覆盖系统配置、无参数执行的危险命令。
- 排查越权访问:读取 SSH 密钥、环境变量、凭证文件、浏览器数据,向外部地址发送数据。
- 确认网络行为:脚本是否发起外部请求、请求发送到哪里、发送了什么。
- 确认依赖来源:是否在执行时动态安装包、是否从可疑源拉取代码。
- 在隔离环境先跑一遍,尤其是来源非官方的 Skill。
还要注意一个容易忽视的传染性风险:Skill 的 references 也可能被间接用作攻击载体——一份看似无害的参考文档里如果写着「遇到某情况请执行某命令」,而这条内容被模型当作指令采纳,就越过了你自己的判断。因此审计范围应当覆盖整个 Skill 文件夹,而不只是 scripts。一句话原则:任何来自外部的 Skill,在你能读懂它要做什么之前,不要让它跑起来。
总结与最佳实践
把这篇指南压缩成一份可执行清单,写 Skill 和装 Skill 时都可以直接照着核对:
- 定位先想清楚:Skill 封装的是程序性知识(怎么做),不是事实性知识(是什么)。如果是纯知识,考虑放 references 或干脆不进 Skill。
- 目录只放该放的:SKILL.md 必需;scripts/ 放可执行代码,references/ 放按需文档,assets/ 放模板与静态资源。
- 元数据守规矩:name 最多 64 字符、小写字母数字连字符、不首尾连字符、无连续连字符、与父目录名一致;description 最多 1024 字符且必须包含触发关键词;compatibility 最多 500 字符;license、metadata、allowed-tools 按需。
- 把 description 当触发器写:写清什么任务该激活、用户可能怎么描述、什么场景不该用。它是第一层常驻内容,约 100 tokens/skill,写摘要等于自废武功。
- 控制长度:SKILL.md 不超过 500 行;第二层指令尽量压在 5k tokens 以内。长内容进 references,别进正文。
- 区分读与跑:需要模型理解权衡的放 references(进上下文、耗 Token);对错可判定的放 scripts(只执行、几乎不占上下文)。
- 别让 Agent 读脚本源码:在 SKILL.md 里写清楚脚本的完整运行命令、参数、输入输出与失败处理,并注明意图是 run 还是 read。
- 把判断交给代码:能用代码判定的一律写成脚本,走「FAIL → 修正 → 重跑 → PASS」闭环,用「请仔细检查」的地方全部替换掉。
- 脚本要工程化:幂等、非交互、退出码可分档、错误信息带路径、从 Skill 根目录执行、只处理声明的编码。
- 用分层对抗注意力衰减:正文只留核心流程与边界,细节下沉到 references,检查下沉到 scripts。规则写在文字里是建议,写在代码里才是命令。
- 理清与 MCP 的边界:MCP 供食材(连接工具与数据源),Skill 给配方(如何加工)。轻量脚本用 Skill;需要鉴权、隔离、审计、集中治理的用 MCP。两者互补,常嵌套使用。
- 认真写 gotchas:正文里最值钱的一节是踩坑记录,它来自真实经验,而不是文档复述。
- 把上下文当预算花:每加一段文字、每读一份 references,都在消耗那份有限的注意力额度。加之前先问:这真的每次都要用吗?
- 安装要整包放对:官方仓库 github.com/anthropics/skills、社群站 skillsmp.com、开源集合 github.com/ComposioHQ/awesome-claude-skills 是主要来源;整个文件夹放进 .claude/skills、Codex 的 skills 目录或 .opencode/skills。
- 用 SKILL.md 判定根目录:哪个目录直接包含 SKILL.md,哪个就是 Skill 根目录,压缩包只拷那一层。拷错一层,Skill 会静默失效。
- 运行之前先审计:通读 SKILL.md 与 scripts/ 全部内容,排查破坏性命令与越权访问,确认网络行为与依赖来源,来源可疑先在隔离环境跑。
收束成一句话:Skill 的竞争力不在于你写了多少字,而在于你把多少不确定的东西钉成了确定的东西。让 description 负责被选中,让正文负责讲清流程,让 references 负责按需补充,让 scripts 负责给出不会说谎的结论——最后在安装别人写的 Skill 之前,先像审查代码一样审查它。