☰
Agent Skills实战:用SKILL.md把提示词变成可复用的技能包
2026/10/8 17:12:51 网站建设 项目流程

1. 从"提示词堆砌"到"按需加载的技能包":Skills到底改变了什么

1.1 我最初对Skills的误解

先说个真实经历。今年早些时候我在做一个自动化内容处理项目,需要让AI模型重复完成一系列固定任务:抓取网页正文、清洗格式、按模板生成摘要、再转成特定格式输出。最开始我的做法很朴素——把一大段包含所有规则和示例的提示词塞进每次对话里。刚开始效果还行,但随着规则增加,提示词越来越长,最终变成了一个两千多行的"怪兽",每次调用不仅浪费token,而且模型经常顾此失彼,处理到后面几条规则时就把前面的要求忘了。

我当时就想过一个问题:如果能把不同的处理能力拆成独立模块,让模型在需要的时候才加载对应的那部分指令,是不是就不会互相干扰了?后来我接触到Skills这个概念,发现它解决的正是这个问题。

但说实话,我一开始对Skills有误解,以为它就是"把提示词拆成几个文件放文件夹里"。直到自己动手写了一个完整的Skill并在项目中跑通之后,我才意识到它背后的设计逻辑远比"拆文件"要深。

1.2 Skills的真实定位:程序性知识的文件化

Skills,也就是Agent Skills,本质上是把一类"做事的方法"——包括操作规程、配套脚本、参考资料——打包成一个独立目录,让AI Agent在任务需要时按需读取并执行。

这里有个关键词:按需。

以前我们的做法是"有备无患",把所有可能的指令全部塞进上下文里,让模型自己挑选。这就像你出门前把所有可能用到的工具都背在身上——伞、锤子、扳手、手电筒——结果背包重得走不动路。而Skills的做法是"分门别类放在工具箱里",模型先看一眼每个箱子上写的标签(description),需要哪个开哪个。

这个设计的核心价值在于:上下文窗口是稀缺资源,知识加载应该是检索式的,而不是全量式的。一次任务里,模型可能只需要全部技能中的一两个,其余技能完全不必进入上下文。这既降低了token消耗,也减少了指令之间的干扰。

还有一个容易被忽视的点:Skills是文件系统级的。这意味着版本管理、团队共享、权限控制都变得非常自然。一个技能可以像代码一样被review、被迭代、被回滚,这比在聊天记录里维护提示词要可靠得多。

1.3 与MCP的本质区别

很多人刚接触这个概念时会问:那Skills和MCP(Model Context Protocol)有什么区别?我刚开始也搞混过,后来总结了一个很直观的区分方式:

  • MCP是"给模型工具",模型决定调用哪些工具来完成操作。它强调连接和执行,比如读取数据库、调用外部API、操作文件系统。
  • Skills是"教模型方法",模型读取后知道"这件事应该怎么做"。它强调知识和流程的传递,比如"分析日志时应该按什么顺序排查""生成周报应该包含哪几个板块"。

用一句话总结:MCP解决的是"模型能做什么",Skills解决的是"模型知道怎么做"。实际项目中两者往往配合使用,但它们的定位完全不同。


2. Skill包拆解:一个文件夹就是一个可复用的能力单元

2.1 目录结构约定

我在实际使用中接触到的Skill目录结构大致是这样的(以Anthropic推出的Claude Skills规范为参考):

my-skill/ ├── SKILL.md # 技能主文件,模型最先读取 ├── scripts/ # 配套可执行脚本 ├── references/ # 参考资料,按需加载 └── assets/ # 静态资源文件

这个结构看起来简单,但每个目录的职责边界是有讲究的。

SKILL.md是核心。它分为两部分:YAML格式的frontmatter(元数据)和Markdown格式的正文(指令主体)。frontmatter里的description字段尤其关键,它相当于这个技能在模型眼中的"名片"——模型就是靠它来判断什么时候该调用这个技能。

