这篇是Agent Skills系列笔记的第二篇。上一篇我整理了Agent Skills的基本概念、目录结构,以及一个最小可用的SKILL.md长什么样,算是把“能用”这条路跑通了。但真到了把Skill往项目里塞的时候,问题马上冒出来了:模型经常不调用我写的技能,偶尔调用了又执行得磕磕绊绊,甚至会在我根本不想触发它的时候自行启动。翻来覆去调了一周,我发现问题不在模型本身,而在我写的Skill文档和配套代码之间,缺少了一套完整的设计规范。这一篇就把我踩过的坑、总结出来的写法,还有从零搭一个实用Skill的完整过程,一并整理出来。
如果你已经看完第一篇,手头有一个能跑起来的Skill,但总觉得哪里不对劲、不顺手,这篇就是专门帮你解决“从能用到好用”这个阶段的。如果还没看过前一篇,也不影响,关键概念我会快速带过,只是建议你先了解SKILL.md的基本格式再来读这一篇,收获会更大。
1. 为什么需要把Skill当“产品”来设计
1.1 从“跑通”到“好用”之间的真实差距
先说我自己的经历。第一个能跑通的Skill做的是“日志分析”,就是在项目目录里扫一遍日志文件,帮我找异常堆栈和耗时超过阈值的请求。当时我很快写完了SKILL.md和一个Python脚本,测试时模型也能正确调用,我还挺得意。结果回到真实项目里,效果完全不是那么回事:模型要么在普通对话里莫名其妙调它,要么在真正需要看日志的时候,又把它忘得一干二净。
后来我把原因拆开看,发现问题几乎都出在同一点上:我写的SKILL.md太“自我中心”了。我清楚这个脚本要干什么,所以描述里写的是“这个技能用于分析日志文件、提取异常堆栈、统计高耗时请求”,看起来没问题对吧?但对模型来说,它需要的是“什么情况下该用这个技能”的判断依据,而不是“这个技能能干什么”的自我介绍。这个差别,就是“跑通”和“好用”之间的根本差距。
类比一下,Skill就像一个工具箱里的专用工具。工具箱外面贴着说明书,说明书不能只写“这是一把螺丝刀”,得写清楚“遇到十字槽螺丝时使用,不要用来撬东西”。模型也是一样,它需要在面对各种任务时,快速决定要不要取出你的工具。描述写得越像“使用场景说明书”,触发准确率越高。
1.2 设计Skill要遵守的几个核心原则
结合我自己反复修改的经验,现在写Skill基本都遵守四条原则,分享出来给大家参考。
第一,单一职责。一个Skill只做一件事,做好一件事。比如“代码审查”和“代码格式化”就应该拆成两个Skill,而不是揉在同一个里。原因很简单:模型选择技能时靠的是描述匹配,技能职责越模糊,匹配越不稳定。你把两件事写进同一个描述里,模型经常会在只需要格式化的时候,把整个审查流程都跑一遍。
第二,最小惊讶原则。Skill的行为要和它的名字、描述高度一致。名字叫“提取PDF表格”,就老老实实做提取,不要顺手做一堆OCR、翻译、数据分析之类的额外操作。模型和用户对Skill的预期,全部来自名字和描述,如果行为超出预期,会使输出结果难以把握。
第三,说明即代码。这是Agent Skills一个很有特色的点:Skill的核心不是代码,而是“代码的使用说明书”。模型通过SKILL.md来理解如何调用你的代码,所以说明文档的优先级甚至高于脚本本身。脚本写得再好,说明文档没写清楚,模型照样不会用或者用错。反过来,只要SKILL.md足够清晰,脚本哪怕只是几十行的简单逻辑,也能发挥很大作用。
第四,示例驱动设计。这是我自己提高触发准确率最有效的一招:在SKILL.md里至少给一个具体示例,说明“什么样的输入、什么样的上下文,应该使用本技能”,并给出调用方式和预期输出。这比简单描述触发条件有用得多,因为模型对具体示例的理解能力,比对抽象规则的理解能力强很多。
这四个原则看起来简单,但真正落到每一个字段、每一段正文里,会有不少细节值得打磨。下面我拆开讲。
2. 拆解一份高质量SKILL.md的写法
2.1 YAML头信息的正确填法
先看SKILL.md最开头的YAML块,这个区域直接影响模型要不要主动选择你的技能。一个标准示例长这样:
--- name: extract-pdf-tables description: 当需要从PDF文件中提取表格数据时使用。适合处理包含结构化表格的发票、报告、研究论文等文档。如果文档本身是扫描图片,请先使用OCR预处理技能,不要使用本技能。 allowed-tools: - Read - Write ---重点在description。我见过太多人把description写成“一个提取PDF表格的Python工具”,这基本等于没写。模型的注意力有限,它读到你describe时,需要快速判断“我现在这个任务,跟这个技能匹不匹配”。因此description一定要包含三个要素:触发场景、处理对象、边界条件。
触发场景就是“当用户想要…… / 当任务涉及……的时候使用”。处理对象要具体,比如“PDF文件中的表格数据”,而不是“文档处理”。边界条件值得多说一句,一定要写清楚“什么时候不要用它”,比如上面例子里的扫描件情况。这个负向边界能大幅减少错误触发。
allowed-tools这块容易被忽略。它是用来限制技能运行期间模型可以调用哪些内置工具的,相当于给技能一个沙箱化的许可列表。如果技能只需要读文件和写结果,就只给Read和Write,不要开放Edit、Bash之类的权限。这不是为了防模型,而是为了防止误操作:权限越宽,意外行为越多。
2.2 正文结构的编排逻辑
YAML下面是正文,模型会完整读取这块内容。这里不是随便写写使用步骤就完事,编排顺序很有讲究。
我建议顺序是这样的:先给“快速上手示例”,再写“输入参数说明”,然后写“执行步骤”,最后补“输出格式规范”和“注意事项”。为什么示例要放最前面?因为模型读长文档时,注意力会自然集中在开头和结尾,开头能建立对技能的整体认知,后面再给规则时更容易理解。你如果把示例放最后,模型前面读一堆抽象规则,可能已经转晕了。
快速上手示例最好是完整可运行的一段对话流程,比如:
## 快速上手 当用户说“把这份PDF里的表格抽出来”或“这个报告里的数据表帮我整理成CSV”时: 1. 运行 `python extract_tables.py --input <pdf路径> --output <csv路径>` 2. 脚本执行结束后,读取生成的CSV,并总结表格内容给用户 示例: 用户:帮我看看这份季度报表里的所有表格 助手:运行 `python extract_tables.py --input ./reports/q3.pdf --output ./reports/q3_tables.csv`这还不够,参数说明要单独写清楚。模型不是程序员,它不会自己去看Python脚本的argparse定义,必须在文档里逐项写明“路径用绝对路径还是相对路径”“输出格式有什么要求”“有没有大小限制”。这一步直接决定了脚本被调用时参数会不会传错。
执行步骤建议写成带编号的有序列表,越细越好。这等于是在告诉模型“你先干什么、再干什么、最后干什么”。如果存在分支情况,比如“如果PDF里没找到表格,尝试第二页重新扫描一遍”,也一定要写出来。模型不擅长自己推断分支逻辑,全靠文档喂。
输出格式规范经常被人漏掉。模型执行完脚本之后,怎么向用户汇报结果,是需要约束的。比如“必须以Markdown表格形式展示提取结果”“CSV文件保存到项目output目录,并在回复中给出相对路径”。有明确格式规范,输出才能稳定可预期,这也是评测Skill质量的一个关键维度。
2.3 脚本与文档如何保持同步
还有一个很实用的细节:SKILL.md文档里提到的脚本参数、执行命令,一定要和实际代码完全一致。协同开发和多次修改后,文档和代码漂移是常态。模型只相信文档,如果文档写了--input,脚本里实际用的是--source,调用就会直接失败。而且这种错误特别隐蔽,因为你自己知道脚本怎么用,但模型不知道。
我建议为每个Skill在SKILL.md里强制放一个自检区块,比如“最近验证过的命令示例”,每次改完脚本就更新一次。还可以在脚本开头写一行注释,标明对应的SKILL.md文档路径。虽然不是强制要求,但维护多套Skill之后,这能省下大量排查时间。
3. 实操过程:从零实现一个代码审查Skill
理论和写法说了一堆,不如完整走一遍。这一节带着大家实现一个真正好用的“Python代码审查Skill”,整个过程包括设计、编写SKILL.md、编写配套脚本、注册调试四个环节。这也是我目前所有Skill里最常用的一个,几乎每个项目都会部署一份。
3.1 需求分析与Skill设计
先说需求。我日常要Review不少Python代码,重点是找出明显的Bug风险、未使用的变量、过长的函数、危险的异常吞没等。这些事如果靠人肉看,效率很低,交给大模型直接做,输出又太自由,每次格式都不一样。于是我决定写一个Skill,让模型调用脚本做静态分析,然后结合分析结果再给出评审意见。
设计阶段,我给这个Skill定了三个约束:
- 输入:一个Python文件路径或一个目录路径。
- 输出:JSON格式的静态分析结果,包括问题列表、严重级别、建议。
- 边界:不做自动修复,只分析和建议。如果需要修改代码,模型必须另行询问用户。
这个边界条件很重要,因为它规约了模型的自由发挥空间。按前面说的原则,技能名称、描述、行为三者高度一致。
3.2 编写SKILL.md
有了设计,直接写SKILL.md。下面是一份完整实例,每个部分的作用我会逐段解释。
--- name: python-code-review description: 当需要对Python代码进行静态分析和代码审查时使用。适合检查潜在的Bug风险、未使用变量、函数复杂度过高、异常处理不当等问题。本技能只做分析和建议,不直接修改代码。如果用户明确要求自动修复,请先征得用户同意后再结合其他工具进行修改。 allowed-tools: - Read - Write --- # Python代码审查技能 ## 快速上手 当用户说“帮我审查一下这个文件”“检查这段代码有没有问题”或“这个模块的代码质量怎么样”时,使用本技能。 示例: 用户:看看 `src/utils.py` 有没有问题 助手:运行 `python review_code.py --target src/utils.py` ## 输入参数 - `--target`:必填。要审查的Python文件路径或目录路径。支持相对路径和绝对路径,相对路径基于当前项目根目录解析。 - `--format`:可选。输出格式,默认 `json`。目前只支持 `json`。 ## 执行步骤 1. 先确认目标文件或目录存在。如果路径不存在,不要运行脚本,直接告诉用户路径无效。 2. 运行 `python review_code.py --target <路径>` 3. 等待脚本执行完毕后,读取返回的JSON内容。 4. 将JSON中的问题列表逐项审阅,对每个问题补充具体的修改建议。 5. 以Markdown列表形式展示所有问题,并按严重级别排序:Critical, Warning, Suggestion。 ## 输出格式 脚本返回的JSON结构如下: { "files_reviewed": ["src/utils.py"], "issues": [ { "file": "src/utils.py", "line": 15, "severity": "Warning", "type": "unused-variable", "message": "变量 'tmp' 定义了但未被使用", "suggestion": "删除该变量或补上实际调用逻辑" } ] } ## 注意事项 - 如果文件行数少于20行,可以直接快速浏览,不必运行脚本,直接在回复里说明代码较简单即可。 - 脚本执行过程中如果出现异常,不要擅自修改脚本,将报错信息原样展示给用户,并建议检查Python环境依赖。 - 分析结果中,Critical级别的问题必须逐条说明,不得省略。不要小看这份文档,每一段都有明确作用。description里不只写了“什么时候用”,还写了“什么时候不要做什么”,这能防止模型在用户想要“自动修复”时,自己拿起Bash工具一顿改。allowed-tools只给了Read和Write,这意味着模型在技能执行期间不能直接运行任意的Shell命令,最大程度减少了误操作。
快速上手给模型一个具体调用样例,让它知道响应方式。如果description触发成功,模型会照着这个格式来执行。注意这里我用的描述是“用户说……”“助手运行……”,等于把对话模式一起教给模型了。
输出格式这一段很重要。它先定义了脚本JSON结构,再告诉模型如何向用户展示。如果没有这个约束,模型很容易自由发挥,把JSON原样丢给用户,或者漏掉关键问题。加了这个区块,输出质量稳定得多。
3.3 编写配套的Python脚本
SKILL.md的配套脚本可以比较简单,核心是输出稳定的JSON,让模型好处理。下面是我实际用的脚本的简化版。
#!/usr/bin/env python3 # Corresponding SKILL.md: .claude/skills/python-code-review/SKILL.md import argparse import ast import json import os import sys def analyze_file(filepath): with open(filepath, "r", encoding="utf-8") as f: source = f.read() tree = ast.parse(source, filename=filepath) issues = [] # 示例:检测未使用变量 for node in ast.walk(tree): if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name): # 粗略检查:名字在文件中只出现过一次,视为未使用 if source.count(target.id) <= 1: issues.append({ "file": filepath, "line": node.lineno, "severity": "Warning", "type": "unused-variable", "message": f"变量 '{target.id}' 定义了但未被使用", "suggestion": "删除该变量或补上实际调用逻辑" }) return issues def analyze_path(target): files = [] if os.path.isdir(target): for root, dirs, filenames in os.walk(target): dirs[:] = [d for d in dirs if d not in {".git", "__pycache__", "node_modules"}] for filename in filenames: if filename.endswith(".py"): files.append(os.path.join(root, filename)) elif target.endswith(".py"): files.append(target) all_issues = [] for filepath in files: try: all_issues.extend(analyze_file(filepath)) except SyntaxError as exc: all_issues.append({ "file": filepath, "line": exc.lineno or 0, "severity": "Critical", "type": "syntax-error", "message": f"语法错误: {exc.msg}", "suggestion": "修复语法错误后重新审查" }) return {"files_reviewed": files, "issues": all_issues} if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--target", required=True, help="目标文件路径或目录") parser.add_argument("--format", default="json", help="输出格式,目前仅支持json") args = parser.parse_args() result = analyze_path(args.target) print(json.dumps(result, ensure_ascii=False, indent=2))这个脚本的目的很明确:接收SKILL.md里规定的参数,输出模型能处理的JSON。它的静态分析能力其实很有限,但这不重要。真正重要的逻辑判断、上下文理解、问题优先级排序,都由模型在读JSON结果后完成。这就是Agent Skills的设计精髓:脚本负责确定性计算,模型负责语义理解和交互表达。
如果你要给自己的项目写配套脚本,我建议遵循几个原则。参数名要和SKILL.md里写的一一对应,不要自己临时加参数。输出一定要是JSON,而且字段键名固定,因为SKILL.md里的输出格式就直接声明了这套结构。脚本处理异常时,宁可返回带错误信息的JSON,也不要打印一堆堆栈后非零退出,否则模型看到的就是一堆无法解析的日志。
3.4 注册到Claude Code并调试
脚本和SKILL.md写完后,注册方式非常简单。把它放进项目根目录下的.claude/skills/python-code-review/里,文件结构如下:
.claude/skills/python-code-review/ ├── SKILL.md └── review_code.py注意,SKILL.md必须放在技能目录的根目录下,名字严格叫SKILL.md,不能改名。目录名是技能的唯一标识,会和YAML里的name对应。放好之后,打开Claude Code,直接说“帮我审查一下src/utils.py”,正常情况下模型就会自动识别并调用这个技能。
第一次调试时,可以手动触发一下,在对话里输入@python-code-review,强制激活这个技能,看看模型能不能正确找到脚本路径。如果模型找不到脚本,大概率是路径写错了,或者SKILL.md里的执行命令没写完整。我建议所有执行命令都用项目根目录的相对路径来写,确保任何工作目录下都能正确解析。
如果自动触发不生效,排查顺序也很固定:先看description是否清晰描述了触发场景,再看有没有足够的示例引导,最后检查技能目录结构是否符合规范。多数情况下,前两项就能解决问题。
4. 常见问题与排查技巧实录
4.1 模型为什么不调用我的技能
这个问题我遇到过太多次。最典型的原因是description里只写了“技能能做什么”,没有写“什么时候该用”。模型在开放式对话里,要把你的描述和用户需求做匹配,如果描述里全是功能,没有场景,匹配自然不准确。改法很简单:把description改成“当用户想……时使用”的句式,并明确边界。
第二个常见原因是技能名称和描述不自洽。比如目录名是pdf-table-extractor,description里写的却是“从Excel和PDF中提取表格”,模型面对混糊信息时会倾向于不用。名称、目录、描述三者必须统一。
还有一个隐蔽原因:项目里技能太多,互相冲突。模型面对多个相似技能时,会倾向于选择那个description最具体、最匹配当前上下文的。如果你的技能明显和其他技能功能重叠,那就要靠更精准的description来区分。比如两个技能都能处理文件,一个是“读取并总结文件”,一个是“按指定格式转换文件”,各自的边界必须写透。
4.2 技能执行过程中报错怎么办
执行报错分两类。一类是脚本本身的代码错误,比如依赖没装、路径不存在。这类问题SKILL.md里就要预判,提前写好“如果脚本异常,不要自行修改,把报错展示给用户”。我在实际使用中发现,如果不写这个约束,模型在脚本报错时真的会尝试用Bash去打开脚本文件、擅自改代码,然后越改越乱。
另一类是权限问题。有时候脚本需要写一个非项目目录下的文件,或者执行一个需要特殊环境的命令,这些都会被沙箱拦下来。我的经验是:尽量让Skill只操作项目目录内的文件,输入输出路径都在项目范围内。如果一定要访问外部路径,功能性写到SKILL.md的注意事项里,明确说明需要用户确认。不要指望模型自己意识到权限边界。
还有一类调试经典问题,就是脚本本身没问题,但模型传参传错了。比如SKILL.md里写了--target,脚本里写的也是--target,但模型却传了--path。这个多半是SKILL.md示例不够清晰,或者描述里同时出现了两种参数名。解决方法是检查全文,确保所有涉及参数的写法完全统一,最好直接在示例里把完整命令写一遍。
4.3 技能多起来之后如何长期维护
Skill数量一多,维护成本会明显上升。我现在同时维护十几个技能,有一整套自己的管理习惯。
命名上,全部用小写字母和连字符,比如python-code-review、pdf-table-extractor。中文名字不是不行,但跨平台、跨终端的兼容性不如英文稳定。目录结构上,我会按用途分两层目录管理,比如.claude/skills/data-processing/pdf-table-extractor/,这样找起来直观。
定期清理也很重要。有些Skill写完用了几次发现不好用,不用抱着不放。我的判断标准是:如果连续一个月没被触发过,或者触发后输出质量一直不满意,就重写或归档。Agent Skills的价值在于精准,而不是数量多。
维护上,我强烈建议给每个Skill目录单独放一个README.md,记录这个技能的版本变更。这不算必须,但迭代到第三四个版本后,你会发现当初的设计理由早就忘了,有记录才能高效迭代。
4.4 几个值得一试的进阶技巧
最后分享几个我自己摸索出来的小技巧。
第一个,用“错误示例”反向标注。在SKILL.md里加一小段“不要这样使用本技能”,比如“不要用本技能解析PDF内的文本段落,文本提取请使用另一个技能”。这种负向示例能把误触发率压到很低。我试过在多个Skill里加这种段落,确实有效果。
第二个,设置输出模板。对产出格式要求严格的场景,直接在SKILL.md里给一个输出示例模板,模型会照着这个模板的格式来生成。这比在注意事项里写一百字“必须按照XXX格式输出”有用得多。
第三个,将常用Skill固化成项目模板。如果你在某类项目上反复使用同一组技能,可以把.claude/skills目录直接放进项目模板仓库中。新项目开箱即用,不用每次重新配置。这个用法在协作场景下特别省事,团队每个人拉到同一个模板,技能也就同步好了。
第四个技巧是给技能加“自检步骤”。在SKILL.md里让模型执行完任务后,自己对照“输出格式”检查一遍。这个简单的小步骤,能把输出格式漏项的问题减少一半。
写Skill这件事,一开始只是图新鲜,用着用着反而成了我梳理工作流的习惯。每写一个Skill,我被迫想清楚一个任务在什么场景下发生、边界在哪、产出物是什么、哪些环节可以自动化。这种梳理本身比技能带来的自动化价值还要大。如果你也在用Agent Skills,建议不要只满足于跑通几个示例,试着把一个真实工作中的高频任务,按这篇的思路完整包装成一个Skill,你会明显感受到差别。下一次迭代,我打算把手里的Skill做一轮基于评测集的回归测试,量化一下每个技能的触发准确率和输出质量,到时候再跟大家聊具体数据和优化思路。