你看到“skills”这个词,第一反应可能是简历上那一栏“专业技能:精通Office、熟悉Python”。但在过去半年里,这个词在AI圈已经被玩出了完全不同的味道——把一段本来要写进系统提示词的长篇规则,打包成一个可以被按需加载的独立“技能包”。我前阵子正好做了一套这样的技能包,不是玩具demo,是真的在每天命令行里跑的东西,今天就拿其中一个最容易上手的例子——“git提交信息助手”技能包——来把这件事彻底讲透。
这套技能包解决什么问题呢?就是我们日常写commit message时总是一团糟:“update files”“fix bug”“改了一下”。Conventional Commits规范大家都知道,但人总有偷懒的时候,AI在没有明确规则约束时也会偷懒。把规则固化成一个技能包之后,AI会先跑脚本解析diff,再套模板生成符合规范的提交信息,整个过程确定又可控。适合谁看?想系统学习Agent Skills怎么写、怎么调试、怎么避坑的人,以及想在团队里推广“AI辅助开发规范”但不知道从哪下手的技术负责人。
1. 先搞清楚:Skills到底是什么,为什么大家都在聊它
1.1 一句话解释Agent Skills
Agent Skills,更通用的说法是“技能包”,本质上就是一个普通目录。目录里面放一个带有特殊frontmatter结构的Markdown说明文件(主文件名一般是SKILL.md),再加上若干脚本、模板、参考文档。当你向AI提的问题命中了某个技能包的描述字段时,AI才会把这份说明加载进来,然后按照里面定义的步骤一步步执行;没命中的情况就完全不占任何上下文空间。
这个机制特别好用,我打个比方你就明白了。想象你刚到一家新公司,人事给你一本员工手册,但要求你第一天就把整本手册背下来,以后每次开会、每个对话都要保持整本手册的内容在线——这是系统提示词的做法。技能包的做法是:你接电话时提到“报销”,前台才把报销流程那一页递给你;你提到“出差”,前台再给你出差制度那一页。省token是一方面,更重要的是,它把“行为规范”从提示词工程里捞了出来,变成了一个独立的、可交付、可版本管理的产物。
1.2 和系统提示词、普通prompt的核心区别
我在踩坑过程中梳理过一张对比表,基本能概括这几类写法的差异:
| 对比维度 | 系统提示词 | 普通prompt | Agent Skills(技能包) |
|---|---|---|---|
| 加载时机 | 每轮请求都全量加载 | 用户每次手动粘贴 | 按描述匹配,命中才加载 |
| 上下文占用 | 高,常驻token | 单次高,用完即弃 | 低,命中时才进入上下文 |
| 可维护性 | 改一行提交一次,全量变更 | 散落在各处,无法统一管理 | 独立目录,独立版本,可review |
| 可测试性 | 难以自动化测试 | 难以断言 | 可对触发条件、步骤、输出做单测 |
| 能跑计算/命令 | 不行 | 不行 | 可以,脚本是技能包一部分 |
| 分享和复用 | 复制粘贴 | 复制粘贴 | 打包目录即可分享 |
这里有一个容易被忽略的关键点:技能包不是“提示词的另一种写法”,它把LLM的能力和外部脚本的能力拧成了一股绳。纯粹的提示词,无论写得多精美,都没法自己执行git diff解析文件变更;而脚本可以。你让AI做的,只是基于脚本的确定性输出去做决策和生成。想清楚这一层,你就明白了为什么社区最近都在聊“skills”——因为它把AI从“对话工具”往前推了一步,变成了一个“有操作接口的执行器”。
1.3 到底什么样的任务,才配得上做一个技能包
不是所有任务都值得做成技能包的。我见过有人把“写情书”做成了技能包,结果每封情书都一个模板味——这种开放创作类任务,技能包只会帮倒忙。我自己的判定标准有这么几条,至少命中两三条才值得动手:
- 规则性强、可重复执行。比如commit message、日志格式化、接口文档模板、周报生成。输入内容每次都在变,但处理流程完全固定。
- 需要确定性输出。代码评审意见、提交信息、错误分类这类场景,AI自由发挥的代价很高,宁可让规则说了算。
- 涉及外部工具或文件解析。需要执行命令、读文件、算统计数据,这是脚本的地盘。
- 团队需要统一标准。当你要让十几个人产出同一风格的产物时,把标准固化成技能包比发通知有效得多。
记住这个筛选逻辑,你后面做技能包就不会什么都想塞进去,也不会做完发现根本没用。
2. 动手前先设计:一个技能包的完整骨架长什么样
2.1 选定场景,把痛点拆到可以落地
我这次选择的场景是“git提交信息规范化”。写代码的人都知道,好的commit message能让你三个月后回看历史时不用打开代码就知道当时干嘛了;但现实是,团队里大多数人提交时只会写“fix bug”,运气好一点的写“修复了登录bug”,没人知道修复方式是什么、影响范围在哪。
我最初试图靠一段超长的系统提示词解决这个问题,给模型列出所有type、scope、正文格式,效果还是不行。第一是提示词太长,每轮都要消耗token;第二是模型经常把“读取diff”这一步跳过,直接根据对话上下文猜;第三是不同仓库有不同规范,我得维护N个版本的提示词。后来我把这套逻辑重构为技能包,让python脚本去解析diff,模型只做“看脚本输出、套模板、生成文案”这件事,问题才算真正解决。
2.2 目录结构:每个文件存在的意义都得说得清
我的commit-helper技能包最终是这么组织的:
skills/ commit-helper/ SKILL.md scripts/ analyze_diff.py templates/ commit_subject.txt commit_body.txt references/ conventional_commits.mdSKILL.md是入口,AI加载技能包时主要读这个文件;scripts/analyze_diff.py负责解析git diff,输出建议的提交类型和影响范围,这是确定性逻辑的承载者;templates/两个文件定义了subject和body的标准结构;references/conventional_commits.md是参考手册,里面放完整的type列表和常见scope,模型只有在不确定的时候才会去查。
我强烈建议目录命名全部用英文小写加连字符(kebab-case)。这有两层原因:第一是兼容性,有些工具链对中文路径、空格支持不好;第二是技能包往往会进git仓库、走CI、被其它框架扫索引,命名越标准,被误解析的概率越低。
2.3 SKILL.md的元数据:description决定“什么时候被触发”
frontmatter是整个技能包的门面。我先写下第一版:
--- name: commit-helper description: 当用户需要生成git提交信息、检查commit message规范性、或要求按Conventional Commits规范提交时使用。包含git diff解析与提交类型判定。 version: 0.2.0 ---写description的坑我踩过不止一次。第一版我写的是“这是一个git提交信息生成工具”,结果模型经常在用户只是问“git怎么提交”的时候就把技能加载进来,属于误触发;第二版我写了“生成提交信息”,结果用户说“帮我把改动提交一下”,模型又迟迟不触发,属于漏触发。
正确的写法是:描述“在什么情况下使用”,而不是“这个工具是什么”。多用行为动词,比如“生成”“检查”“规范化”。触发词也不要只堆名词,最好把常见的用户意图写进去——“提交一下”“看看commit合规吗”“帮我写git提交信息”,这些口语化表达远比一个规范名字更容易命中。
3. 从零实现:commit-helper技能包的完整落地过程
3.1 把SKILL.md正文写成AI可执行的清单,而不是百科词条
写完frontmatter,接下来是最关键的正文部分。我第一版犯的错误是写得太像一段教程:“分析用户的git改动,生成合适的提交信息”。模型读完等于没读,该自由发挥还是自由发挥。后来我改成了一套带强制顺序的操作流程,效果立刻不一样:
# Git提交信息助手 ## 输入 接受以下任一触发信号:用户要求生成提交信息、要求规范commit message、或要求提交代码。 ## 执行步骤 1. 运行 `git status --short` 获取工作区与暂存区的变更文件列表。 2. 运行 `git diff --cached --stat` 获取暂存区改动的统计信息。 3. 调用 `python3 scripts/analyze_diff.py`,获取建议的提交类型、范围和置信度。 4. 依据脚本输出的类型建议,结合下面的“类型对照表”确定最终type: - feat:新功能、新特性 - fix:缺陷修复 - docs:文档变更 - refactor:重构,不改变外部行为 - style:格式调整,无逻辑变更 - test:补充或修改测试 - chore:构建、依赖、杂项 5. 生成subject,要求: - 总长度不超过50个字符 - 使用祈使句式,如“add xxx”而不是“added xxx” - 首字母大写,句末不加句号 - 必须填入scope(模块名,取diff中变更最集中的目录名) 6. 如果存在多个不相关变更,按类型拆分成多条提交,并逐条让用户确认。 7. 在最终回复中,必须展示“检查清单”: - [x] 已运行三个git命令 - [x] 已读取脚本输出 - [x] 已按规则生成subject我把这个清单贴出来是想让你看清楚一个核心原则:每个步骤都以动词开头,明确输入、动作、输出。不要写“分析变更”这种虚词,要写“运行什么命令”“读取什么输出”“根据什么规则生成什么”。另外,第7步是我调试时加进去的,它让模型在输出时自带“已执行”的痕迹,我能一眼看出这个技能包到底有没有被严格遵循。
3.2 脚本把脏活累活干完:analyze_diff.py的设计思路
这个脚本负责从git diff里挖掘出结构化结论。它的输入是git diff --cached --name-status的内容,输出是一段关键信息:建议类型、范围、变更文件统计。
核心逻辑是基于文件名和路径的启发式判断。规则不复杂,但稳定且高效:
#!/usr/bin/env python3 """Analyze git diff and suggest conventional commit type/scope.""" import subprocess import sys from collections import Counter TYPE_KEYWORDS = { "feat": ["add", "new", "feature", "implement", "create"], "fix": ["fix", "bug", "error", "crash", "wrong", "hotfix"], "docs": ["readme", "doc", "comment", "guide", "md"], "refactor": ["refactor", "rename", "move", "clean"], "style": ["format", "whitespace", "indent", "lint"], "test": ["test", "spec", "fixture", "jest", "pytest"], } def get_staged_files(): out = subprocess.check_output( ["git", "diff", "--cached", "--name-status"], text=True, errors="replace", ) return [line for line in out.strip().splitlines() if line] def suggest_type(files): score = Counter() for line in files: status = line[0] path = line.split("\t")[1] if "\t" in line else line[1:] if status == "D": score["fix"] += 1 continue path_lower = path.lower() for t, kws in TYPE_KEYWORDS.items(): if any(k in path_lower for k in kws): score[t] += 1 if "." not in path_lower.split("/")[-1]: score["chore"] += 1 if not score: return "chore" return score.most_common(1)[0][0] def main(): files = get_staged_files() if not files: print("暂无暂存区的变更,请先执行 git add") sys.exit(0) type_ = suggest_type(files) # 简单scope:取变更最集中的一级目录名 scopes = Counter() for line in files: path = line.split("\t")[1] if "\t" in line else line[1:] parts = path.split("/") if len(parts) > 1: scopes[parts[0]] += 1 else: scopes["root"] += 1 scope = scopes.most_common(1)[0][0] print(f"建议类型: {type_}") print(f"建议scope: {scope}") print(f"变更文件数: {len(files)}") if __name__ == "__main__": main()这段代码有几个值得注意的细节。第一,命名强制--cached,只分析暂存区的变更,因为提交信息只应该关心它。第二,对“删除文件”我直接赋予fix类型——删文件往往意味着清理或修复某个导致问题的资源,这个启发式判断在多数仓库里是成立的。第三,scope取的是“变更最集中的一级目录”,这比让模型自己猜可靠得多,模型拿到的不是“app/controllers/user_controller.rb”这种啰嗦信息,而是一个干净的“app”。
脚本的价值在于剔除不确定性。规则写在SKILL.md正文里,不同模型可能理解不一致;规则写在python里,所有人、所有模型拿到的都是同一个答案。这也是我对“skills”这个方向最大的体会:能让脚本做的事,就别让模型自由发挥。
3.3 模板和参考文档:让产出格式也变成一种约束
模板文件不复杂,但它的存在让输出格式不再是AI临时决定的。commit_subject.txt内容:
{type}({scope}): {subject}commit_body.txt内容:
{type}({scope}): {subject} - 变更点1 - 变更点2 Refs: issue-#{issue_id}为什么模板单独拆成文件而不写在SKILL.md里?因为后期我想扩展一个场景:把模板里的subject和body用于生成git提交时的-m参数,拆分文件后脚本可以直接读取模板拼装。技能包做到后面,你会发现“资源文件”和“说明文件”分开能省很多事,别图省事全堆在一个Markdown里。
references/conventional_commits.md我放的是完整的type表、每个type的使用样例、以及“什么情况别用这个type”。比如我会在里面写:不要用docs去描述“修复了文档里错的API地址”,那其实是fix;不要用refactor去描述“定义了新变量”,那是feat。让模型在生成前快速查一遍这个表,比让它靠记忆强。
3.4 挂载、调用与验证:从“能用”到“好用”
技能包的挂载方式因框架而异,但通用逻辑是告诉框架“你的技能目录在哪”。我先创建目录结构并放入文件:
mkdir -p skills/commit-helper/{scripts,templates,references} # 放入SKILL.md、analyze_diff.py、模板文件和参考文档 # 然后在你的agent配置中把技能根目录指向 ./skills 即可挂载完第一件事不是直接聊天,而是先做一次本地冒烟测试。我会造一个包含新增文件、修改文件、删除文件的暂存区,然后调用技能包生成提交信息。实测产出类似:
feat(cli): add format command for output alignment - 新增 --format 参数支持表格输出 - 调整列宽计算逻辑,避免中文对齐异常这个输出就可以直接git commit -F使用了。从这一步开始,我基本不再手写commit message,遇到“把改动提交一下”的需求,直接让AI调用技能包,几秒钟出结果,不满意就让它改style。
4. 调试和排查:技能包翻车实录
4.1 技能没被触发:问题多半出在description
这是技能包最常见的翻车现场。用户说“帮我把代码整理一下提交了”,结果AI没加载技能包,而是把它当成普通对话处理,最后回了一段“建议你执行git add……”的废话。
排查思路很简单:先看description里有没有覆盖用户这句话里的行为词。用户说的是“提交”,description里如果只写了“生成git提交信息”,模型就很可能不认为这是在请求生成提交信息。我的解决办法是给description加一层“触发场景枚举”:
description: 当用户要求提交代码、生成commit message、检查commit规范性、或出现“提交一下”“写个commit”“看看怎么提交”等意图时使用。枚举触发场景时不用怕啰嗦,但要防过度触发。我见过有人把description写成“处理与git相关的所有问题”,结果用户问“git怎么回退版本”也加载了技能包,然后技能包强行走“生成提交信息”的流程,闹出大笑话。原则是:宁可窄,不可宽。窄了最多漏触发,宽了会误触发,误触发比漏触发难排查得多。
4.2 模型不按流程走:如何把指令从“建议”变成“强制”
另一个高频问题是:模型看了SKILL.md,但它不执行“运行脚本”这一条,直接凭空生成submit message,还编得有模有样。这是LLM的通病——它更喜欢“直接产出”而不是“先调用工具再产出”。
对治的办法有三个,我反复调优后认为都有效,建议叠加使用。第一,把“运行脚本”提为第0步并且用加粗标记,告诉模型“在我们拿到脚本输出之前,禁止生成任何提交信息”。第二,在SKILL.md里加一句“本技能定义的流程优先于模型默认行为”,这句话还真能提高不少遵循率。第三,也是最有用的,要求模型在最终回复中附上“检查清单”(就像前面第7步那样),一旦模型知道自己要在输出里交代执行过程,它就不太敢跳步了。
4.3 脚本环境问题:路径、依赖、编码
脚本跑不起来,再好的设计也白搭。我在Windows和macOS两个环境都踩过坑,总结成几个要点。
路径得用相对当前脚本文件的定位方式,别写死绝对路径。SKILL.md中引用脚本时我已经用scripts/analyze_diff.py这种相对路径了,脚本内部如果有子模块,也用Path(__file__).parent来推导基础目录,这样整个技能包在任意文件夹解压都能跑。
编码是大坑。Windows下subprocess默认返回bytes,且中文文件名经常触发gbk/utf-8混乱。我在代码里用了text=True加上errors="replace",基本能挡住绝大多数编码异常。如果你们的仓库里还有更生僻的文件名,可以再考虑在SKILL.md的“环境要求”一节注明“运行于python3.9+,建议使用UTF-8终端”,给使用方一个明确预期。
依赖要克制到最少。我的脚本只依赖python标准库和git命令本身,没有引入第三方包。任何第三方依赖都会让技能包的分发成本上升一个量级,团队里别人一装就跑不起来。能用标准库解决的问题,绝不上依赖。
4.4 多技能协作时的冲突与隔离
当你的skills目录里不只一个技能包时,冲突就来了。我后来做了“文档排版校验”技能,它也宣称处理“docs”类型;结果有一次同时触发两个技能,模型分身乏术,一份输出里混合了两套规则。
解法分两层。第一层是description边界写清楚:每个技能的description都加上“本技能不处理XXX情况”。比如commit-helper里写明“只处理与git提交信息相关的内容,不负责文档排版规范”,这能显著降低并发触发概率。第二层是在SKILL.md里增加一个字段声明优先级:
--- name: commit-helper description: ... priority: high conflicts: docs-formatter ---当框架读到两个技能都命中时,优先级高的先执行,冲突技能的规则明确不采用。这属于进阶玩法,但如果你要把技能包铺开给团队用,建议从一开始就留出这个字段位,别等冲突爆炸了再返工。
5. 技能包设计的三条原则,也是我踩坑后的心得
5.1 能写进脚本的规则,就绝不留白给模型
我做的这些技能包里,凡是“确定性逻辑”,最终都下沉到了脚本:commit类型判定、scope提取、diff统计,全是代码算出来的。SKILL.md只保留两样东西:操作流程和决策参考。为什么?因为模型在“确定性判断”上不可靠,但在“基于确定结果做表达”上非常强。让AI干它擅长的活,把脏活累活交给代码,这是我做完四五个技能包后最深的感触。
5.2 技能包要当产品维护,不是当提示词随手扔
大多数人的技能包活不过一周,是因为它没有版本管理、没有changelog、没有测试样例。我现在每个技能包都紧跟git仓库,frontmatter里的version字段与tag对齐;SKILL.md的正文变更必须附带更新说明;还会在tests/目录下放3-5个输入样例和期望输出。这些做法听着繁琐,但好处是团队里其他人接手时能快速理解这玩意的边界和用法,而不是靠群聊里翻聊天记录。
5.3 先保证稳定触发,再去追求生成质量
技能包迭代的顺序是:先保证该触发的时候必触发、不该触发的时候不触发,再看产出内容质量。很多人一上来就打磨SKILL.md里的措辞,结果发现技能包根本没被加载,白费功夫。我的调试顺序永远是:description触发测试、流程遵循度测试、输出质量测试、跨模型一致性测试,一步步来,每一步过了再进下一步。
这套commit-helper技能包后来我还扩展出了changelog生成器、PR描述生成器,最后直接接到CI里,每次PR都会自动检查commit message合规性,不合规就由机器人评论提示。从那以后,代码评审时再也没人追着问“这句commit到底改了什么”。
“skills”这个方向,真正的价值不在于你写了多少花哨的规则,而在于你把它变成了一个可测试、可复用、可协作的工程产物。如果你也想做一个技能包,我建议从“你天天要做的重复性规范任务”入手,比如commit message、周报模板、接口文档格式。选定一个场景,按我上面的思路搭出目录写上SKILL.md,跑通一次你就能彻底理解。
最后分享一个具体的小技巧:给SKILL.md正文里每个步骤都设定“可验证的输出物”。比如“运行命令之后,你会拿到一份脚本输出”,这就是这个步骤的输出物。步骤是否有输出物,是判断它写没写到位的最快标准。没有输出物的步骤,多半是废话,删掉它。