scripts目录放的是可执行的代码文件。Python、JavaScript、Shell脚本都行。这些脚本的特点是:它们能做的事情需要外部环境支持,而模型本身无法直接完成。比如操作图片、调用命令行工具、解析复杂格式的文件。

references目录放参考资料。这些资料不是每次都会被读取,只有当SKILL.md正文指示"如果需要更详细的信息,请参考references/xx.md"时,模型才会去读取。这种分级加载设计进一步节省了上下文空间。

assets目录放静态资源,比如模板文件、样本数据、配置文件。我在实际使用中很少用到这个目录,但在需要模型生成特定格式文件的场景下很实用。

2.2 SKILL.md的frontmatter与正文如何协作

frontmatter是YAML格式,长这样:

--- name: image-compress description: 当用户需要批量压缩图片、调整图片尺寸或转换图片格式时使用此技能。支持常见格式(JPEG、PNG、WebP)的互转。 ---

这里最重要的是description的写作质量。我踩过一个大坑:第一次写Skills时,我把description写得太宽泛——"处理图片相关任务"。结果模型在用户只是想了解图片格式区别时也触发了这个技能,白白浪费了上下文加载。后来我改成了上面那种带具体操作场景的写法,触发准确率明显提升。

写description有一条经验:描述"用户会怎么说"而不是"技能能做什么"。也就是说,要站在模型接收到用户请求时的视角来写,让模型能通过语义匹配判断"这个请求和那个技能的描述是否对应"。

正文部分则是给模型的具体操作指引。这里我吃过不少亏,后面第3章会详细展开。简单说,正文要写成标准操作程序(SOP)的形式,而不是开放式的建议。

2.3 scripts、references、assets的边界职责

刚开始写Skills的人容易犯一个错:把什么逻辑都往SKILL.md里塞,让模型"自己看着办"。这是不对的。

正确的做法是:凡是能用代码确定性完成的事情,就写成脚本;凡是需要模型判断和生成的事情,才写在SKILL.md正文里。

举个例子。我做图片压缩技能时,缩放算法、质量参数这些确定性逻辑全部放在Python脚本里,脚本接收输入输出路径,返回结果。而SKILL.md正文只负责告诉模型:什么时候运行脚本、脚本参数怎么填、脚本输出结果如何呈现给用户。

这样分工的理由很简单:脚本的结果是确定的、可复现的,而模型每一步操作都有概率性。能用代码兜底的不要用模型自由发挥,这是Skill设计的第一原则。


3. 手写图片压缩Skill:从设计到跑通的全过程

3.1 为什么选图片压缩作为第一个Skill

如果你也想上手写Skill,我强烈建议从图片压缩练手。原因有三:

  • 图片处理逻辑清晰,不涉及复杂的状态管理,适合用来理解"脚本+指令"的分工模式。
  • 模型本身无法直接操作图片文件(至少不擅长),必须依赖脚本,这样能逼着你把"模型该干什么、脚本该干什么"想清楚。
  • 结果可视化,压缩前后的文件大小对比一目了然,方便验证技能是否真正生效。

3.2 完整实现:SKILL.md怎么写给模型看

先看我的SKILL.md正文核心部分:

# 图片压缩与格式转换技能 当用户提供图片路径或包含图片的目录路径时,按以下步骤操作: 1. 确认输入路径是否存在,以及图片格式是否受支持(JPEG、PNG、WebP)。 2. 运行 `python3 scripts/compress.py`,参数如下: - `--input`:输入图片或目录路径(必填) - `--output`:输出目录(必填) - `--quality`:JPEG/WebP压缩质量,默认85,取值范围1-100 - `--max-width`:可选,限制图片最大宽度,超过则等比缩放 3. 脚本执行完毕后,读取脚本输出的JSON结果。 4. 将结果整理成易读的文字反馈给用户,说明压缩前后的大小、节省比例、输出位置。

