开头就直接点明Skills在当下Agent开发里的位置,不铺垫太多,直接讲清楚这个概念重要在哪。
我最近翻了不少Agent相关的工程代码,发现一个特别明显的变化:大家讨论的不再是"怎么给大模型写Prompt",而是"这个能力应该封装成什么Skill"。这个词在圈子里几乎被用滥了,但真正能把它讲清楚、做规范的人并不多。有的项目把Skill等同于一个Python脚本,有的把它当成一个Prompt模板,还有的干脆把所有工具函数都叫Skill。
这篇东西我打算把"Skills"这件事从头到尾捋一遍。它到底是什么、和Tool/Plugin/Workflow的区别在哪、一个合格的Skill应该长什么样、怎么写才能让模型稳定地用起来,以及我实际落地时踩过的坑。目标是让你看完之后,能直接动手把你手头的重复性工作改造成一组合格的Skills,而不是只停留在概念层面。
我默认看这篇文章的同学对Agent、大模型、Prompt Engineering有一定了解,但不要求你写过完整的Skill,我会从设计思路讲到代码实现,把路径补全。
1. 先搞清楚:Skills到底是个什么东西
1.1 Agent开发为什么突然都在聊Skills
在Agent开发里,模型本身只负责"理解意图"和"生成决策",真正干活的还是外部能力。早期大家习惯把这些能力一股脑塞进System Prompt里,让模型自己看着办。问题是Prompt长度有限,模型对超长上下文的注意力会明显衰减,而且每次对话都把所有能力的描述加载一遍,token开销实在不划算。
Skills的出现,本质上是为了解决"能力如何被按需加载"这个问题。它的核心思路是:把某项可复用的能力,连同它的触发条件、参数说明、注意事项和使用示例,打包成一套自包含的描述文件,普通情况下不参与上下文,只有模型判断需要时才会被动态注入。
这和我们人类学技能的路子很像。你不会把"修水管"的全套知识时刻记在脑子里,但当你看到厨房漏水,你会想起来"我有这个能力",然后按流程操作。Skills就是给Agent准备的那本"能力手册",平时放在书架上,用到哪本抽哪本。
1.2 Skill、Tool、Plugin、Workflow到底差在哪
这四个词混着用的现象太常见了,先掰开揉碎。
| 概念 | 核心特征 | 典型产出 | 我的理解 |
|---|---|---|---|
| Tool | 单次原子操作,输入输出都很明确 | 函数、API封装 | 一个动作,不包含决策逻辑 |
| Skill | 一套完整的能力方案,包含触发判断、执行流程、输出规范 | 描述文件+脚本+示例 | 一组动作的编排,带使用说明书 |
| Plugin | 程序层面的扩展封装,通常带依赖和版本管理 | 安装包、插件框架 | 解决"怎么装",不一定考虑模型怎么用 |
| Workflow | 固定顺序的流程编排,节点固定,不依赖模型临时发挥 | 流程图、DAG配置 | 流程确定性优先,留给模型的空间少 |
我见过最典型的误用,是把Skill写成一个大号的Tool文档。比如有些人写"代码检查Skill",描述只有一句"检查代码质量",参数列表写了二十多个,模型根本不知道什么场景该调、输出该长什么样。这本质上还是Tool的思路,只是套了个Skill的壳。
Skill和Tool最大的差异在于"描述是否引导决策"。Tool文档只需要说清楚"我接受什么、返回什么";Skill还必须回答"我什么时候该被使用、我不适合处理什么、我的副作用是什么、我的输出如何被解读"。这些信息是Agent做规划时的依据,少了它们,模型就会在错误的场景下调用你精心写好的能力。
2. 从零搭一个Skill:目录设计、数据格式与编写规范
2.1 一个规范Skill的目录骨架
先说结论,一个标准的Skill目录长这样:
repo_health_skill/ ├── SKILL.md ├── scripts/ │ ├── scan_codebase.py │ └── report_generator.py ├── assets/ │ └── report_template.md ├── tests/ │ ├── test_scan.py │ └── cases/ │ ├── normal_repo/ │ └── edge_case_repo/ └── requirements.txtSKILL.md是灵魂,scripts是执行层,assets放非代码资源,tests是很多人会忽略但极其重要的一部分。我后面会专门说为什么测试对Skills至关重要,这里先记住:没有tests的Skill,就像没有刹车说明书的汽车,开着能走,但你不知道它什么时候会失控。
具体到实际项目,你不一定每个目录都建全。如果Skill只有几百行逻辑,没有独立资源文件,那assets可以省掉。但SKILL.md和scripts这两个是底线,缺一个都不算完整的Skill。
2.2 SKILL.md怎么写才像话
SKILL.md本质上是给模型读的说明书,而不是给人看的开发文档。这意味着它的组织方式必须符合模型从"判断"到"调用"再到"解读"的思维路径。
我比较推荐的结构是这样的:
--- name: repo_health_scan description: 对本地代码仓库进行健康度体检,输出结构化体检报告。适合在接手不熟悉代码库、code review前、或CI流程中需要快速定位风险点时使用。 when_to_use: 用户要求评估代码质量、统计仓库规模、发现WIP代码、检查依赖配置或README缺失情况时 when_not_to_use: 用户只要求修改某个具体bug、或需要深度业务逻辑审查时,请勿调用此技能 parameters: repo_path: type: string description: 代码仓库的本地路径 required: true include_details: type: boolean description: 是否输出文件级明细,默认false required: false output_formats: - markdown_report version: 1.0.0 license: MIT ---YAML头之后,正文分几个固定段落:
- 概述:三到五句话说明这个Skill能干什么、不能干什么。
- 执行流程:用步骤列表描述脚本调用的前后顺序,以及每步输出怎么处理。
- 输出规范:报告的结构、评分规则、必须包含的章节。
- 注意事项:哪些操作有副作用、哪些目录不能碰、超时阈值多少。
- 完整示例:给一个理想输出的样例,模型会拿它当"标准答案"参考。
- 错误处理:脚本可能返回哪些异常码、分别代表什么含义、模型该如何向用户解释。
很多人写SKILL.md时容易犯一个毛病:写得像面向人的README,满屏的"该模块基于Python 3.10开发,使用Click库实现参数解析,支持JSON和YAML两种输出格式"。这些信息对模型做决策没有任何帮助,模型不关心你用什么库,只关心"什么时候调用、怎么调、结果怎么看"。面向模型的文档,要刻意避免实现细节,把注意力放在"使用时机"和"输出解读"上。
2.3 脚本部分的接口约定与参数风格
SKILL.md负责"决策层",scripts负责"执行层"。两者之间的接口设计,直接决定这个Skill在真实环境中稳不稳。
我采用的约定如下:
- 每个脚本必须是可独立执行的命令行程序,避免"必须先import某个公共模块才能跑"的情况。
- 统一使用argparse或click解析参数,不直接从环境变量读配置,保证可调试性。
- 输出统一写stdout,错误写stderr,退出码三态:0成功、1业务错误(比如路径不存在)、2内部错误(比如依赖缺失)。
- 所有输出内容必须是纯文本或JSON/Markdown,不打印额外日志到stdout,否则模型会分不清哪部分是结果、哪部分是日志。
参数命名上,我建议全小写加下划线,避免驼峰。原因很实际:模型在生成参数值时,倾向于复制用户的原始表达,而下划线风格和文件系统命令的直觉更匹配,能减少模型编造参数名的概率。
还有一个很多人忽略的点:脚本必须能处理"空输入"。比如仓库路径指向一个空目录,你需要返回一个明确提示"该目录为空,无法生成体检报告",而不是抛一个Python traceback。模型对traceback的处理能力很弱,它不知道应该重试、换路径还是向用户解释,但一段清晰的业务错误信息会触发它正确的下一步行为。
3. 手写一个"代码仓库体检"Skill的完整过程
3.1 需求拆解与方案选型
理论说再多都不如动手来一遍。我拿"代码仓库体检"这个场景演示,因为它足够常见、又不复杂到失控。
需求其实很直接:接手一个陌生代码库时,我希望快速知道这个仓库的健康状况——代码量多大、有没有WIP痕迹、README是不是摆设、依赖声明是否完整、有没有明显的垃圾文件。以前靠人一个个翻,效率太低。
拆解下来,这个Skill需要做四件事:
- 统计仓库规模:文件数、代码行数、语言分布。
- 检查文档完备性:README是否存在、内容是否足够。
- 扫描WIP痕迹:TODO/FIXME注释密度、临时文件残留。
- 分析依赖配置:有没有锁文件、依赖声明是否完整。
方案选型上,我特意不用任何第三方代码分析库,只基于标准库的pathlib、re、collections写。原因有两个:一是降低部署成本,requirements.txt为空也能跑;二是这个Skill的核心价值在于"快速体检",不在于"深度分析",用ast做语法树分析反而过重了,误判率也会更高。
3.2 骨架落盘与脚本实现
先写SKILL.md的正文部分,重点在"执行流程"和"输出规范":
## Execution Flow 1. 使用参数repo_path定位仓库目录。 2. 调用scripts/scan_codebase.py --repo-path <repo_path> [--include-details]。 3. 脚本输出JSON结果到stdout。 4. 调用scripts/report_generator.py --scan-result <json_string> --template assets/report_template.md。 5. 将生成的Markdown报告作为最终回复内容,不要在报告中添加额外评论。 ## Output Specification 报告必须包含以下章节: - 仓库概况(文件总数、代码行数、语言Top3) - 文档健康度(README存在性、篇幅、是否有明确的使用说明) - 代码清洁度(WIP注释密度、临时文件数、空目录数) - 依赖完整度(是否有requirements.txt/package.json/go.mod等声明文件,是否有对应锁文件) - 综合评分(满分100,按权重加权计算)然后写scan_codebase.py的核心逻辑。重点在于各部分统计的实现:
#!/usr/bin/env python3 """仓库体检扫描脚本,输出JSON格式的扫描结果。""" import argparse import json import re from collections import Counter from pathlib import Path # 常见的临时文件/目录模式 TEMP_PATTERNS = [ r"\.DS_Store$", r"^__pycache__$", r"\.pyc$", r"^tmp_", r"~$", r"\.bak$", r"^\.cache$", ] WIP_MARKERS = re.compile(r"\b(TODO|FIXME|HACK|XXX)\b", re.IGNORECASE) LANGUAGE_EXT_MAP = { ".py": "Python", ".js": "JavaScript", ".ts": "TypeScript", ".java": "Java", ".go": "Go", ".rs": "Rust", ".c": "C", ".cpp": "C++", ".rb": "Ruby", ".php": "PHP", ".vue": "Vue", ".md": "Markdown", ".json": "JSON", ".yml": "YAML", ".yaml": "YAML", ".html": "HTML", ".css": "CSS", ".sh": "Shell", } def scan_repo(repo_path: str, include_details: bool = False) -> dict: root = Path(repo_path) if not root.exists(): return {"status": "error", "message": f"路径不存在: {repo_path}"} if not root.is_dir(): return {"status": "error", "message": f"路径不是目录: {repo_path}"} # 如果目录为空,直接返回空报告,不继续扫描 if not any(root.iterdir()): return {"status": "empty", "message": "该目录为空,无法生成体检报告"} total_files = 0 total_lines = 0 lang_counter = Counter() wip_count = 0 temp_files = [] empty_dirs = [] readme_info = {"exists": False, "size_bytes": 0, "has_setup_guide": False} for path in root.rglob("*"): if path.is_file(): # 跳过.git目录,避免把版本库内部当业务代码统计 if ".git" in path.parts: continue total_files += 1 suffix = path.suffix.lower() if suffix in LANGUAGE_EXT_MAP: lang_counter[LANGUAGE_EXT_MAP[suffix]] += 1 try: content = path.read_text(encoding="utf-8", errors="ignore") total_lines += content.count("\n") + 1 wip_count += len(WIP_MARKERS.findall(content)) except (UnicodeDecodeError, OSError): pass # 判断是否临时文件 if any(re.search(pat, path.name) for pat in TEMP_PATTERNS): temp_files.append(str(path.relative_to(root))) # 识别README if path.name.lower() in ("readme.md", "readme.txt", "readme"): readme_info["exists"] = True readme_info["size_bytes"] = path.stat().st_size head = content[:500].lower() readme_info["has_setup_guide"] = any( kw in head for kw in ["install", "usage", "部署", "运行", "安装"] ) for path in root.rglob("*"): if path.is_dir() and ".git" not in path.parts: # 空目录 + 无子文件 = 空目录 if not any(path.iterdir()): empty_dirs.append(str(path.relative_to(root))) top_langs = lang_counter.most_common(3) return { "status": "success", "repo_path": str(root.resolve()), "total_files": total_files, "total_lines": total_lines, "top_languages": [lang for lang, _ in top_langs], "wip_comment_count": wip_count, "temp_file_count": len(temp_files), "temp_files": temp_files if include_details else temp_files[:5], "empty_dir_count": len(empty_dirs), "readme_info": readme_info, } def main(): parser = argparse.ArgumentParser(description="代码仓库体检扫描器") parser.add_argument("--repo-path", required=True, help="仓库本地路径") parser.add_argument("--include-details", action="store_true", help="输出文件级明细") args = parser.parse_args() result = scan_repo(args.repo_path, args.include_details) print(json.dumps(result, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()这段代码有意识地处理了几个边界情况:不存在的路径、空目录、无法解码的二进制文件、.git目录跳过。我不追求统计的绝对精确,因为Skill的输出是要给模型做定性判断的,误差在5%以内完全不影响结论。
接着是report_generator.py,它负责把JSON转成Markdown评分报告。评分规则我用了加权:代码量占比30%、文档健康度25%、清洁度25%、依赖完整度20%。这个权重不必太纠结,实际使用中发现模型的判断能力足以消化这部分信息,你只需要保证评分结果可解释、可追溯。
3.3 接入测试与调用验证
Skill写完不是直接丢给Agent就完事了,必须要过一遍"模拟调用"。
我建议的测试方式是:用一个真实的项目仓库跑一遍完整流程,逐步验证每个环节。我的测试记录大致长这样:
- 先跑扫描脚本,确认输出JSON的字段完整、没有异常抛出。
- 再跑报告生成器,确认Markdown结构正确。
- 最后把完整的SKILL.md内容塞进一个对话测试里,模拟用户问"帮我看看这个仓库健不健康",观察模型是否能正确判断调用场景、是否能传对参数、是否能照着报告给用户做解读。
其中一个很有价值的细节是:把SKILL.md中的示例报告故意做"坏"一点——比如包含一个只给了60分的仓库,然后在测试中观察模型是否会在60分的基础上做合理解释,而不是机械复读分数。技能文档里给的示例质量,决定了模型输出质量的天花板。
4. 从能跑到好用:设计原则与常见坑
4.1 设计Skills的几个关键原则
原则一:一个Skill只做一件完整的事。判断标准很简单:看它的描述里有没有"和"。如果描述里出现"检查仓库健康并生成报告并发送通知",赶紧拆开。连词是设计过度的信号。
原则二:描述里写场景,不要写功能。错误示范是"提供代码统计功能";正确示范是"在接手陌生代码库时快速评估仓库健康度"。前者描述的是执行动作,后者描述的是使用场景。模型是靠场景匹配来触发调用的,写清场景就是在帮它做决策。
原则三:输入参数宁少勿多。参数越多,模型生成参数值的自由度越高,出错概率也越大。能用默认值解决的问题就不要暴露成参数。我见过有人做一个"发送邮件Skill",参数里居然有"邮件编码方式"这种选项,模型根本不知道该选UTF-8还是GBK,反而常常编造一个错误值。
原则四:给Skill配示例,尤其是边界示例。模型会模仿示例的语气、格式和判断标准来生成输出。你给一个"拒绝场景"示例,告诉它"用户要求删除代码时,你应拒绝调用本Skill并说明原因",模型就真的会学会拒绝。
4.2 我踩过的坑和排查笔记
坑一:description写得太宽,导致模型误调用。我第一次做仓库体检Skill时,description写的是"检查代码质量",结果用户问"这段代码哪里写得不好"时,模型也触发了这个Skill。但体检脚本只看仓库统计,根本不看具体代码逻辑,输出完全驴唇不对马嘴。后来我把description改成了"检查仓库整体健康度和工程规范的完整性,针对的是工程级别的统计信息,不具备代码逻辑审查能力",误调用率明显降下来了。加一句"不具备什么能力",比只说具备什么能力更重要。
坑二:脚本退出码和错误信息不统一,模型无法理解失败原因。早期我的脚本碰到异常时直接打印traceback退出,模型很困惑,回复用户"技能执行出错,可能是指标配置有问题"。后来我改成所有业务错误都输出"error:路径不存在"这种结构化信息,模型就能准确告诉用户"你给的路径不对,请检查路径是否正确"。模型不是调试器,它能处理的是语义清晰的错误提示,不是堆栈信息。
坑三:在SKILL.md里塞了太多与模型决策无关的实现细节。有一版我写了很多"脚本由Python编写,依赖requests库,配置文件支持YAML和JSON"。这导致模型在描述Skill能力时开始胡扯,说它"支持YAML格式的配置"。这些信息完全可以放在代码注释或外部文档里,不该出现在模型看的说明书里。
坑四:忘记处理空输出和超时。如果脚本跑一个超大的仓库,耗时可能超过模型等待的耐心阈值。我的方案是在SKILL.md里明确写"扫描超过30000个文件的仓库时,建议分目录扫描",并且脚本本身对大仓库做了统计上限保护。不要指望模型能处理超时,你要提前在设计层规避超时。
4.3 问题排查速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 模型在错误场景调用了Skill | description场景描述不具体 | 在description中明确when_not_to_use |
| 模型传了不存在的参数名 | 参数说明不清晰,或者参数名和用户表达习惯不匹配 | 用全小写下划线命名;在示例中展示典型的参数值 |
| 模型看不懂脚本输出 | 输出格式不统一或包含噪音日志 | 统一stdout输出格式,把日志和结果分离 |
| 脚本报错时模型胡乱解释 | 错误信息不语义化 | 捕获业务异常,返回可读的错误说明 |
| Skill之间互相抢调用 | 多个Skill描述有重叠 | 明确各自的边界,在when_not_to_use里互相引用排除 |
| 同样的任务这次调、下次不调 | 描述中"确定性触发词"不足 | 在description里列出明确的触发条件词汇 |
排查的时候还有个实用的技巧:把调用记录拉出来看,重点关注模型在"接近边界"的对话里怎么做选择的。边界情况才是考验描述质量的试金石。
5. 关于Skills生态的一些个人判断
Skills这套设计正在迅速演化,各方框架对于"技能应该长什么样"的界定还在动态变化中。但我觉得有两点是确定性的方向:一是Skill会越来越像"可插拔的专家能力",每个Skill本质上是一个微型的领域专家;二是Skill的评测和质量控制会越来越重要,因为随着Agent自主性增强,一个坏Skill的破坏力也会被放大。
对于个人开发者来说,起步方式很简单:梳理你日常重复做的、可以规范流程的事情,把它封装成第一个Skill。你会发现,写SKILL.md的过程本身就是一次深度复盘,它强迫你把自己的经验从"隐性知识"转化为"显性知识"。
我的经验是,先别追求Skill的数量。拥有三个经过实战打磨的Skill,远好过三十个写得很糙的Skill。宁可让Agent只擅长三件事,也别让它看起来什么都会、实际上什么都不精。每多一个Skill,Agent在规划时的选择空间就大一分,选择空间越大,决策错误的风险也越高。
提示:Skill的质量控制要像管理代码一样严格——版本控制、变更记录、测试覆盖,一个都不能少。Skill是Agent的"肌肉记忆",写坏了很难发现,但影响面极大。
我实际使用中还有一个体会:把一个能力写成Skill之后,它对你自己的工作效率提升,可能比对Agent的提升还明显。因为写Skill的过程,本质上是在逼你把一项能力彻底想清楚。想不清楚的地方,模型会用各种方式暴露给你——误调用、错参数、烂输出,这些都是你能力边界的映射。
以后干活的时候,不妨保持一个习惯:每次发现你在重复做一个三步骤以上的操作,就问自己一句——这个要不要抽成一个Skill?攒几个之后,你就会拥有一套自己专属的Agent能力库,这比任何现成的框架模板都靠谱。