如果你最近在折腾 Agent,从“能聊天”跨到“能干活”那一步,大概率绕不开 Skill 这套东西。Anthropic 官方推出 Agent Skills 规范之后,社区里一下子冒出来大量 Skill 仓库,但很多仓库只是应景地写了一个 SKILL.md,目录怎么摆、脚本怎么接、描述怎么填、多步任务怎么组织,并没有形成一套可复用的思路。我按官方规范拆过不少示例,也把自己手上几个文档处理流程改造成 Skills 结构,跑了两个迭代之后,基本沉淀出了一套目前看比较主流、也比较稳的结构设计方案,这篇就把它完整梳理一遍。
这套内容适合正在做智能体应用的人、准备把提示词工程升级成技能包的人,以及看了官方规范但不知道从哪下手落地的开发者。理解 Skill 的正确姿势,不是把它当成一个插件框架,而是当成一套“带说明书的能力文件夹”:模型在需要的时候加载说明书,按说明书调用脚本、读取参考、输出结构化结果。
1. 先理清一个底层问题:Skill 到底是什么形态的存在
1.1 它不是插件,也不是提示词,而是一个可执行目录
一开始我很自然地把它理解成“高级提示词”,毕竟里面也是 Markdown,也是给大模型看的指令。但实际跑过之后会发现,两者有本质区别:提示词是被动拼接在上下文里的文字,Skill 则是可以主动调用的能力单元,它的载体是一个目录,入口是 SKILL.md,能力可以是脚本、参考文档和示例文件。
把 Skill 理解成插件同样不对。插件往往要接 SDK、要管理生命周期、要和宿主进程深度耦合;Skill 不需要编译、不需要注册中心,本质上就是一个文件夹。你可以把它想象成给新员工准备的任务卡片:卡片正面写“什么情况找你能解决”,背面写“具体怎么做,出问题找谁”,旁边还挂着一套顺手可用的工具。模型就是那个新员工,看到任务卡片后决定要不要接这个活,接了就按背面流程走。
这种形态带来的好处非常明显:能力可以随目录分发、随 Git 版本管理、随场景按需挂载。你不需要为了多一个技能重写一遍系统,只需要在调用时把 Skill 目录路径传进去。
1.2 官方规范里真正硬性的只有 SKILL.md
官方规范把很多东西都定义得很克制。一个最小合法 Skill,目录结构可以简单到这样:
skills/any-skill-name/ └── SKILL.md其中 SKILL.md 的文件名是固定的,必须全大写;文件内部用 YAML frontmatter 声明元信息,正文部分写操作说明。目录名、frontmatter 里的 name 字段、description 字段,这几个地方符合规范,你的 Skill 就能被解析和加载。
但“符合规范”和“好用”之间还有很长的路。scripts、references、examples、tests 这些目录都不是官方硬性要求,却是主流工程化结构中真正拉开差距的部分。所以下面我先说清楚 SKILL.md 的字段和正文怎么写,再给目录骨架的选型方案。
| 项目 | 官方要求 | 我建议 |
|---|---|---|
| 目录名 | 与 name 一致,建议小写短横线 | 小写短横线,能看出功能 |
| SKILL.md | 必须,文件名固定大写 | 一个目录只放一个 |
| frontmatter name | 必须 | 与目录名完全一致 |
| frontmatter description | 必须 | 描述触发条件、动作、边界 |
| allowed-tools | 可选 | 能不给就不给,避免滥用 |
| scripts/references | 可选 | 按场景决定,别贪多 |
2. SKILL.md 的字段与正文布局:先过一遍不容易错的写法
2.1 frontmatter 是模型“要不要用你”的第一判断依据
一个典型的 SKILL.md 开头长这样:
--- name: doc-converter description: 将 Word、PPT、PDF 文档转换为 Markdown,并保留标题层级、表格、图片引用;当用户上传办公文档要求转换格式或整理内容时使用。 allowed-tools: - Bash - Read ---这里最容易被轻视的是 name。它不只是标识符,还承担了工具命名的职责。按照官方规范,name 应该和目录名保持一致,并且我建议一律使用小写字母加短横线,例如invoice-extractor,而不是发票提取工具V2。原因有两个:一是模型在处理工具列表时对短标识更敏感,二是你以后在日志、调试、回归测试里看到一个干净的技能名,会比看到一串中文加版本号舒服得多。
接下来是 description。这是整个 SKILL.md 里最重要的字段,因为模型判断“这个场景要不要用这个 Skill”时,主要依赖的就是 name 和 description。description 写得模糊,能力再强也容易被晾在一边。
举个例子,处理PDF文件这种描述基本等于没有。更好的写法是:
description: 从 PDF 文档中提取正文、表格和扫描图片中的文字,并输出为 Markdown;当用户上传 PDF、需要阅读 PDF 内容、或者要求把 PDF 转成可编辑格式时使用。它把输入(PDF 文档)、动作(提取正文、表格、OCR)、输出(Markdown)、触发场景(上传/阅读/转换)都包含了。
2.2 allowed-tools 是能力白名单,不是“顺手授权”
官方规范里的 allowed-tools 字段,很多人理解成“告诉模型我可以用什么工具”,实际上它更像给 Skill 的执行环境做能力白名单。你在这个字段里声明了 Bash,Skill 才能调用脚本;声明了 Read,模型在执行过程中才能读取 references 里的文件。
这里有个原则:给到最小必要权限。如果 Skill 只需要跑一个脚本,那就只声明 Bash;如果脚本还要读取配置文件,再把 Read 加进来。省略这个字段意味着 Skill 默认没有外部工具能力,这一点对“纯知识型”技能其实很合适,不会让模型在执行中乱跑脚本。
2.3 正文按“决策—操作—边界—故障”四段组织
frontmatter 决定的是“要不要用”,正文决定的是“用得稳不稳”。我见过很多人在正文里堆大段背景介绍、原理说明,从阅读体验上没问题,但对模型来说,这就是噪音。
我的建议是正文固定采用下面这种结构:
- 适用场景:用一两句话说清楚什么时候用这个 Skill。
- 操作流程:用编号列出步骤,包含确切的命令和输入输出。
- 输出结构:给出明确的输出格式、JSON Schema 或文件路径。
- 不要做的事:写明边界,防止模型越权或误用。
- 故障处理:脚本失败、文件不存在时应该怎么排查。
比如:
# 发票字段抽取 当用户提供 pdf/png/jpg 格式的发票图片或文件,且需要提取发票信息时使用。 ## 操作流程 1. 使用 Read 确认输入文件存在。 2. 运行以下命令: python3 scripts/extract_invoice.py --input <文件路径> --output /tmp/invoice_result.json 3. 读取 /tmp/invoice_result.json,按“输出结构”校验字段。 ## 输出结构 { "invoice_no": "发票号码", "date": "开票日期", "amount": 0.00, "seller_tax_no": "销售方税号" } ## 不要做的事 - 不要修改原始发票文件 - 输入不是发票时不要使用 ## 故障处理 - 脚本返回非零退出码,优先检查输入文件是否为可读图片或 PDF。这套结构写下来,模型在执行时很少会跑偏。它不是给人看的文档,而是给模型看的标准作业程序。
3. 三种主流目录骨架:从最小可用到工程化
我拆过不少 Skill 仓库,也自己踩过不少弯,最后发现所谓“主流结构设计方案”,其实不是一套万能模板,而是三种骨架按场景做选择。
3.1 知识规则型:只有一个 SKILL.md 就够
如果你的技能核心是“判断规则”和“话术模板”,不涉及任何脚本执行,那完全没必要建 scripts 目录。比如一个“客户投诉分类规则”技能,所有分类标准、优先级、输出话术都可以写进 SKILL.md 正文。
skills/support-classifier/ └── SKILL.md这种结构简单到极致,维护成本也最低。唯一要注意的是:不要把需要模型“临时去查”的内容放到外部文件。知识型技能的关键内容应该直接写在正文里,否则模型还得通过 Read 去翻文件,体验会差很多。
3.2 单脚本工具型:一个 Skill 对应一个确定操作
当技能的核心是一个明确的可执行操作,比如“把 Markdown 清洗成统一格式”“把 JSON 转成 CSV”,目录结构就变成:
skills/markdown-cleaner/ ├── SKILL.md └── scripts/ └── clean.pySKILL.md 里重点写清楚命令行怎么调,参数是什么,输出写到哪里。脚本本身不需要特别复杂的逻辑,但一定要有清晰的 CLI 接口和退出码,因为调用它的是模型,不是懂代码的人类用户。模型传错参数时,你的脚本要用argparse给出明确错误提示,而不是抛出一串 traceback 让模型自己猜。
3.3 复合任务型:多步骤、多参考资料时的完整骨架
如果技能要处理的是“发票抽取 + 字段映射 + 异常校对 + 结果校验”这类多步骤任务,就需要更完整的结构:
skills/invoice-extractor/ ├── SKILL.md ├── scripts/ │ ├── extract_invoice.py │ ├── ocr_preprocess.py │ └── requirements.txt ├── references/ │ ├── field_mapping.yaml │ └── exceptions.md ├── examples/ │ ├── sample_input.pdf │ └── expected_output.json └── tests/ ├── cases.json └── run_tests.shscripts放可执行程序,步骤越多越要把复杂逻辑从模型上下文里剥离出去。references放字段映射表、术语表、边界说明这种“需要时再查”的资料。examples给模型提供输入输出样例,帮助它理解预期效果。tests用来做回归验证,防止改一次脚本把之前的功能改崩。
三种骨架之间的取舍,核心就看一点:这个能力的“不确定性”有多大。规则固定就选知识型,操作固定就选单脚本型,流程长、分支多、结果需要校验就选复合型。
| 形态 | 核心目录 | 适用场景 | 我的建议 |
|---|---|---|---|
| 知识规则型 | SKILL.md | 分类、模板、规则问答 | 内容尽量全写进正文 |
| 单脚本工具型 | scripts | 格式转换、清洗、解析 | 重点打磨 CLI 参数 |
| 复合任务型 | scripts + references + tests | 多步处理、OCR、数据抽取 | 保证脚本和参考文档解耦 |
4. 一个完整的设计案例:发票信息抽取 Skill 的落地过程
4.1 需求边界拆解
我在一个报销系统改造场景里做过一个“发票信息抽取”技能。需求和大多数类似项目一样:用户上传发票 PDF 或图片,系统需要输出发票号、开票日期、金额、购买方、销售方、税号等字段。
第一步不是写代码,而是写边界:输入只能是发票;输出必须是一份 JSON;不能修改原文件;遇到模糊字段宁可空着也不要乱填。把边界写清楚,后面做 model 触发判断和脚本设计都会省很多事。
4.2 目录结构和脚本设计
我最终落地为复合任务型结构:
skills/invoice-extractor/ ├── SKILL.md ├── scripts/ │ ├── extract_invoice.py │ ├── ocr_preprocess.py │ └── requirements.txt ├── references/ │ ├── field_mapping.yaml │ └── exceptions.md ├── examples/ │ ├── sample_input.pdf │ └── expected_output.json └── tests/ ├── cases.json └── run_tests.shextract_invoice.py 的核心不复杂,但要求很明确:接收--input和--output,成功时写入 JSON 并返回 0,失败时返回非零退出码,并把错误信息写到 stderr。
#!/usr/bin/env python3 import argparse import json import sys def main(): parser = argparse.ArgumentParser(description="Extract invoice fields") parser.add_argument("--input", required=True, help="input file path") parser.add_argument("--output", required=True, help="output json path") args = parser.parse_args() try: # 实际项目中这里会调用 OCR、PDF 解析等逻辑 result = { "invoice_no": "12345678", "date": "2025-01-01", "amount": 0.00, "seller_tax_no": "91110000MA1234567X", "status": "ok" } with open(args.output, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) return 0 except Exception as exc: print(str(exc), file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())很多人会问:为什么不让模型直接读发票文件然后输出 JSON,非要套一层脚本?因为大模型直接读 PDF 图片的字符级信息并不可靠,而且会把大量图像 tokens 吃进上下文。用脚本做确定性抽取,模型只负责纠错、判断和整理,成本和稳定性都会好很多。
4.3 SKILL.md 怎么把模型和脚本粘起来
SKILL.md 在这个结构里相当于一个接口文档。它要告诉模型:先检查文件、再跑命令、最后按 schema 校验。我把 core 部分补全成:
--- name: invoice-extractor description: 从 PDF/PNG/JPG 格式发票中抽取发票号、开票日期、金额、购买方、销售方、税号,并输出 JSON;当用户上传发票、报销截图或要求提取发票信息时使用。输入不是发票时不要使用。 allowed-tools: - Bash - Read --- # 发票信息抽取 用户提供发票文件,需要提取结构化字段时使用。 ## 操作流程 1. 使用 Read 确认输入文件存在且可读。 2. 执行脚本: python3 scripts/extract_invoice.py --input <path> --output /tmp/invoice_result.json 3. 读取 /tmp/invoice_result.json,逐字段检查。 4. 如果字段缺失,不要猜测,输出为空字符串并注明原因。 ## 输出结构 {"invoice_no": "", "date": "", "amount": 0.00, "seller_tax_no": ""} ## 故障处理 - 脚本退出码非 0,先看 stderr 输出。 - 文件不是可解析图片时,提示用户上传清晰图片,不要硬编。这个例子能说明一个问题:SKILL.md 并不负责所有业务逻辑,它负责把模型引导到正确的执行路径上。模型看懂了路径,脚本干脏活,两者配合才不会互相拖后腿。
4.4 为什么脚本逻辑要尽量“死板”
在 Skill 设计里,脚本越“死板”越好。所谓死板,是指参数严格、格式固定、行为可预期。模型不是人,它不会因为你脚本很灵活就发挥主观能动性;它只会按照 SKILL.md 的说明传参数,然后根据返回结果判断下一步。如果你的脚本同时支持文件路径、URL、base64 字符串三种输入,反而会让模型陷入选择困难。
我在这个案例里刻意把输入输出都简化成--input和--output,所有复杂解析调度交给脚本内部处理。这样 SKILL.md 写起来简单,模型执行也稳定。
5. description 写得准不准,直接决定技能会不会被“找到”
5.1 模型判断触发时,看到的不是全部正文
这是 Skill 设计里最容易被忽略的一点:模型在执行任务时,通常不会先把所有 SKILL.md 全文读完再决定用哪个。它面对的是一个技能列表,每个技能露出的是 name 和 description,本质上是一个检索问题。
换句话说,你的技能能力再强,如果 description 和用户问题匹配不上,就等于不存在。
5.2 用“触发场景 + 动作 + 边界”三段式写 description
我现在写 description 基本固定用三段式:
- 触发场景:用户提到什么关键词、上传什么类型的文件时用。
- 执行动作:这个 Skill 会做什么,产出什么。
- 边界:什么情况下不要用。
拿前面的例子再演示一遍:
description: 从 PDF/PNG/JPG 发票中提取发票号、日期、金额、购买方、销售方税号并输出 JSON;当用户上传发票图片、PDF,或要求提取发票信息、生成报销数据时使用。如果输入不是发票,或用户只想了解发票常识,不要使用该技能。我把“若只是闲聊发票常识,不要使用”写进去,就是为了防止误触发。实际测试中,很多误触发都来自 description 只写了正向条件,没写反向边界。
5.3 给 description 做“召回增强”
如果技能的核心场景里有很多同义表达,可以在 description 里带上一组触发词。发票场景就带上“发票、报销、invoice、电子发票、扫描发票”这些词。这样模型在术语匹配时更容易命中。
但注意别矫枉过正。description 不是关键词堆砌,长度太长会吃掉上下文窗口,也会分散模型的注意力。触发词列到“能覆盖 90% 常见表达”就够了,写二十个同义词并不会带来线性收益。
6. 结构设计里最容易踩的五个坑
6.1 SKILL.md 越写越长,把技能变成工具书
第一个坑就是把 SKILL.md 写成百科全书。官方规范允许你写清楚,但“清楚”不等于“详尽”。正文一旦超过一屏,模型在执行时就开始跳过关键步骤。
我处理长文档的原则是:正文只留操作路径,细节放进 references。字段映射表、公司名缩写、特殊格式说明,全部移到 references 目录,并在正文工作流里显式说明“遇到 XX 情况时读取 references/xx 文件”。这样模型才知道什么时机去查,而不是傻傻地把整个技能目录读一遍。
6.2 脚本只写好路径,不处理错误
给 Skill 写脚本时,最常见的翻车点是:脚本只处理理想输入,模型传了一个不存在的文件路径,脚本直接抛异常,模型看着 traceback 不知所措。
正确做法是:脚本对所有参数做校验,用 argparse 保证必填参数缺失时有清晰提示,业务异常全部捕获,错误信息写到 stderr,退出码区分“参数错误”和“执行失败”。模型虽然不会读懂所有 traceback,但一个干净的 stderr 信息往往能帮它修正下一步。
6.3 allowed-tools 和脚本能力脱节
第二个常见翻车点:Skill 里明明要跑 python 脚本,allowed-tools 却只给了 Read,没给 Bash;或者反过来,纯知识型技能放了 Read 和 Bash,模型动不动就想跑命令。
权威建议是:每个 Skill 的 allowed-tools 要和正文操作严格对应。你希望它读文件就给 Read,希望它跑脚本就给 Bash,不希望它乱动系统就什么都不给。权限和白名单写清楚,模型反而会更专注。
6.4 references 目录形同虚设,模型根本不会主动去看
很多人建了 references 目录,却发现模型从不去读。问题往往不在模型,在于 SKILL.md 里没有给“读取时机”。你光写“更多信息见 references”是不够的,模型不知道什么时候叫“更多”。
正确做法是在工作流里加上条件分支,比如:字段名称不在常见范围内时,先读取references/field_mapping.yaml;格式无法解析时,查看references/exceptions.md。明确的条件角色编码,比模糊提示更有效。
6.5 多个 Skill 的描述互相覆盖
当系统里同时挂了发票抽取和报销单整理两个 Skill,风险就来了:两者的 description 里都可能带“发票”“报销”这两个词,模型可能随机选一个,触发不稳定。
我的解决办法是边界拆分:发票抽取只负责“图片/PDF → 字段 JSON”,报销单整理只负责“字段 JSON → 报销单文档”。两者在 description 里互相声明“不做对方的活”,把任务边界切割干净。
| 常见问题 | 表现 | 处理方式 |
|---|---|---|
| SKILL.md 过长 | 模型跳过步骤 | 细节下沉 references |
| 脚本脆弱 | 模型无法继续 | 强化参数校验和退出码 |
| 权限脱节 | 脚本被拒或乱跑 | 收紧 allowed-tools |
| 参考文件无效 | 模型不读 | 在流程中写清读取时机 |
| 技能边界重叠 | 误触发 | 拆分任务域并声明边界 |
7. 上线前的验证清单:不要只测“能跑”
7.1 用测试集代替感觉
我一开始做 Skill 也是“手搓几个案例跑通了就上线”,后来发现换个说法就触发失败。现在每个 Skill 上线前都会建一个测试集,至少覆盖三类情况:
- 直接命令:用户明确说“帮我提取这张发票”。
- 模糊意图:用户说“我要报销,这里有张 pdf”。
- 反例:用户说“发票是什么”,这时候不该触发发票抽取。
每一条测试我都会记录期望触发的 Skill、实际触发的 Skill、输出是否正确。一张简单的表格就能看出漏触发和误触发的问题。
| 测试输入 | 期望触发 | 实际触发 | 输出正确 | 备注 |
|---|---|---|---|---|
| 帮我提取这张发票 | invoice-extractor | invoice-extractor | 是 | 直接命令 |
| 我要报销,pdf 在桌面 | invoice-extractor | invoice-extractor | 是 | 模糊意图 |
| 发票和收据有什么区别 | 不触发 | invoice-extractor | 否 | 误触发 |
| 图片太模糊,识别失败 | invoice-extractor | invoice-extractor | 部分 | 需故障提示 |
7.2 关注 token 消耗和调用延迟
Skill 结构设计不只影响正确率,也影响成本。技能描述越长、references 文件越大,模型每次决策所需要处理的 token 就越多。尤其当系统里挂了十几个 Skill 的时候,光技能列表的描述都可能成为不可忽略的开销。
我的做法是每轮迭代记录一次平均 token 消耗,重点观察:
- description 是否过长,能不能缩到五行以内。
- references 是否能在确需时才读取,不是一上来就被翻个遍。
- 脚本输出体积是否过大,模型每一次都要处理全量输出。
7.3 把 Skill 当成代码一样维护
Skill 是一个目录,但它本质上也是需要持续维护的代码资产。我建议把它放进 Git 仓库,每次修改 SKILL.md 或脚本都做代码评审,测试集跟着一起更新。目录名和 name 字段不要带版本号,否则工具名一变,历史测试和模型触发都会受到干扰。版本信息用 Git tag 去管理,比在技能名里写 v2、v3 要干净得多。
我现在每个 Skill 上线前都会把上面那张表格完整跑一遍,看起来挺朴素,但比“想起来再测”靠谱得多。结构设计这件事没有银弹,真正有效的就是把描述、目录、脚本、参考文档当成一套接口去打磨,模型自然越用越顺。