注意这里的写法:每一步都是明确的指令,模型不需要猜测"下一步干什么"。特别是第4步,我明确要求模型读取JSON结构化的输出结果,而不是让模型自己"看着脚本输出随便发挥"。

为什么JSON输出这么重要?模型读文本的能力虽然强,但面对格式混乱的日志输出,仍然可能解析错误。而JSON结构清晰、层级固定,模型只需要按字段取值,出错概率大幅降低。这是一条我在实际开发中总结出来的关键经验——Skill配套脚本的输出,应当优先考虑机器可解析性,而不是人可读性。

3.3 配套脚本:结构化输出比"能跑"更重要

脚本我用了Python + Pillow库,核心逻辑并不复杂,但输出格式花了不少心思:

#!/usr/bin/env python3 """批量图片压缩脚本,输出JSON结果。""" import argparse import json import os import sys from pathlib import Path from PIL import Image def compress_image(input_path: Path, output_path: Path, quality: int, max_width: int = 0): """压缩单张图片,返回压缩结果信息。""" original_size = input_path.stat().st_size original_format = input_path.suffix.lower() with Image.open(input_path) as img: if max_width and img.width > max_width: ratio = max_width / img.width new_size = (max_width, int(img.height * ratio)) img = img.resize(new_size, Image.LANCZOS) output_format = "JPEG" if output_path.suffix.lower() in (".jpg", ".jpeg") else ( "WEBP" if output_path.suffix.lower() == ".webp" else "PNG" ) if output_format == "JPEG" and img.mode in ("RGBA", "P", "LA"): img = img.convert("RGB") img.save(output_path, format=output_format, quality=quality, optimize=True) compressed_size = output_path.stat().st_size return { "file": input_path.name, "original_size_kb": round(original_size / 1024, 2), "compressed_size_kb": round(compressed_size / 1024, 2), "saved_percent": round((1 - compressed_size / original_size) * 100, 1) if original_size else 0, } def main(): parser = argparse.ArgumentParser(description="批量图片压缩脚本") parser.add_argument("--input", required=True, help="输入图片或目录路径") parser.add_argument("--output", required=True, help="输出目录路径") parser.add_argument("--quality", type=int, default=85, help="压缩质量,1-100") parser.add_argument("--max-width", type=int, default=0, help="最大宽度限制") args = parser.parse_args() input_path = Path(args.input) output_path = Path(args.output) if not input_path.exists(): print(json.dumps({"error": f"输入路径不存在: {input_path}"}, ensure_ascii=False)) sys.exit(1) output_path.mkdir(parents=True, exist_ok=True) if input_path.is_file(): images = [input_path] else: images = list(input_path.rglob("*.jpg")) + list(input_path.rglob("*.jpeg")) + \ list(input_path.rglob("*.png")) + list(input_path.rglob("*.webp")) if not images: print(json.dumps({"error": "未找到受支持的图片文件"}, ensure_ascii=False)) sys.exit(1) results = [] for img_path in images: try: out_file = output_path / f"{img_path.stem}_compressed{img_path.suffix.lower()}" result = compress_image(img_path, out_file, args.quality, args.max_width) results.append(result) except Exception as exc: results.append({"file": img_path.name, "error": str(exc)}) summary = { "total": len(results), "success": sum(1 for r in results if "error" not in r), "failed": sum(1 for r in results if "error" in r), "results": results, } print(json.dumps(summary, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()

脚本里有一个我特别想强调的细节:对RGBA、P模式的图片,在输出JPEG格式时先转成RGB。这是我执行第一次实测时发现的问题——Pillow直接保存RGBA图为JPEG会报错,这个错误信息只有脚本运行时才会冒出来。做Skill时,脚本的稳健性直接影响模型的执行成功率,因为模型遇到脚本报错时虽然可以尝试修复,但每次修复都意味着额外的上下文消耗和潜在的思维发散。

