最近这半年,“skills”在 Agent 项目里的出现频率高得离谱。不管你是做企业内部自动化,还是在捣鼓个人助理,开会、看 PR、翻技术群,绕不开这个词。我一开始也以为它就是“给大模型写 prompt”的另一种说法,后来把一个遗留系统里的重复流程真正改成 skills 结构之后,才发现这玩意儿不是概念包装,它是真的能把模型的能力边界往外推一层。这篇文章就围绕 skills 的实际落地来写,适合已经在用或者打算用 Agent 处理真实任务的开发者。我会从最基础的结构讲起,给一个可以直接抄的完整示例,再把部署、调用、踩坑和组合用法都过一遍。
1. 为什么“skills”突然成了 Agent 开发里的高频词
先说结论:skills 解决的痛点是“模型什么都会一点,但什么都不熟”。你在 system prompt 里写一百条规则,模型确实会听,但每次对话都要把这些规则重新读一遍,token 在烧,效果还不见得好。更麻烦的是,一旦规则多了,它们之间会互相干扰,你改一条,另外几条就变得模棱两可。
传统做法大概有三条路。
第一条是纯 prompt 工程。把操作手册、业务规则、输出格式全部堆进 system prompt。优点是简单,缺点是上下文越来越胖,推理速度变慢,维护成本直线上升。而且模型对长文本中段内容的注意力会衰减,你精心写的第 80 条规则,它可能真的“看不见”。
第二条是 function calling。让模型调用你定义好的函数,适合获取实时数据、操作外部 API。但它解决的问题是“连接”,不负责“教会模型怎么把一个多步骤的活儿干完”。你还是要自己在函数外面写清楚什么时候该调、参数怎么传、返回结果怎么处理。
第三条就是 skills。它把一整套流程、说明、脚本打包成一个独立单元,平时完全不占对话上下文,模型判断“现在需要这个技能”的时候,才把对应文件加载进来,按里面的说明一步步执行。
我举个具体场景。我们之前有个数据处理任务,需要把客户发来的 Excel 文件清洗、转换、合并,再生成一份 PDF 报告。用 function calling 做也行,但每个环节都要单独写函数定义和调用逻辑,模型在函数之间跳来跳去,稍微复杂一点就乱。后来我把整套流程写成一个 skill,里面就三样东西:一份操作说明、一个 Python 脚本、一个输出模板。模型拿到新文件后,自己读 skill 说明,按步骤执行,一步到位。
所以 skills 本质上不是替代 prompt 或 function calling,而是把这两者能力的“可复用部分”沉淀下来。它最核心的价值不是“让模型做什么”,而是“让模型知道你这里有一整套干这个活的方法,并且随时可以调用”。这一点想通之后,后面看结构、写配置、排故障就顺了。
2. 一个 skill 的基本盘:目录结构、SKILL.md 与加载逻辑
先看目录,一个 skill 就是一个文件夹,官方推荐的命名是短横线小写,比如log-archiver、weekly-report-builder。里面最关键的文件是SKILL.md,这是模型的“说明书”,也是唯一的入口。其余脚本和资源都放在子目录里,按需加载。
my-skill/ ├── SKILL.md ├── scripts/ │ ├── main.py │ └── config.json └── assets/ └── templates/ └── report.md结构看上去简单,但这里有一个容易被忽略的设计逻辑:模型默认只读SKILL.md,不会主动去翻scripts/和assets/。你必须在SKILL.md里明确告诉它脚本在哪里、怎么运行、什么时候该读 assets 里的模板。换句话说,SKILL.md是写给模型看的工作手册,而不是写给人类看的说明文档。
SKILL.md的第一部分是 YAML frontmatter,用来声明元信息:
--- name: log-archiver description: 用于归档和压缩日志文件。当用户提到日志整理、磁盘空间清理、旧日志归档时使用。 ---name是唯一标识,description是触发条件。description特别重要,因为模型就是靠它来判断“什么时候该亮出这个技能”。写得太窄,模型想不起来用;写得太宽,模型在任何不相关的对话里都会强行套用。
SKILL.md的正文部分直接是 Markdown,写给模型看的操作步骤。你可以写得直白,甚至可以像给实习生交代任务一样,分步骤、给命令、写注意事项。以官方 skills 仓库为例,里面 docx、pdf、pptx、xlsx 这些技能都是这么组织的:先说明目标,再给出脚本调用方式,最后说明对输出结果的预期。
我自己的习惯是,正文里控制在一屏以内能读完,步骤不超过五条。如果流程特别长,把它拆成多个 skill 或者让脚本内部自己处理复杂分支,而不是让模型在 Markdown 里做大量条件判断。模型读说明是为了知道“什么时候运行什么命令、结果怎么检查”,不是来模拟一个微型程序员的。
3. 手写一个真实技能:日志归档 skill 从需求拆解到可用配置
理论说多了容易飘,直接来一个能落地的例子。假设你的服务器上有一堆应用日志,按天增长,磁盘空间告急,手工归档太费劲。这个需求适合做成 skill,因为它的流程固定、可复用、而且不同环境下都能用。
先拆解需求:给定日志目录,把超过 30 天的.log文件按月份归档到归档目录并压缩,最后输出本次归档了哪些文件。就这三件事。
第一步,建目录和SKILL.md:
--- name: log-archiver description: 将指定目录下的日志文件按月份归档并压缩,用于磁盘空间清理和日志轮转。当提到日志归档、清理旧日志、日志文件太多、磁盘空间不足时使用。 --- # 日志归档 目标:把 source 参数指定目录下的 .log 文件,按最后修改时间归档到 archive 参数指定目录,压缩为 .gz 格式,并删除原文件。 执行步骤: 1. 运行以下命令: python3 scripts/archive_logs.py --source /var/log/app --archive /data/log-archive --keep-days 30 2. 脚本会输出每一条归档记录,格式为 `archived: <文件路径>`。 3. 最后一行输出 `done, total N files`。N 为 0 表示没有需要归档的文件。 4. 如果脚本报错,直接向用户展示错误信息,并根据错误提示排查路径是否可读、目录是否存在。第二步,写脚本。我用 Python 标准库,不引入任何第三方依赖,这样在任何带 Python 3 的机器上都能直接跑:
#!/usr/bin/env python3 """归档指定目录下超过保留期限的日志文件。""" import argparse import gzip import shutil from datetime import datetime from pathlib import Path def archive_logs(source_dir: Path, archive_dir: Path, keep_days: int) -> list[str]: cutoff = datetime.now().timestamp() - keep_days * 86400 archive_dir.mkdir(parents=True, exist_ok=True) archived = [] for log_file in sorted(source_dir.glob("*.log")): mtime = log_file.stat().st_mtime if mtime < cutoff: date_str = datetime.fromtimestamp(mtime).strftime("%Y-%m") dest_dir = archive_dir / date_str dest_dir.mkdir(parents=True, exist_ok=True) dest_path = dest_dir / (log_file.stem + f"_{date_str}" + log_file.suffix + ".gz") with log_file.open("rb") as f_in, gzip.open(dest_path, "wb") as f_out: shutil.copyfileobj(f_in, f_out) log_file.unlink() archived.append(str(dest_path)) return archived if __name__ == "__main__": parser = argparse.ArgumentParser(description="归档日志文件") parser.add_argument("--source", type=Path, required=True, help="日志目录") parser.add_argument("--archive", type=Path, required=True, help="归档目录") parser.add_argument("--keep-days", type=int, default=30, help="保留天数") args = parser.parse_args() for item in archive_logs(args.source, args.archive, args.keep_days): print(f"archived: {item}") print(f"done, total {len(archived)} files")这段脚本有一个值得注意的点:删除原文件前先完成了压缩写入并确认没有异常。这不是随手写的,而是考虑到如果压缩中途失败,原文件又被删了,数据就没了。归档类操作优先保证数据安全。
第三步,本地手动测试。先不用模型,自己跑一遍脚本:
python3 scripts/archive_logs.py --source ./test-logs --archive ./test-archive --keep-days 30看到输出正常,再把它交给模型用。这样后面排查问题的时候,你就知道脚本本身没问题,问题出在调用方式或环境上。
4. 部署到项目里:加载路径与让模型“想起来用”
写好 skill 之后要放进模型能读到的地方。我常用的方式有两种:项目级和用户级。
项目级是把 skill 放到当前项目的.claude/skills/目录下:
your-project/ ├── .claude/ │ └── skills/ │ └── log-archiver/ │ ├── SKILL.md │ └── scripts/ │ └── archive_logs.py这样整个项目共享这个 skill,团队克隆仓库之后自带技能,适合跟业务强相关的技能。
用户级是放到用户目录下,比如~/.claude/skills/,对所有项目生效。适合放一些通用的、跨项目的技能,比如文档格式转换、代码仓库整理、文件批处理之类。两者可以共存,同名时项目级优先。
把 skill 放好之后,怎么确认它能被调用?最简单的办法是主动触发一次。比如直接跟模型说“帮我把 /var/log/app 下的旧日志归档一下”。如果它真的去执行了,说明加载没问题。如果它回你一段“你可以运行以下命令”这种话,说明它没识别出这个技能,或者description没写好。
我见过很多次这种问题,最后都出在description上。模型匹配技能,靠的是把用户当前请求跟description做语义对齐。你写“用于归档和压缩日志文件”,用户说“磁盘快满了帮我清理一下”,模型不一定能反应过来。更好的写法是把用户可能说的话也放进去:“当用户提到日志归档、磁盘空间不足、旧日志清理、log archive 时使用”。这相当于给模型几个“钩子”,让它更容易命中。
如果模型调用了 skill,但它执行时没有按SKILL.md里的步骤走,比如自己改了脚本路径或者跳过了某一步,那就要检查正文是不是有歧义。每一条步骤最好都给出明确的命令和预期的输出,不要让模型自己“发挥”。模型在没有明确指令时倾向于脑补,而脑补在生产环境里就是灾难。
5. 实测中踩过的坑:路径、依赖、权限与调试链路
技能写多了,=踩坑也踩多了。这里把最有代表性的几个问题拎出来,每个都带排查思路,而不是直接甩答案。第一个坑是路径问题,而且是最隐蔽的。
我把SKILL.md里的命令写成了python3 scripts/archive_logs.py ...,但模型执行的时候,工作目录不一定在 skill 所在目录。它在项目根目录下跑,就找不到scripts/。当时我排查了很久,因为手动在 skill 目录里跑脚本一切正常,但模型一调用就报错“No such file or directory”。解决办法有两个:一是命令里写相对当前工作目录的路径,比较脆弱;二是脚本开头通过Path(__file__).parent定位自身位置,再基于这个位置拼接路径。推荐第二种,因为不管从哪个目录调用都不会出错。
第二个坑是第三方依赖。我早期写过一个处理 Excel 的 skill,脚本里import openpyxl,结果模型环境里没装。报错那一刻我才意识到,skill 脚本不能默认环境里什么都有。现在我的原则是:优先用标准库实现;如果必须用第三方库,在SKILL.md里明确写安装命令,并在脚本开头做 import 异常提示。你可以在技能说明里加一句“运行前先执行 pip install openpyxl”,也可以让脚本在缺依赖时打印安装提示,让模型看到之后自己装。
第三个坑是权限和文件安全。模型运行脚本时通常没有 sudo 权限,脚本里一旦有写入 system 目录、修改 root 权限文件的操作,必然失败。更危险的是删除操作:日志归档脚本里有log_file.unlink(),如果参数校验不严,模型随手传入一个根目录或误把非日志文件传进来,后果不可控。我现在会在脚本里做两层防护:限定只处理*.log后缀文件,并且要求--source路径存在且是目录。删除操作之前先打印警告,让模型在输出里明确“我准备删除以下文件”,再执行。
排查问题的基本链路是这样的:先手动跑脚本,确认脚本本身没问题;再看SKILL.md里的命令路径是否可靠;接着让模型执行时把完整输出打出来,看它实际运行了什么命令;最后根据错误信息逐层定位。我一般会让模型把“我准备怎么做、执行了哪些命令、输出是什么”完整展示出来。这三点看起来基础,但很多问题其实就在其中一环。
调试时还有一个技巧:把SKILL.md里的步骤拆得足够细,让模型每一步都能输出中间结果。比如归档脚本,最好每归档一个文件就打印一行archived: xxx,而不是最后只给一句“归档完成”。模型能根据中间输出判断哪里出了问题,你自己排查的时候也有据可循。
6. 更进一步:多个 skill 组合成真实工作流
单技能的威力有限,真正好用的是把多个 skill 串起来完成一条完整的工作流。比如“每周自动生成运维周报”这个任务,就可以拆成三个 skill:log-archiver负责归档日志并统计各应用日志量、metric-collector负责从监控接口拉取指标、docx-builder负责把前面两步的结果生成 Word 文档。三个技能各自独立,但通过SKILL.md里的描述串成一个流程。
关键是怎么让模型知道要按顺序调用。我的做法是在每个SKILL.md里都写一段“关联技能”,例如在metric-collector的描述里写:“当用户要求生成周报时,先运行 log-archiver 获取日志统计,再运行 metric-collector 获取指标,最后使用 docx-builder 生成文档。”模型的执行路径就清晰了。
技能拆分的粒度也很讲究。太细会导致模型频繁做“决策”,每一步都要读一个 skill,效率低;太粗又会导致一个技能里堆了太多逻辑,复用性差。我现在遵循的原则是:一个 skill 只解决一个明确的问题,但这个问题本身是完整闭环。日志归档就是“清理+压缩+输出统计”,不包含发送通知;发送通知单独拆出去。这样任何一个环节要替换或调整,都不影响其他部分。
再补充一点,如果已经在用 MCP 或类似的外部工具连接机制,不要慌,skills 和它可以共存。MCP 适合连接外部系统拿数据,skills 适合把数据处理逻辑沉淀下来。两者的边界很简单:数据从哪来靠 MCP,数据怎么处理靠 skills。配合起来之后,Agent 才能从“会说话”进化到“会干活”。
最后分享一点个人体会:技能化改造不是把旧的 prompt 搬个家那么简单。我做过好几次“重构式”技能化,就是把系统里一段长 prompt 直接塞进SKILL.md,结果模型调用时效果并不好。后来想明白了,prompt 是给人看的指令,而SKILL.md是要执行的工作手册——它的表达方式要更像“操作 SOP”,更短、更明确、每一条都能被验证。把这个转变做完之后,技能的成功率才是真正稳定下来。