“skills”这个词单看很抽象,但在2025年下半年AI Agent的开发语境里,它已经成了和“MCP”并列的热门关键词。如果你关注过Claude最近的更新,一定见过Agent Skills这个新概念——它本质上是把一组针对特定任务的指令、脚本和上下文打包成一个可复用的独立模块,让智能体在需要的时候加载进去,直接获得某项“技能”。这篇文章我就围绕skills这个标题,完整拆解一下Agent Skills是什么、怎么设计、怎么落地,以及我在实际项目里踩过的坑。适合正在做AI应用开发、想要让Agent能力更加模块化的朋友参考,看完可以直接照着写一个自己的skill。
1. 为什么“skills”突然这么火:先搞清楚它在解决什么问题
Agent Skills之所以在智能体开发圈里讨论度飙升,不是因为它又是一个花哨的新框架,而是它确实切中了当下Agent工程实践里的一个真实痛点:模型很聪明,但“稳定的专业能力”很难沉淀。
你在对话里告诉Claude“帮我写一个Python爬虫”,它大概率能写得像模像样;但如果你让它“每一次写爬虫的时候都自动遵守我们团队的代理配置、错误重试策略、User-Agent伪装规范,并且把结果统一存成JSON”,它就很难每次都做到。原因很简单,大模型的对话上下文是临时的,记忆和偏好无法固化。过去我们解决这个问题靠的是system prompt不断加长,结果就是提示词越来越臃肿,既浪费token,效果还不稳定。
1.1 MCP是左膀,Skills是心脏
很多人会把Agent Skills和MCP拿来对比,其实它俩解决的问题完全不在一个维度上。MCP(Model Context Protocol)解决的是“Agent怎么连接外部工具和数据源”,本质是一个标准化接口协议,让模型可以调用文件系统、数据库、浏览器等外部能力。Agent Skills解决的是“Agent怎么做好某一类复杂任务”,它打包的是指令、流程、规则和可选脚本,相当于给Agent注入了一套做事的方法论。
我用一个类比帮你理解:MCP是给Agent装上了手和眼睛,Skills是给Agent装了大脑里的“操作手册”。手和眼睛决定了它能碰什么、看什么,操作手册决定了它拿到任务后以什么样的流程、标准和姿势去做。两者完全不冲突,实际项目里经常是搭配使用——skill里声明需要用到的MCP工具,运行时再把工具注入进去。
1.2 Skills和普通Prompt、工具调用的本质区别
如果你只是把一段详细指令写进system prompt,效果和skill看起来差不多,但区别藏在几个关键点上:
第一是加载时机。system prompt是每次请求都会完整注入的,不管你当前任务用不用得上;skill则是由模型根据用户请求动态判断、按需加载的。我用一个不严谨但很形象的比喻:system prompt是每天背着全部家当出门,skill是出门前根据今天要去的地方往包里塞对应的工具。显然后者更轻、更省token、干扰更少。
第二是知识封装方式。一个skill不是单纯的文本指令,它可以携带附件、脚本、参考示例,甚至是一整套数据处理的代码。这意味着你可以把“经验”固化成真正的可执行资产。比如做一个文档转换skill,里面直接带好PDF解析脚本和格式模板,模型加载进来以后就知道该调用哪个脚本、按什么顺序跑。
第三是可复用性。一个写好的skill文件就是一个标准目录,放进~/.claude/skills就是全局可用,放进项目的.claude/skills就是团队共享。不用改一行代码,你就能把同一个技能复制到任意项目里,这对做工程化的人来说价值非常大。
1.3 一个技能,一个目录:核心的最小单位
Agent Skills在落地层面最小的单位是一个目录,目录里至少包含一个名为SKILL.md的Markdown文件。文件名是固定规范,不能改。SKILL.md的头部带YAML格式的元信息,声明技能名称、描述、参数、依赖等字段;正文部分则是给模型阅读的具体操作流程、规则、注意事项,用自然语言写成。
如果你需要让技能执行复杂的确定性操作,比如处理文件、调API,还可以在同目录下放一个可执行脚本,命名规范是SKILL.py、SKILL.js这种。模型在执行任务时会在工具调用里直接运行这个脚本,把参数传进去,拿到输出结果再接续后续推理。
my-skill/ ├── SKILL.md ├── SKILL.py └── reference/ └── sample_data.json这样一个“指令 + 脚本 + 参考数据”的目录,就是Agent Skills的最小闭环。后面我沿着这个结构,逐步拆解怎么从零设计并实现一个能直接用于生产的skill。
2. 手写一个Skills的前期准备:格式、结构和设计决策
动手写skill之前,有几项设计决策需要先想清楚。这些决策直接决定你后期维护成本高不高、模型调用成功率高不高,往往比写代码本身更重要。
2.1 格式选择:Markdown为主,脚本为辅
现在主流的Agent Skills规范里,SKILL.md是技能的核心入口,几乎所有逻辑都应该先用自然语言描述清楚,包括任务的输入是什么、输出是什么、中间步骤有哪些、每一步要注意什么边界条件。Markdown本身天然适合这个场景——你可以用列表、表格、代码块组织内容,模型对这些结构的理解能力很强。
脚本文件SKILL.py或SKILL.js是辅助,用来处理那些“模型靠文本推理搞不定、必须靠代码确定性完成”的部分,比如文件批量重命名、PDF解析、Excel汇总、正则清洗。判断标准很简单:这件事换成文本指令让模型干,会不会产生随机性?会的话就写成脚本。
我在实际项目里的习惯是:能用指令说清楚的绝不写脚本,因为脚本会增加调试成本和跨环境兼容性问题;但一旦涉及文件读写、格式转换、网络请求这类操作,绝不指望模型“用代码生成代码”再“执行”,因为那样会产生多层错误叠加。
2.2 SKILL.md的核心结构:YAML头与正文要点
一个标准的SKILL.md长这样:
--- name: pdf-splitter description: 将PDF文件按页数或书签拆分成多个独立PDF文件。 fields: - name: input_file description: 要拆分的PDF文件路径 example: /path/to/input.pdf - name: mode description: 拆分方式,可选 pages 或 bookmarks default: pages - name: chunk_size description: 按页数拆分时每份的页数 default: 10 --- # 任务目标 用户要求拆分PDF时,按以下流程执行... ## 执行步骤 1. 使用提供的 input_file 参数定位文件 2. 调用 SKILL.py 执行拆分 3. 汇总输出文件列表这里有几个容易被忽略的细节。description字段非常关键,因为模型判断“当前用户请求是否需要加载这个skill”靠的就是它。描述写得越具体,越容易出现结果里包含明确的行为动词、技术名词和场景限定词,比如“拆分PDF”、“批量重命名Excel”、“解析并提取日志关键错误”,模型就越容易在正确的时机触发它。相反,如果写成“用于文件处理”,那模型大概率会把这个skill雪藏,因为太泛了,它不知道该不该用。
fields参数列表则决定了模型需要从用户请求中抽取哪些信息才能调用这个技能。它是模型向你写的脚本传参的“接口契约”,所以每个字段的description也要写得足够明确,最好给出example。我见过很多人在这个位置偷懒,结果模型每次调用时参数传递格式都不一样,脚本里写死了解析逻辑就直接翻车。
2.3 工具选型:什么时候自带脚本,什么时候依赖MCP
在写skill的过程中,你一定会遇到一个问题:我这个skill里需要“读文件”的能力,是直接让模型调用MCP文件工具,还是自己在SKILL.py里写代码实现?我的经验是分两层判断。
如果读文件只是实现主任务的一个中间步骤,并且这个文件格式很简单,那就让模型直接用MCP工具读,读完了再按SKILL.md里的指令继续做事。如果读文件这件事本身是整个技能的核心,而且涉及复杂的解析、编码、异常处理,那就把它完整写进脚本。你可以理解成MCP是通用的基础设施,skill脚本是专用的业务逻辑,两者不要互相越界。
另外要注意依赖声明。如果你的SKILL.py用了第三方库,比如pypdf、openpyxl、requests,建议在SKILL.md里用明确的说明段落写清楚运行前置条件,包括Python版本、必须安装的包。这句话看起来不起眼,但换一台机器、换一个环境跑的时候,它能救你很多次。
3. 实操过程:从零写一个可用的PDF拆分skill
理论讲一堆,不如直接上手一个完整的例子。我挑一个很多人在办公场景里会遇到的真实需求来拆解:把一本几百页的PDF按指定页数拆分成多份独立文件。这个任务既涉及文件操作,又涉及参数解析,非常适合演示一个skill的完整落地过程。
3.1 定义适用场景和输入输出
设计skill的第一步不是写代码,而是想清楚边界。我把它拆成三个问题:
这个技能适合处理什么输入?我限定为单个PDF文件路径。用户在对话里给出路径,或者给出一个明确的文件位置描述。
这个技能输出什么?拆分后的一组PDF文件,生成在输入文件同目录的split_output文件夹下,并且把文件列表返回给用户。这一步要想清楚,因为有人的需求是“拆完直接告诉我文件在哪”,有人希望“拆完再合并成zip”,如果描述里不写清楚默认策略,模型每次执行都可能给你不同的结果。
哪些情况应该拒绝处理?加密PDF、扫描版没有文本层但用户要求“提取文字”的PDF、超大文件导致脚本超时,这些应该在SKILL.md的执行流程里写明“遇到这种情况直接告知用户,不要硬跑”。设定边界不是限制能力,是减少错误执行后的连锁麻烦。
3.2 配置YAML元信息和参数约束
确定了场景,开始写SKILL.md的YAML头。我提供一份实际用过的配置:
--- name: pdf-splitter description: 将PDF文件按页数拆分成多个独立PDF文件。当用户要求拆分PDF、把一个PDF分成两份、按指定页数切分文档时使用。 fields: - name: input_file description: PDF文件的绝对路径或相对路径 required: true example: ./docs/input.pdf - name: chunk_size description: 每份文件包含的页数 default: 10 - name: start_page description: 起始页,从1开始 default: 1 - name: end_page description: 结束页,默认到最后一页 ---注意两个细节。我在description里刻意用了“拆分PDF”、“把一个PDF分成两份”、“按指定页数切分”几个说法,因为用户在对话里描述需求的语言千变万化,触发词覆盖越多,召回率越高。另一个细节是start_page和end_page加了没用?看起来有用,实际只对一部分场景有意义,但它会让模型在参数抽取时多两个可填的选择,反而造成不必要的犹豫。我最终的实际版本里其实删掉了这两个参数,只保留input_file和chunk_size,因为这两个是“最小可用参数集”,模型只需要判断两件事:拆哪个文件、按多少页拆。
3.3 SKILL.py脚本编写要点与完整代码
接下来是脚本部分。我直接贴一份精简但可用的SKILL.py,并逐段解释几个关键点:
#!/usr/bin/env python3 """PDF splitter skill script.""" import sys import json import os from pypdf import PdfReader, PdfWriter def split_pdf(input_path, chunk_size=10): reader = PdfReader(input_path) total_pages = len(reader.pages) output_dir = os.path.join(os.path.dirname(input_path), "split_output") os.makedirs(output_dir, exist_ok=True) file_list = [] for i in range(0, total_pages, chunk_size): writer = PdfWriter() end = min(i + chunk_size, total_pages) for page_num in range(i, end): writer.add_page(reader.pages[page_num]) output_name = f"{os.path.splitext(os.path.basename(input_path))[0]}_part_{i//chunk_size + 1}.pdf" output_path = os.path.join(output_dir, output_name) with open(output_path, "wb") as f: writer.write(f) file_list.append(output_path) return file_list if __name__ == "__main__": try: input_file = sys.argv[1] chunk_size = int(sys.argv[2]) if len(sys.argv) > 2 else 10 paths = split_pdf(input_file, chunk_size) print(json.dumps({"status": "success", "files": paths})) except Exception as e: print(json.dumps({"status": "error", "message": str(e)})) sys.exit(1)脚本本身不难,但有几个工程细节值专门说说。
第一,脚本的输入参数从sys.argv读取,这个顺序必须和SKILL.md里fields的声明顺序一致。比如我在fields里先写了input_file再写chunk_size,脚本里就按这个顺序接收第1个和第2个参数。如果顺序乱了,模型按描述传参后脚本解析就会错位,这类问题排查起来非常隐蔽。
第二,输出必须是一个结构化的JSON字符串。为什么?因为模型需要从脚本输出中提取信息继续后续的对话,比如告诉用户“生成成功”“生成了5个文件”,如果脚本输出一堆毫无格式的print日志,模型就得靠猜,失败的概率大增。让脚本输出JSON,相当于给模型一个明确的数据接口,它拿到的结果总是干净、可解析的。
第三,脚本里的异常要捕获并打印结构化错误信息,而不是让Python直接抛traceback。一个包含大段调用栈的工具输出会污染模型的上下文窗口,让它分不清到底发生了什么。你只需要把用户听得懂的错误信息传回去,比如“文件不存在”,或者“PDF已加密,无法读取”。
3.4 目录结构、挂载方式和验证方法
写完了文件,按下面的结构放好:
~/.claude/skills/pdf-splitter/ ├── SKILL.md └── SKILL.py放在~/.claude/skills下是全局生效,放到项目的.claude/skills下则只对本项目生效。我个人的建议是先放全局,验证稳定后再决定是否下沉到具体项目。因为全局目录适合通用的能力,比如PDF拆分、Excel处理这类场景不挑项目;项目级目录适合强业务绑定的能力,比如“按某团队规范生成周报”“按某库的表结构生成CRUD代码”。
验证方法也很直接。在Claude的对话界面里直接输入一句自然语言:“把这个PDF按每10页拆分一下:./docs/手册.pdf”,然后观察它是否自动调用了pdf-splitter这个skill,检查输出的文件列表是否和你预期一致。如果没触发,大概率是description写得不够精准;如果触发了但参数传错了,大概率是fields描述或脚本参数顺序的问题。
4. 进阶:让skills变成一套可维护的技能库
一个skill跑通只能算demo,真正进入生产环境后,你需要管理的是一个持续增长、不断迭代的技能库。这里分享几个我在项目里沉淀下来的工程化经验。
4.1 目录命名与内部组织规范
技能目录的命名建议只用小写字母和连字符,比如pdf-splitter、excel-merge、log-parser。不要在目录名里使用空格、驼峰和中文字符,因为你后面可能在脚本里以某种方式引用目录名,特殊字符会带来不必要的麻烦。
技能内部除了必须的SKILL.md和可选脚本,建议再放一个README.md,用给人类看的方式记录这个技能的变更历史、依赖环境、维护者。模型不读它,但你的同事和未来的你需要读。别笑,我见过太多团队里只有代码没有文档的skill,最后没人敢改,因为不知道改坏了哪里。
如果你的技能引用了外部模板或参考文件,放在reference/子目录下,脚本和指令都通过相对路径引用。这样整个目录打包zip分发到另一个环境时不会出现路径断裂。
4.2 依赖管理和版本迭代
SKILL.py 用到的Python依赖,怎么管理?我在SKILL.md里固定写一段:
## 依赖 - Python 3.10+ - pypdf>=4.0.0然后本地用Python虚拟环境跑通后,把依赖记录到目录内的requirements.txt。这样不管谁拿到这个skill,都能用pip install -r requirements.txt快速还原环境。
版本迭代上,强烈建议在SKILL.md的YAML头里加一个version字段,每次修改指令或脚本都升级版本号。这个字段模型不关心,但当你同时在多个项目中使用同一个skill时,没有版本号你将完全失去对“当前环境跑的是哪一版逻辑”的掌控。我自己的做法是每次改动后除了升级版本号,还会在README里追加一行变更说明,形成可回溯的历史记录。
4.3 给skill做测试:比你想的更重要
技能本身没有传统意义上的单元测试框架,但你仍然可以做系统测试。最简单的方式是准备一个测试脚本,把调用效果用结构化输入输出比对验证。
python SKILL.py ./test_files/sample.pdf 10跑完检查三点:退出码是否为0、输出JSON里status是否为success、生成的文件数量是否符合预期。这几个维度能覆盖大部分回归风险。
更进阶的做法是让Claude自己当测试员——把测试场景写成一段对话,比如“请用pdf-splitter拆分这个文件:./test_files/manual.pdf,每5页一份”,然后把回复里的工具调用记录和最终结果人工核对一遍。这种方式能验证模型对不同口语表达的触发能力,比单测脚本更接近真实场景。
5. 常见问题与排查技巧实录
这部分是全文最值钱的实操经验汇总。我在把skills引入日常工作流的过程中踩过不少坑,下面按问题频率排一下。
5.1 skill没有被自动加载,怎么办
最常见的现象是:你明明已经定义了skill,对话里也输入了相关需求,但它就是无动于衷,完全像没看到这个技能一样。
这时候先不要怀疑模型能力,大概率是description写得不够明确。我建议把它写成一个包含多个触发词、行为动词和结果导向描述的句子。比如:
将PDF文件按页数拆分成多个独立PDF文件。当用户要求拆分PDF、把一个PDF分割成几份、按每N页切分文档时使用。输入是PDF路径,输出是拆分后的多个PDF文件路径列表。
这样的描述覆盖了用户常见的几种说法,模型更容易理解什么情况下应该加载。
还有一种原因是模型确实正在纠结“该不该用”,这时候可以在SKILL.md正文的开头加一句“当用户请求涉及PDF拆分时,不要犹豫,直接调用本技能”。语气肯定一点,模型决策时会更容易偏向调用。
5.2 参数传递总是出错,传了错位参数
如果你发现脚本收到了错误参数,比如chunk_size接收到了文件路径,问题大概率出在YAML fields和脚本arg顺序不一致上。排查方法是把YAML头的字段顺序和脚本接收顺序并排写下来对照:
| YAML fields顺序 | sys.argv对应 |
|---|---|
| input_file | sys.argv[1] |
| chunk_size | sys.argv[2] |
只要这个表一一对上了,基本不会出问题。如果还出问题,再看fields的description是否足够清楚,模型需要理解每个参数的含义才能抽取正确的值。
5.3 脚本运行环境不一致
同一个skill在你的Mac上跑通,到Linux服务器上脚本报缺模块,这属于经典的环境迁移问题。解决思路就一条:把运行环境锁死。
我建议在SKILL.md里写清前置条件,然后在脚本开头加一个依赖检查:
try: import pypdf except ImportError: print(json.dumps({"status": "error", "message": "缺少依赖 pypdf,请运行 pip install pypdf"})) sys.exit(1)这样即使环境不完整,用户(或者模型)拿到的也是一个清晰的修复指引,而不是一坨traceback。
5.4 多个skill相互干扰的问题
技能数量一多,会出现模型把A技能和B技能弄混的情况,尤其当两个技能的应用场景存在重叠时。比如你有一个“PDF拆分”技能,又有一个“PDF转Word”技能,模型面对一份PDF可能同时加载两个,导致结果混乱。
我的解决办法是两招。第一,把两个技能的description写得更差异化,明确划分边界:PDF拆分只做切页,PDF转Word只做格式转换。第二,在SKILL.md正文里写明“本技能只负责XX,如果用户还需要XX,请另外调用对应技能”。相当于给模型一个显式的路由指引,有效降低技能间的混淆概率。
我在实际踩过几次坑之后最大的体会是:skill能不能被用对,60%靠description,20%靠脚本可靠性,剩下20%靠边界划分。不要把skill设计成包打天下的万能模块,职责越单一,效果越稳定。它本质上是一种让经验以文件为单位沉淀和传播的机制。今天我用各种拆解讲了一个PDF拆分技能从设计到落地的全流程,但其实没有涉及什么深奥理论,底层逻辑就是“把Agent做事的方法整理成可复用的资产”。你完全可以照着这个思路,把工作中那些重复性高、规则明确的AI协作任务逐个变成skill,攒上几个之后,你会发现团队里不同人在同一个项目上获得的Agent能力开始变得一致,这才是这个小小的“skills”目录背后真正值得投入的东西。