3.4 实测验证与调试过程

把文件按目录结构放好后,我在Claude Code里做了一次完整测试。测试场景是:把一个包含二十多张PNG截图的目录压缩成WebP格式,质量设80,最大宽度限制到1280。

整个调用过程分三个阶段:

第一阶段,模型读取SKILL.md的frontmatter,判断当前用户的请求"把图片压一下"和这个技能的description匹配,于是加载了SKILL.md正文。

第二阶段,模型根据正文指示,构造出运行命令:

python3 scripts/compress.py --input screenshots/ --output screenshots_webp/ --quality 80 --max-width 1280

第三阶段,脚本执行完成后输出了一大段JSON,模型读取结果,把关键数据——压缩前后大小、节省比例、输出目录——整理成用户友好的文字反馈。

这次测试中暴露的第一个问题是:模型执行前会先检查输入路径是否存在,这本身是好事,但它花了额外的步骤"验证环境"。后来我在SKILL.md正文里加了一句"除非用户明确要求,否则不要在执行前进行额外的路径探查,直接运行脚本,由脚本自行校验",省掉了不少无用操作。

第二个问题是输出格式的稳定。在我最早的版本里,脚本输出的是纯文本日志,结果有一次模型把日志里的中间数据当成了最终结果,反馈给用户时数字明显不对。改成JSON输出后,这类问题再也没有出现过。


4. 真实项目里的效率对比与选型经验

4.1 同样任务:纯提示词 vs Skill的差别

我做了一个对照实验,来量化"用Skill"和"用一段长提示词"在同样任务上的差异。任务很简单:每次对话时,用户上传一个日志文件,模型需要按固定规则提取关键信息、生成结构化摘要,并输出为指定的JSON格式。

对照组用的是传统做法——在系统提示词里塞入完整的分析规则、输出模板、字段说明,大约五百行。实验组建了一个包含该逻辑的Skill包,SKILL.md正文里写处理流程,references里放字段说明文档,脚本负责解析日志。

实测结果非常明显:

对比维度纯提示词方案Skill方案
每轮任务消耗token约4200约1800
输出格式稳定性偶尔缺字段稳定
规则更新维护成本改动一次,全量影响只改对应文件
新增任务扩展方式追加到原提示词新建独立Skill

token消耗差距这么大的原因在于,纯提示词方案每轮对话都要把五百行规则全部过一遍,而Skill方案只在任务被触发时加载一次流程指引,字段说明文档这种"细节型知识"更是按需读取。实测下来,运行效率大概提升了一倍多。

4.2 什么时候该用MCP,什么时候该用Skill

这个选型问题,我在多个项目里反复琢磨过,目前的判断依据是"知识的稳定性"和"动作的交互性"两个维度:

  • 动作需要实时查询外部状态——比如查询数据库、调用未公开的API、读写远端文件——用MCP。因为这些场景需要真实的网络IO和权限管理,Skill脚本虽然也能做到,但MCP在连接管理、鉴权、生命周期上更成熟。
  • 任务是确定性的内部流程——比如按模板生成报告、对本地文件做批量处理、对文本做标准化清洗——用Skill。因为这些逻辑稳定不变,把它沉淀为技能包后成本极低。
  • 需要两者结合的场景也存在。比如我的一个数据处理项目里,模型先通过MCP读取数据库,再加载一个"数据清洗Skill"来按既定规则清洗,然后再通过MCP写回。MCP负责"手",Skill负责"脑"。

我个人对Skill和MCP的使用原则是:能够用本地确定性脚本解决的问题,优先用Skill;涉及外部系统实时交互的,才引入MCP。

4.3 实际踩过的坑:触发词过宽、上下文浪费、指令冲突

