最近在带团队整理 Agent Skills 的落地结构时我最大的感触是很多人不是不会写 Skill而是不知道一个“符合官方规范、能拿到生产环境复用”的 Skill 到底应该长什么样。大家常以为 Skill 等于一个 Markdown 文件往里塞一段高质量 Prompt 就结束了但真正深入之后会发现Anthropic 官方 Skill 规范背后其实是三层规范的目录骨架、作为入口的 SKILL.md、以及决定技能上限的“心智模型”。这篇就把我在整理主流 Skills 结构设计方案时的思考、踩坑和沉淀下来的默认模板完整展开直接给你一套可以照着搭的骨架。如果你正在做 Claude 类 Agent 应用、想把技能包工程化或者准备导入别人的 Skill 却对目录结构有疑惑这篇文章都值得看完。1. 为什么需要一套规范Skill 的本质与适用边界1.1 Skill 到底是什么它和 Prompt、MCP 有什么区别先明确一个容易混淆的点Skill 不是一个“高级 Prompt”。Prompt 是你在对话时给模型的一段指令用完即走不会被系统性地复用和迭代。Skill 是在模型外部定义的一个自包含目录里面用 SKILL.md 描述“什么场景该被触发、按什么步骤执行、有哪些参考资源”并可以附带脚本、模板和测试用例。模型在运行时会把这个技能包作为额外的能力注入相当于随身带了一本“岗位操作手册”。MCP 解决的是“模型如何调用外部工具”的问题它更像给模型提供了手和工具可以读写数据库、调用 API、操作文件系统。Skill 解决的则是“模型如何完成一件完整的事”它把领域知识、决策流程、遇到异常时的处理原则都明确写下来。我经常打一个比方MCP 是一把电钻Skill 是“带着电钻、安全规范、验收标准一起打包的工作台”。两者不是替代关系而是配合关系——一个 Skill 内部完全可以约定通过 MCP 工具去执行某一步。官方的 Skill 规范之所以强调整体目录化核心原因有三层。第一层是控制上下文窗口如果技能内容很长系统可以在触发后按需读取 references 或脚本说明而不是把所有内容一次性塞进对话第二层是工程化目录可以纳入 Git、可以写测试、可以做版本回滚这比维护一段散落在聊天记录里的 Prompt 靠谱得多第三层是隔离每个 Skill 只负责一个领域互不污染模型可以根据任务描述精确选择一个技能来加载。1.2 Skill 适合解决哪些问题哪些场景不要硬套以我和不同团队交流下来的经验Skill 适合解决三类问题。第一类是输出一致性要求高的重复任务。比如每周生成项目周报、对一篇文章做结构化摘要、把聊天记录整理成会议纪要。这些任务的结果格式必须稳定描述不清楚的地方会反复变化正好适合用 Skill 把“固定步骤 输出模板”固化下来。第二类是需要多步处理并且要配合代码的任务。典型的如日志分析先读取日志文件再做正则匹配然后聚合统计最后输出报告。模型擅长判断和归纳但在精确处理和批量化处理上不如小脚本可靠。Skill 可以把这些脚本收纳在 scripts 目录里并在 SKILL.md 中告诉模型“什么时候该调哪个脚本、脚本的参数是什么、输出怎么解读”。第三类是高频出现、值得沉淀经验的场景。比如同一个团队反复要做代码评审、安全巡检、依赖升级影响分析。每做完一次就把评审时发现的共性问题和检查项写回 SKILL.md让技能越用越准这才是它和普通 Prompt 拉开差距的地方。反过来有些场景不要硬套。开放性闲聊、一次性的内容创作直接用 Prompt 反而更快需要实时同步、强交互、涉及用户授权和审批的操作应该走 MCP 工具链或者人工确认流程而不是让 Skill 包揽。一个铁律就是如果一件事你只是偶尔做一次写 Prompt如果一件事你已经做了十次并且还要继续做把它做成 Skill。2. 基于官方规范的主流目录结构拆解2.1 一套标准目录骨架长什么样刚接触 Skill 的人多半会有个疑问是不是只要有一个 SKILL.md 文件就够了从“能跑”的角度来说确实够但从“能维护、能复用、能在团队里协作”的角度来说远远不够。主流的结构设计会把能力拆开让不同的内容待在不同的位置模型需要什么就拿什么。my-skill/ ├── SKILL.md # 技能入口路由信息 工作流程 决策原则 ├── scripts/ # 可执行脚本负责确定性高的数据操作 │ ├── run.py │ └── helper.py ├── references/ # 参考文档按需读取不被全文注入 │ └── domain-notes.md └── assets/ # 模板、样例、静态数据 └── template.yaml这里的核心约定是SKILL.md 必须放在技能根目录文件名和大小写要保持一致因为官方解析器正是通过这个固定位置来识别技能的。scripts、references、assets 这些子目录不是必须的但它们是主流实践总结出来的自然分区。scripts 目录存放代码。什么代码适合放进来那些需要精确、重复、机械化执行的逻辑比如 JSON 转换、文本批处理、数据校验。模型负责的是“判断应该怎么处理”脚本负责的是“把处理做对”两者边界清晰错误率才会低。references 目录存放“模型按需翻阅”的资料。比如一份完整的产品规范、一篇文章的二十种风格样例、一份包含大量编号规则的内部手册。不要让模型在技能被加载时把所有资料读完而是让它在真正需要的时候才去打开对应的文件这能大幅节省上下文开销。assets 目录则用来放模板和样例数据。比如生成特定格式报告时SKILL.md 告诉模型“去 assets/report_template.md 里读取字段结构对照填写”。模板和心智流程分开意味着改样式时不用动逻辑改流程时也不用动样式。2.2 SKILL.md 的 frontmattername 与 description 怎么设计任何合法的 SKILL.md 开头都要有一段 YAML frontmatter。官方规范最核心的两个字段是 name 和 description它决定了一个技能在模型眼中“叫什么、什么时候该被想起”。--- name: json-cleaner description: 当用户提供 JSON 文本并要求清理格式、删除冗余字段、转换为更紧凑或更易读的结构时使用。 ---name 建议全小写、用短横线连接并且全局唯一。生产环境里建议加团队或项目前缀比如 acme-json-cleaner避免和社区里出现的同名技能冲突。name 是技能的标识一旦被多处引用改动成本就很高所以第一次定名时要想清楚粒度如果这个技能未来可能扩展出多个子功能就不要把一个过于具体的操作作为名字否则后面只能新建技能来迁移。description 的价值被很多人低估了。模型不是靠“读完整份 SKILL.md”来决定该不该调用技能而是先通过 description 做一次快速语义匹配命中之后才会加载详细内容。所以 description 要包含三块信息触发条件、输入形式、预期效果。一个反例是“用于处理所有与 JSON 相关的任务”这就是典型的空泛描述。模型看了不知道怎么归类遇到任何 JSON 都有可能尝试触发结果要么误触发要么触发后技能帮不上忙。正例是“当用户提供 JSON 文本并要求清理格式、删除冗余字段、转换为更紧凑或更易读的结构时使用。”这句描述给了明确触发边界也给了输入和输出形式模型做路由时几乎不会犹豫。我建议团队在写 description 时养成一个习惯写完以后抄给不看 SKILL.md 正文的同事问他“看到这段描述你能不能猜到技能会干什么”。如果他猜不出来模型大概率也猜不出来。别小看这一步很多技能“偶尔能被触发、偶尔不被触发”的玄学根源往往就在 description 上。3. 实操从零搭建一个可复用的 JSON 清理助手3.1 建立目录并确认技能边界空谈结构没有意义我拿一个完整示例走一遍。这个示例是一个“JSON 清理助手”用户给出一段 JSON技能负责检查合法性、删除冗余字段、输出指定缩进格式。听起来很简单但做成 Skill 后会发现它解决了一个很常见的痛点模型直接改 JSON 时偶尔会手滑漏掉一个逗号、或者在超长文本上截断而交给脚本来做这类问题可以根治。第一步先确定目录和名称。我把它放在项目下的 skills 目录里技能本身叫 json-cleaner整体路径是 skills/json-cleaner/。skills/json-cleaner/ ├── SKILL.md ├── scripts/ │ └── clean_json.py ├── references/ │ └── field-rules.md └── assets/ └── sample-output.json第二步是明确“技能到底做什么、不做什么”这一步在动手写任何文件之前就要完成。json-cleaner 只做四件事解析 JSON、校验合法性、按照字段规则精简、格式化输出。它不做“猜字段含义”的事也不做“帮你改业务数据”的事更不允许在用户没有明确意图时把数据保存到某个固定位置。边界越早写死后面模型执行时就越不会越界。3.2 编写 SKILL.md把判断逻辑写成可执行的心智模型SKILL.md 的正文部分不是给人类看的说明书而是给模型看的工作指令。优秀的写法是结构化、步骤化并且包含分支判断。上面规划出来的这个 JSON 清理助手其工作流程应清晰划分为输入判断、合法性校验、字段过滤、格式输出和结果确认几个阶段每个阶段都要给模型明确的行动指令。--- name: json-cleaner description: 当用户提供 JSON 文本并要求清理格式、删除冗余字段、转换为更紧凑或更易读的结构时使用。 --- # JSON 清理助手 ## 职责 - 校验 JSON 合法性 - 按用户要求过滤冗余字段 - 以可读格式输出结果 ## 工作流程 1. 判断输入是文件路径还是文本内容。 2. 检查 JSON 语法如果存在解析错误报告错误位置并终止。 3. 询问或提取字段保留规则。规则不明确时保留全部字段但只做格式整理。 4. 调用脚本处理数据。 5. 输出结果并说明哪些字段被删除、做了哪些转换。 ## 调用脚本 当需要清理字段或格式化时将输入写入临时文件执行 python scripts/clean_json.py --input 输入文件 --output 输出文件 [--keep 字段名列表] 脚本位置相对 SKILL.md 所在的技能根目录计算不要假设当前工作目录。 ## 禁止事项 - 不得臆造 schema不得擅自修改业务字段含义。 - 不得在用户未允许时覆盖原始文件。 - 用户要求删除字段时先输出删除清单再执行删除。这里有一个特别重要的设计思路SKILL.md 不追求把所有逻辑写满它只需要让模型知道“什么情况做什么、什么时候调用哪个脚本、哪些是红线”。繁琐的字段规则放在 references/field-rules.md 里例如“businessName 和 merchantId 是内部字段默认应被过滤如果输出目标是对外接口保留字段只有 code、data、message”。SKILL.md 中只写一句“字段过滤规则参考 references/field-rules.md按用户场景选择”。3.3 编写配套脚本把确定性操作交给代码接下来写 scripts/clean_json.py这个脚本承担所有确定性操作读取输入文件解析 JSON按照字段保留规则过滤再按指定的缩进写入输出文件。核心逻辑用 Python 标准库就够了不需要额外依赖这能减少部署时环境不一致的问题。import argparse import json import sys from pathlib import Path def parse_args(): parser argparse.ArgumentParser(descriptionJSON Cleaner) parser.add_argument(--input, requiredTrue, help输入 JSON 文件路径) parser.add_argument(--output, requiredTrue, help输出 JSON 文件路径) parser.add_argument(--keep, nargs*, help需要保留的顶层字段名) parser.add_argument(--indent, typeint, default2, help缩进空格数) return parser.parse_args() def clean_json(data, keep_fields): if keep_fields is None: return data if isinstance(data, dict): return {k: v for k, v in data.items() if k in set(keep_fields)} if isinstance(data, list): return [clean_json(item, keep_fields) for item in data] return data def main(): args parse_args() input_path Path(args.input) output_path Path(args.output) try: with input_path.open(r, encodingutf-8) as f: data json.load(f) except json.JSONDecodeError as e: print(fJSON 解析失败: {e}, filesys.stderr) sys.exit(2) cleaned clean_json(data, args.keep) with output_path.open(w, encodingutf-8) as f: json.dump(cleaned, f, ensure_asciiFalse, indentargs.indent) print(f处理完成输出到 {output_path})脚本里有一个小细节值得拿出来讲路径定位。当模型在某个会话中创建临时文件时它所在的工作目录不一定是技能根目录。如果 SKILL.md 里只写“运行 clean_json.py”模型可能找不到文件。所以在 SKILL.md 中要用“脚本位置相对技能根目录计算”这类明确指引更稳妥的做法是让脚本内部用Path(__file__).resolve().parent来定位自身路径再把输入输出文件路径通过参数传入避免对运行环境做假设。测试时我用几组典型输入验证脚本可靠性合法的普通 JSON、包含中文和转义字符的 JSON、带顶层嵌套的数组、空对象、以及包含非法语法的 JSON。前四类都应该正常输出最后一类应当在 stderr 打印清晰错误并以非零退出码结束。SKILL.md 中写入“看到退出码 2 时告诉用户 JSON 解析失败并展示错误位置”这样模型在拿到非零退出码时不会一脸茫然而是能给出正确反馈。3.4 用模拟请求测试触发率和输出质量脚本和文档都齐了最后一步是测试。我通常准备一组模拟用户请求分别观察模型是否正确触发技能、是否按照 SKILL.md 步骤执行、输出是否符合验收标准。模拟请求可以这样设计“帮我把这段 JSON 清理一下去掉多余的空格和缩进”——应当触发做格式清理。“这个 JSON 里只要 code 和 message 两个字段”——应当触发先列出删除清单再过滤。“把这段 JSON 转成 YAML”——不应触发或者说技能应当显式表示自己不支持转换并建议其他方式。“分析这个 JSON 的业务含义”——不应触发因为超出了技能边界应由模型直接回答。第一次测试通常会发现两种问题一种是该触发时没触发另一种是不该触发时乱触发。前者用“把一组典型请求截图回贴到 SKILL.md 文档里”的方式补入 description后者则要收紧 description 和“禁止事项”。我在多次迭代后把 description 从“处理 JSON 相关任务”缩窄到“清理格式、删除冗余字段、输出指定缩进”误触发率明显下降几乎不再出现“用户只是问问题却触发技能”的情况。4. 常见问题与排查技巧实录4.1 Skill 一直不触发先看一眼 description很多同学跑通第一个 Skill 之后遇到频率最高的问题是“我已经把技能装好了也试了几个说法但模型就是不理它。”排查的第一步不要去看工作流先检查 frontmatter 里的 description。反例问题正例用于处理所有与 JSON 相关的内容范围太宽模型无法判断边界当用户提供 JSON 并要求清理格式、删除冗余字段时使用一个文档整理助手没有触发场景模型不知道该何时调用当用户提供一段非结构化文本并要求输出分级 Markdown 大纲时使用处理数据高度模糊几乎没有有效信息当用户上传 CSV 并要求按字段聚合统计时使用description 的本质是给模型做“路由”用的它的语法不是“这个技能是什么”而是“当……时候使用它”。如果你发现自己写的 description 里没有“当”字结构那大概率不合格。这里还要提醒一点不同模型的触发策略不完全一样描述风格可能需要微调。克制的做法是先按标准结构写不要堆砌同义关键词堆关键词反而会让模型在语义匹配时产生错误关联。4.2 SKILL.md 太长上下文被无谓占用我见过有人把一个技能的全部知识都塞进 SKILL.md结果一份文档超过两千行。当时他还会困惑为什么模型跑得又慢又笨——因为每次技能被触发整份文档都会被读取模型的核心工作被两千行旧规则拖累自然显得“反应迟钝”。正确的做法是遵循“精益入口”原则SKILL.md 只写路由信息、工作流程、调用脚本的方法、禁止事项和“什么情况下需要看哪个参考文件”把需要大量阅读的内容拆到 references 目录。SKILL.md 一般控制在几十到一百行左右具体没有硬性限制但建议不要让它成为模型的主要阅读负担。如果一份参考文档真的非常长可以在 SKILL.md 中用一个“查阅索引”区块列清楚文件名、适用场景、关键词。例如## 参考资料索引 - references/field-rules.md字段保留与过滤规则处理字段删除前必须阅读。 - references/output-guidelines.md不同目标场景的输出格式规范生成外部交付物时必须阅读。这样模型没有碰到相关场景时就不会打开长文碰到时才按需读取上下文自然不会爆炸。4.3 脚本路径混乱与依赖环境不一致脚本类 Skill 最常踩的坑是“本地能跑模型执行时找不到文件”。原因多数是路径假设错了模型可能在技能目录外的某个工作区运行脚本也可能把输入输出路径写到了错误位置。我的解法是三层防御第一SKILL.md 里明确写出“脚本位置相对技能根目录计算不要假设当前工作目录”第二脚本内部通过Path(__file__).resolve().parent定位自身不依赖os.getcwd()第三输入的临时文件一律通过参数传入调用方负责生成路径脚本只负责处理。依赖环境也需要留心。尽量让脚本只用标准库如果确实需要第三方库例如 pandas、numpy在技能根目录放一个 requirements.txt并在 SKILL.md 里写清楚“首次运行前执行 XXXX 安装依赖安装失败时提供提示而不是直接报错”。我在团队内部会顺手加一个小脚本scripts/check_env.py用来检查解释器版本和依赖是否齐全减少模型对环境的误判。4.4 同名 Skill 互相覆盖导入后表现诡异从社区下载的 Skill 和自己写的 Skill 撞名是另一个高频问题。规范要求 name 在运行时环境中尽量唯一但如果不同目录下出现同名技能解析器可能只加载其中一个或者因为找不到目标技能而表现得很奇怪。如果你的技能明明存在却不生效请先检查一下是否和其他技能同名。生产环境我建议给每个技能加团队或项目前缀比如acme-json-cleaner而不是json-cleaner。虽然名字长了一点但换来了导入时的确定性。这个做法实际上是把“全局唯一”这个要求内化到命名规范里等到出问题时再改名字已经要重写引用那时成本更高。5. 设计经验与避坑心得5.1 心智模型是 Skill 的灵魂比代码更需要打磨回看这几个月做 Skill 的经验我最大的体会是一个技能的代码可以很简单真正决定它好用与否的是心智模型——也就是“模型遇到情况时应该怎么思考”的路径。它和代码最大的不同在于代码可以一次写完反复执行而心智模型的每个字都会被模型按概率重新解释所以必须写得非常清楚。一个比较通用的工作流模板是先判断输入形态再确认用户意图然后决定执行路径最后校验输出结果。每一步之间用短句明确“如果 A则执行 B如果 C则停止并向用户询问”。比写流程更重要的是写边界明确“禁止做什么”比“应该做什么”更能防止模型按惯性跑偏。我常常提醒团队给 SKILL.md 写流程不要用大长句。模型在理解长句时容易丢失信息一句只表达一个分支比塞满各种条件的复合句可靠得多。宁可文档看起来“碎”一点也不要让人感觉像一篇论文。5.2 容易翻车的几种设计习惯想清楚了再动手第一种是“万能 Skill 梦”。有人希望一个 Skill 覆盖整个岗位的职责写出来却是大而全、没有边界模型无法判断该在哪个粒度触发结果每个任务都半吊子。我的建议是“一个技能只干一件事”如果事情确实复杂拆成多个 Skill 联动。第二种是把一次性数据写死在 SKILL.md 里。比如把某次活动的截止日期、某个客户的具体名称写进通用技能里场景一换就全部失效。数据应该放 assets 或者 references逻辑和数据分离后Skill 的生命力会强很多。第三种是只写流程不写“不被允许的行为”。模型在和用户对话时是倾向于迎合用户意图的如果 SKILL.md 没有禁止事项它可能为了“完成用户需求”去执行不该做的操作比如覆盖原始文件、修改已有 schema。把“禁止事项”放在显眼位置并写明违反后的替代方案是成本最低的纠偏方式。第四种是完全不做版本管理。Skill 本质上是代码应该有提交记录、有变更日志。我在项目里给每个技能都维护一个简单 CHANGELOG里面记录“哪次调试后改了心智模型、哪次加了脚本参数”。不要忽略这个否则一个月后你会发现自己也看不懂当初为什么要写某个分支判断。我个人的习惯是每发布一个新 Skill 或大改一次流程会把样本请求和输出留存下来作为回归样例。下次再改流程时直接拿这批样例去重测如果命中率或输出质量下降立刻能定位是哪一段描述引起了行为变化。这套做法看起来笨但维护起来的稳定性远比“凭感觉调描述”要高。最后再分享一个小的实际体会如果你刚开始接触 Skill不用一步到位。先用一个最简目录、一份只包含核心流程的 SKILL.md 跑通一次真实任务再慢慢把 references、scripts、tests 加进去。结构是服务问题的不是用来穷尽所有可能性的。等到你的技能积累到三个以上、开始要跨团队共享时这套规范化的优势就会彻底显现出来。