如果你已经在用 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 执行?

对比维度MCPAgent 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 字符的额度其实相当宽裕,完全够你把触发场景铺开。

字段必填上限关键约束
name64 字符小写字母/数字/连字符;首尾不能为连字符;不能连续连字符;与父目录同名
description1024 字符必须含触发关键词,当作触发器而非简介来写
compatibility500 字符声明环境与依赖要求
license无硬性上限许可声明
metadata按需版本、负责人等自定义键值
allowed-tools按需工具白名单,用于收窄权限

渐进式披露三层:常驻元数据、激活指令与按需资源的加载时序

渐进式披露(Progressive Disclosure)是 Skills 架构的灵魂。它把信息按「什么时候真的需要」分成三层,逐层展开:

  1. 第 1 层:元数据。始终加载,约 100 tokens/skill。它相当于一本书的目录——Agent 每轮对话都能看到所有已安装 Skill 的 name 与 description,从而判断该不该激活。因为只有约 100 tokens,即使装几十个 Skill,常驻开销也在可控范围内。
  2. 第 2 层:指令。只有当某个 Skill 被激活时才加载,也就是 SKILL.md 的 Markdown 正文。官方建议控制在 5k tokens 以内。这一层是流程与规则的主体。
  3. 第 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 给的是配方:程序性知识,重点在「做得对、做得稳」。

对比维度MCPAgent 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 文件,容易让人放松警惕。

这正是工程界总结的第五条准则「运行之前先审计」所针对的场景。落到操作上,安装前至少要做这几件事:

  1. 通读 SKILL.md 全文,特别是执行流程和脚本调用部分,确认真实会执行什么命令。
  2. 逐行审查 scripts/ 下的所有代码,不要因为「只是个校验脚本」就跳过。重点看是否存在删除、覆盖、批量改写文件的操作。
  3. 排查破坏性命令:递归删除、格式化磁盘、清空目录、覆盖系统配置、无参数执行的危险命令。
  4. 排查越权访问:读取 SSH 密钥、环境变量、凭证文件、浏览器数据,向外部地址发送数据。
  5. 确认网络行为:脚本是否发起外部请求、请求发送到哪里、发送了什么。
  6. 确认依赖来源:是否在执行时动态安装包、是否从可疑源拉取代码。
  7. 在隔离环境先跑一遍,尤其是来源非官方的 Skill。

还要注意一个容易忽视的传染性风险:Skill 的 references 也可能被间接用作攻击载体——一份看似无害的参考文档里如果写着「遇到某情况请执行某命令」,而这条内容被模型当作指令采纳,就越过了你自己的判断。因此审计范围应当覆盖整个 Skill 文件夹,而不只是 scripts。一句话原则:任何来自外部的 Skill,在你能读懂它要做什么之前,不要让它跑起来

总结与最佳实践

把这篇指南压缩成一份可执行清单,写 Skill 和装 Skill 时都可以直接照着核对:

  1. 定位先想清楚:Skill 封装的是程序性知识(怎么做),不是事实性知识(是什么)。如果是纯知识,考虑放 references 或干脆不进 Skill。
  2. 目录只放该放的:SKILL.md 必需;scripts/ 放可执行代码,references/ 放按需文档,assets/ 放模板与静态资源。
  3. 元数据守规矩:name 最多 64 字符、小写字母数字连字符、不首尾连字符、无连续连字符、与父目录名一致;description 最多 1024 字符且必须包含触发关键词;compatibility 最多 500 字符;license、metadata、allowed-tools 按需。
  4. 把 description 当触发器写:写清什么任务该激活、用户可能怎么描述、什么场景不该用。它是第一层常驻内容,约 100 tokens/skill,写摘要等于自废武功。
  5. 控制长度:SKILL.md 不超过 500 行;第二层指令尽量压在 5k tokens 以内。长内容进 references,别进正文。
  6. 区分读与跑:需要模型理解权衡的放 references(进上下文、耗 Token);对错可判定的放 scripts(只执行、几乎不占上下文)。
  7. 别让 Agent 读脚本源码:在 SKILL.md 里写清楚脚本的完整运行命令、参数、输入输出与失败处理,并注明意图是 run 还是 read。
  8. 把判断交给代码:能用代码判定的一律写成脚本,走「FAIL → 修正 → 重跑 → PASS」闭环,用「请仔细检查」的地方全部替换掉。
  9. 脚本要工程化:幂等、非交互、退出码可分档、错误信息带路径、从 Skill 根目录执行、只处理声明的编码。
  10. 用分层对抗注意力衰减:正文只留核心流程与边界,细节下沉到 references,检查下沉到 scripts。规则写在文字里是建议,写在代码里才是命令。
  11. 理清与 MCP 的边界:MCP 供食材(连接工具与数据源),Skill 给配方(如何加工)。轻量脚本用 Skill;需要鉴权、隔离、审计、集中治理的用 MCP。两者互补,常嵌套使用。
  12. 认真写 gotchas:正文里最值钱的一节是踩坑记录,它来自真实经验,而不是文档复述。
  13. 把上下文当预算花:每加一段文字、每读一份 references,都在消耗那份有限的注意力额度。加之前先问:这真的每次都要用吗?
  14. 安装要整包放对:官方仓库 github.com/anthropics/skills、社群站 skillsmp.com、开源集合 github.com/ComposioHQ/awesome-claude-skills 是主要来源;整个文件夹放进 .claude/skills、Codex 的 skills 目录或 .opencode/skills。
  15. 用 SKILL.md 判定根目录:哪个目录直接包含 SKILL.md,哪个就是 Skill 根目录,压缩包只拷那一层。拷错一层,Skill 会静默失效。
  16. 运行之前先审计:通读 SKILL.md 与 scripts/ 全部内容,排查破坏性命令与越权访问,确认网络行为与依赖来源,来源可疑先在隔离环境跑。

收束成一句话:Skill 的竞争力不在于你写了多少字,而在于你把多少不确定的东西钉成了确定的东西。让 description 负责被选中,让正文负责讲清流程,让 references 负责按需补充,让 scripts 负责给出不会说谎的结论——最后在安装别人写的 Skill 之前,先像审查代码一样审查它。