踩坑一:description写太宽导致误触发。一次我写了一个"代码审查Skill",description写的是"用于代码质量检查"。结果用户在讨论架构设计时,模型也触发了这个技能,白白读了一遍SKILL.md。后来我把description改成了"当用户要求对已有代码进行审查、找出bug或安全隐患时使用",并且加了"不要在日常编码讨论中主动使用"的排除说明,误触发问题基本消失。

踩坑二:技能内部指令与主指令冲突。项目里有一套全局的代码风格规范,但某个Skill内部也写了一套相反的命名规范,结果模型执行时犹豫不决,导致输出不一致。解决方式是Skill里的SKILL.md开头明确加了一行"本技能遵循项目全局指令中的编码规范约定,仅对xx流程做补充说明"。

踩坑三:Skill被加载进上下文,但它所引用的reference文件路径写错,模型读了半天读不出内容,最后只能硬着头皮"凭经验"完成任务。这个教训让我养成了一个习惯:Skill上线前,必须用包含路径引用操作的完整场景测试一遍。


5. 把Skills变成团队资产:编排、复用与维护

5.1 私有技能库的组织方式

Skills实践多了之后,我意识到了一个更深的价值:它是一个团队级别的知识沉淀单元。以前团队里的经验文档,比如"售后日志分析SOP""周报自动生成规范",都存在wiki里,人需要主动去查。而Skill包的好处是,模型在对应场景出现时,会主动加载这个SOP并执行。

团队协作时,我建议用一个独立的仓库来管理技能包,按场景分目录:

team-skills/ ├── analysis/ │ ├── log-triage/ │ ├── root-cause/ │ └── trend-report/ ├── content/ │ ├── weekly-digest/ │ └── meeting-minutes/ └── images/ └── compress-convert/

每个技能一个目录,独立版本管理。团队成员提交新技能时走merge request流程,至少另一个人review。这种做法把"写提示词"变成了"写代码",质量可控性完全不同。

5.2 多技能协作的编排思路

单个技能解决单点问题,但真实项目往往需要多个技能配合。我的经验是:不要试图在一个Skill里塞进所有步骤,而是拆成多个单一职责的Skill,通过SKILL.md里的交叉引用实现编排。

举个例子。我的网站更新流程分三个Skill:内容清洗Skill、图片压缩Skill、发布检查Skill。当用户说"把这篇文章发到网站上",模型会依次加载三个Skill,每个Skill只负责一个环节,环节之间通过脚本文本传递中间产物,不需要在上下文里互相直接调用。这样每个Skill可以独立测试、独立改进,互不牵连。

我在组织脚本时,也会特意让脚本之间通过约定好的中间目录传递数据,这和微服务架构中"通过接口通信"的思路是一致的。

5.3 维护节奏:什么样的Skill值得长期保留

最后谈谈维护。我目前的经验是定期review两个指标:

  • 触发频率:如果一个Skill上线两个月都没被触发,可能不是用户没用到,而是它的description写得不够贴合实际请求方式。这时我会重写description,而不是直接删除。如果重写后仍然长期不触发,说明这个能力本身就不该独立成一个Skill,删除或合并掉。
  • 输出稳定性:如果一个Skill经常输出不满意的结果,优先检查SKILL.md正文是否给了足够明确的步骤,而不是急着加脚本。很多时候模型出问题是因为指令太模糊,给了它自由发挥的空间。

写完这些,回过头来看,Skills最大的价值不在于"省token"或者"提升准确性"这些技术指标,而在于它改变了我们组织AI协作的方式——从"每一次都要重新交代上下文"变成了"把方法论固化成可版本管理、可分享、可演化的文件"。

我个人的建议是:如果你已经在用AI模型处理重复性任务,第1~2个Skill可以从最常见的日常工作流开始做,比如日志分析、报告生成、批量文件处理。做完一个完整的流程,再回头看其他任务,你会清楚地看到"哪些工作应该交给模型判断、哪些应该沉淀为确定性脚本"。这比任何教程都更能帮助你真正理解这套机制的用意。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询