上周有个同事拿着他刚调好的"AI 周报机器人"来找我,说模型动不动就把三条记录合并成一句废话,问我是不是 prompt 写得太短。我打开他所谓的 skill 看了一眼:没有目录结构,触发描述一团浆糊,所有逻辑全堆在提示词里。那一瞬间我想起自己刚入坑时的样子,也就有了这篇东西。
这篇文章聊的是 skills,AI 助手生态里越来越常见的"能力打包"玩法。它不是简历上的技能清单,而是把"让 AI 干一件具体的事"所需的一切——指令、脚本、边界、触发条件——封装成一个可复用、可分发、可版本管理的单元。很多人的第一反应是"这不就是更长的 prompt 吗",实际写完几个之后你会发现,prompt 只占三成功力,剩下七成藏在工程细节里。下面这些内容适合两类人:把 AI 当主力生产力工具的重度用户,以及想在公司里推广 AI 工具、又怕大家各自为战的工程师。
1. "Skills"的实质:岗位说明书 + 工具箱 + 一圈边界
先解决认知。很多人觉得 skills 是新概念,其实它的思路老得不能再老。想象你招了个实习生,只扔给他一句"好好干",他大概率把事情办得稀碎。真正有效的是给一套入职包:岗位说明书(负责什么、做到什么程度)、工具箱(常用系统、数据源、权限)、边界(哪些事必须问、哪些事绝不能碰)。skills 就是这套逻辑在 AI 助手上的落地。
1.1 模型看到的并不是整个技能包
这是我最早踩的坑。我原本以为,加载 skill 之后模型会把 SKILL.md 从头到尾通读一遍。实际上在主流实现里,模型先看到的是这个技能的"门面"——SKILL.md 开头 frontmatter 里的name和description。模型根据这段描述判断当前对话要不要激活技能,激活之后才会读取正文、调用配套脚本。
这个机制极其重要。description 写得好不好,直接决定技能是"召之即来"还是"抬都抬不动"。我后来把 description 当成触发开关来写,不当成简介来写,效果差距非常大。
1.2 Skills 和 Prompt、RAG、Plugin 到底是什么关系
我一度把这些概念叠在一起,困惑了很久,后来用一句话理顺:它们不在同一层,不打架。
| 概念 | 主要负责 | 一句话类比 |
|---|---|---|
| Prompt | 告诉模型"怎么回答" | 上岗培训手册 |
| RAG | 让模型"有资料可查" | 资料库门禁 |
| Plugin / 工具调用 | 让模型"能实际动手" | 工具箱 |
| Skills | 决定"什么时候动手、按什么流程动手" | 岗位说明书 + 值班排班 |
所以 skills 不是 prompt 的替代品,而是更高一层的编排。它可以引用 prompt,可以通过插件的动作调用外部服务,也可以结合 MCP 这类协议去访问数据源。它的核心价值在于把"会干一件事的人"整建制地打包,而不是每次对话都从零拼凑。
1.3 一个标准技能包的骨架
各家实现虽然叫法不同,目录结构却高度相似。我自己在用的格式长这样:
my-skills/ └── weekly-report/ ├── SKILL.md ├── scripts/ │ └── clean_up.py └── assets/ └── template.mdSKILL.md:核心说明书,frontmatter 写元信息,正文写操作流程。scripts/:可选,放模型要调用的本地脚本。assets/:可选,放模板、示例等被动资源。
很多技能一个SKILL.md就够用了。一旦涉及"读取一堆脏数据再整理"这类活,我强烈建议把确定性高的部分丢进scripts/,让模型只做判断和编排。理由后面讲。
2. 从零写一个能用的 Round Report Skill:周报整理助手实战
理论说多了飘,直接上手。我的第一个练手项目是"周报整理助手",任务是把每天随手记的琐碎记录变成一份能交差的周报。选它是因为足够轻,但已经覆盖目录、说明书、脚本三要素。
2.1 先把目录搭出来
我习惯在本地建一个专门的文件夹装所有技能,用 Git 管理。这一步只需要两行:
mkdir -p my-skills/weekly-report/scripts cd my-skills/weekly-report别小看目录命名。我一开始用过report、weekly这种模糊词,结果技能多了根本分不清哪个是哪个。现在统一用"动词+对象":weekly-report、review-code、summarize-docs,一看就知道是干什么的。
2.2 写好 SKILL.md 这份"岗位说明书"
这个文件决定模型能不能接住活。下面是我后来稳定在用的版本,加了不少注释:
--- name: weekly-report description: 把分散的每日工作记录整理成结构化周报。当用户提到"周报""工作总结""整理这周干了什么"或直接粘贴多条工作记录时使用。 --- # 周报整理助手 ## 目标 把用户提供的零散记录合并为一份结构清晰、可提交的 Markdown 周报。 ## 输入处理 - 用户可能粘贴多行文本、列表或文件内容。 - 优先使用用户主动提供的内容,不自行脑补数据。 - 如果缺少时间范围,先向用户确认起止日期。 ## 整理步骤 1. 把每条记录标记为:"任务""进展""阻塞""计划"四类。 2. 按"本周任务 -> 关键进展 -> 阻塞与风险 -> 下周计划"组织内容。 3. 合并重复描述,保留量化结果(数字、百分比、交付物)。 4. 同一件事出现多次时,按最近一次的状态更新。 ## 输出要求 - 必须输出 Markdown,开头带"时间范围:xx - xx"。 - 没有内容的分类写"暂无",不要强行编造。 - 不要在周报里出现"根据你的记录,我帮你整理"这类废话。写这份说明时我也踩过典型坑:一开始我把公司模板、往期范例、领导偏好全塞进去,结果模型反而选择困难,不知道该听哪条。后来删到只剩"目标、步骤、输出要求",正确率立刻上来了。给模型写说明书,重点是约束边界,不是堆砌信息。
2.3 配套脚本:只做能确定的事
周报里大量记录是"今天修了登录接口的 bug""本周完成了权限模块"这种半结构化文本。全让模型逐条读,量一上来就容易漏。我配了个小脚本做清洗:
#!/usr/bin/env python3 import sys, re text = sys.stdin.read() lines = [l.strip() for l in text.strip().splitlines() if l.strip()] categorized = {"task": [], "progress": [], "blocker": [], "plan": []} for line in lines: if re.search(r"(阻塞|风险|问题|卡住)", line): categorized["blocker"].append(line) elif re.search(r"(下周|计划|准备|规划)", line): categorized["plan"].append(line) elif re.search(r"(完成|上线|修复|接入|实现)", line): categorized["progress"].append(line) else: categorized["task"].append(line) print("=====RESULT_START=====") for key, items in categorized.items(): print(f"## {key}") for item in items: print(f"- {item}") print("=====RESULT_END=====")注意这个脚本只做"分类和归一化",不做"判断价值"。哪些事情重要、该怎么表述,仍然留给模型。这就是下一节要展开的边界设计。
2.4 把它装进你的 AI 助手
安装方式各家实现不一样:有的有"添加 skills"入口,有的要放进指定目录,有的直接拖进对话读取。我的经验是:先把文件夹路径给模型看,让它读完SKILL.md并跑一遍测试输入,确认可用之后再放进正式目录。不要一上来就装进生产环境,半成品技能很容易污染后续判断。
3. 决定 Skill 上限的不是描述,是脚本和调度的边界
很多人在写 skills 时用力过猛,把脚本写成"万能执行器",什么都往里塞。结果模型调用它跑完,输出反而没法用。关键是分清职责:模型是调度员,脚本是手。
3.1 模型该干的事,别让脚本抢
脚本适合做确定性的、可验证的事情:读文件、清洗数据、调 API、算数字、格式转换。模型适合做不确定性高的事情:理解意图、判断优先级、润色表达、决定输出结构。
写过一个反面典型。某技能里我用正则判断"这句话是否重要",判断标准写死了,结果遇到"周四跟供应商对开会对齐交付计划"这种表述,正则匹配不上,直接被归为不重要。后来改成脚本只做分片,每行带编号原样传回,由模型判断重要性,效果立刻稳了。规则越写越复杂的脚本,本质上是在逼自己实现一个 AI,最后必然崩。
3.2 脚本设计三原则
这三个原则我改到第三轮才提炼出来,直接决定技能可不可用:
- 短:单次执行时间要短,不要把所有处理塞进一个脚本跑批。
- 纯:同样的输入得到同样的输出,不和系统状态绑定。
- 结构化输出:结果用 JSON 或带明确标记的文本返回,别在 stdout 里混日志。
如果脚本要访问外部服务,比如数据库、接口,我的做法是把它封装成独立动作或 MCP 工具暴露给模型,skills 负责编排而不是自己硬写网络请求。这样权限边界清晰,出问题也容易查。
3.3 给脚本加"失败面"
技能翻车最隐蔽的地方,是脚本静默失败。比如脚本读了一个不存在的文件,返回空内容;调外部接口超时,没有任何提示。模型拿到空结果,往往会编一段"看起来合理"的答案,直接污染输出。
所以我在每个技能里都加一条铁律:脚本不给结果时,必须明确告诉模型"执行失败,请向用户确认输入是否完整"。宁愿停下来问用户,也不要让模型脑补。这条规矩救过我太多次。
4. 技能一多就翻车?五个高发故障的完整排查链路
单个技能写得再顺,也不代表整套体系稳。等技能超过三个,问题就会集中爆发。我把实际踩到的坑按频率排了个序,每个都讲排查思路,不是直接给答案。
4.1 该触发时不触发,不该触发时乱触发
大概率出在description上。写得太宽泛,模型分不清什么时候用;写得太窄,遇到语义变体就认不出来。
我自己的修法是给 description 写"一句触发场景 + 若干同义触发词":
description: 把零散工作记录整理成周报。当用户提到"周报""工作总结""本周干了啥"或要求汇总多条日常记录时使用。排查链路:先拿十句话,其中一半应该触发、一半不该触发,问模型要不要调用这个技能,然后看它的判断和你想的是否一致。不一致就改 description,别改正文。这步是定位触发问题最省力的方法。
4.2 指令太长,模型越读越糊涂
SKILL.md 不是文档,是速查卡。我写过一份三千字的技能说明,模型执行时明显"选择困难"。后来测出相对舒服的区间:核心指令控制在 200-600 字,细节放assets/或references/,等模型需要时再去查。
这个调整立竿见影。说明书短了,模型对流程的记忆稳了,输出也守规矩了。记住一个原则:正文里只留"必须做的",凡是"可能用到的"全部外置。
4.3 脚本输出混乱,模型不知道该信哪行
常见场景:脚本 print 了一堆调试日志,最后才放结果。模型分不清哪些是日志哪些是结果,直接拿日志编答案。修法就一条:stdout 只放最终结果,日志全部打 stderr。或者像我上面例子那样用明确标记包裹结果段落。
4.4 路径和权限问题导致"它说不存在"
技能里写死绝对路径是最大的隐患。换台机器、换个账号,路径立刻崩。我的规矩是:技能里永远不写死绝对路径,一律要求模型先询问用户"文件在哪里",再基于用户提供的路径执行。
另外注意平台权限。很多技能以为能读某个目录,实际上根本没被授权。排查时先看清报错是"文件不存在"还是"没有权限",这两个方向完全不同。
4.5 改了 SKILL.md 却不生效
这坑非常隐蔽。我遇到过一次,改完 description 怎么测都不触发,最后发现是文件名大小写不对(skill.md而不是SKILL.md),平台压根没把它当技能。另一次是旧会话还挂着老版本,我以为在改同一份,其实加载的是缓存。
现在的习惯是:每次改动都在 frontmatter 里升版本号(version: 1.2.0),然后新开会话验证,并且只改一个变量。版本号不只是给自己看的,也是给模型判断"用哪份说明"用的。
4.6 把排查变成一张清单
我在团队里贴了一张速查表,照着走基本能定位:
| 现象 | 优先检查 | 常用解法 |
|---|---|---|
| 不触发 / 乱触发 | description 宽泛、语义变体多 | 收窄触发场景、加同义词 |
| 执行走样 | SKILL.md 太长、指令冲突 | 压缩正文、移除互相矛盾的规则 |
| 结果读不懂 | 脚本输出脏、日志混入 | 脚本返回结构化 JSON |
| 路径不存在 | 写死绝对路径、权限不足 | 改为动态询问路径、检查授权 |
| 改了没生效 | 文件命名、缓存、目录冲突 | 改版本号、新会话验证 |
5. 从单个技能到技能库:把它当成代码来维护
单兵技能是玩具,技能库才是生产力。等你有五六个技能,就要面对版本、协作、一致性的问题。我的做法很简单:把这些技能当代码仓库管理。
5.1 仓库结构先定清楚
skills-repo/ ├── weekly-report/ │ ├── SKILL.md │ └── scripts/ ├── meeting-notes/ │ └── SKILL.md ├── code-review/ │ ├── SKILL.md │ └── scripts/ ├── skills-index.md └── tests/ ├── weekly-report.test.md └── code-review.test.mdskills-index.md是总目录,每个技能一行,写清楚"做什么、入口在哪"。团队新成员或者新的 AI 会话接项目时,先读索引,比自己翻目录高效得多。
5.2 给技能上"金丝雀测试"
跟写代码一样,技能改了不能盲目上线。我给每个技能配 2-3 个固定测试用例,放在tests/下,改完 SKILL.md 或脚本就带着旧用例跑一遍。用例要覆盖正常输入、边界输入、异常输入三种情况。
比如周报技能,我固定的三个用例是:
- 正常:给 10 条不同日期的记录,输出应合并同类项。
- 边界:只有一条记录,输出不应出现空分类。
- 异常:输入全是口号式内容、没有实际细节,应提示用户补充而不是编造。
这一步很朴素,但我至少有三次"改崩了"是靠它挡下来的。
5.3 权限和密钥永远不进技能库
脚本如果需要用密钥,一定通过环境变量或平台的安全配置注入,不能写进SKILL.md或scripts/里。技能库会被复制、被分享、被提交到仓库,密钥一旦进去就是泄露,没有例外。
权限配置也遵循最小化原则:技能能读不能写、能写不能删,按实际需要给,别图省事。有一次我给一个技能开了"可执行任意命令"的权限,当天晚上就在日志里看到它把临时目录扫了个遍——瞬间出了一身冷汗。
6. 说几句体己话
我把第一个技能改到第三版才真正稳定,中间交了不少学费。现在回看,最重要的心得不是某个目录结构,也不是某个写法,而是一句话:技能的复杂度应该往脚本里转移,而不是往说明文字里转移。
说明文字越长,模型的执行方差越大;脚本越确定,技能的稳定性越好。凡是能被规则描述清楚的事,尽量丢给脚本;凡是需要理解上下文才能做的判断,才留给模型。这套分工想明白了,skills 就不再是玄学。
最后分享一个小技巧:给每个技能的目录里放一个testcase.md,写三条"我期望它怎么做"的用例。每次改动后先拿这三条用例喂一遍,再决定要不要正式启用。这个习惯把技能维护成本压到了最低,也让我敢在团队里放心地推着十几个技能跑日常任务。希望这份踩坑笔记能帮你少走几圈弯路,早点把散落在各处的能力变成真正能干活的东西。