说个最近一直在琢磨的事。我也是被 Andrej Karpathy 在公开分享里反复提到的那个观点“真正重要的不是某个 prompt 写得有多精巧,而是你能不能把一次成功的工作方式沉淀成可复用的能力”反复敲打,最后彻底掉进了 Agent Skills 这个坑。这一年多,我从最开始收藏一堆提示词,到后来被 Claude Code、Codex、OpenCode 这些工具带着走,再到自己动手写 SKILL.md、设计技能包的目录结构、甚至给团队内部的技能库做回归测试,整个思路发生了很大的变化。这篇文章不打算写成什么标准教程,而是从我自己的踩坑和复盘讲起,把“skills”究竟是什么、它的底层结构怎么设计、实际开发中哪些环节最容易翻车,完整地拆给你看。无论你是刚开始接触 agent 开发的新手,还是已经上手了几个工具想在工程化上更进一步,这篇应该都能给你一些能直接落地的参考。
1. 从“提示词收藏夹”到“Agent 技能库”:一次方法论迁移
我最早用大模型干活的方式大概率和你差不多:在聊天窗口里把背景信息、需求、约束条件一股脑贴进去,然后一边调整措辞一边祈祷生成结果靠谱。遇到有价值的长 prompt,就存到收藏夹或者网盘里,下次干活再复制出来改改。这套玩法在单轮对话、任务边界清晰的时候是没问题的,可一旦涉及到多文件项目分析、持续集成、批量代码重构这类复杂任务,就会发现两个特别要命的问题:第一,同样的 prompt 在不同的上下文窗口里表现极不稳定,稍微多贴几段日志它就“忘了”前面的规则;第二,所谓经验只存在于聊天记录里,换个人、换个项目、换台机器,一切都要从头再来。
Karpathy 有一句被我摘进笔记的话,大意是“我们不该训练模型去匹配提示词,而应该让执行环境去承载一部分智能”。这句话我一开始没太当回事,直到后来接触到 Claude Code 的 skills 目录和 Codex 的 skill 机制,才反应过来:skills 本质上就是把“一次性对话”升级成了“可挂载的工程资产”。它不是一个提示词,而是一个自包含的目录,里面装着任务说明、示例、脚本、约束文件,甚至还有一组用来验证结果是否达标的测试用例。Agent 在启动任务时按需加载这个目录,相当于给模型外挂了一套“操作手册 + 工具集”。
为什么偏偏是这个时间点 skills 突然火起来?两个原因。一是模型本身的上下文窗口虽然变大了,但“上下文质量”并没有等比提升,给得越多,注意力越分散,结构化、局部化的指令反而更可控;二是 agent 类应用的执行链路变长了,单靠自然语言已经没法约束住模型的自由度,必须把一部分“约束”下沉到文件层、脚本层和校验层。所以 skills 不是一个锦上添花的功能,而是 agent 工程化的必然产物。
1.1 为什么“skills”比“精心设计的 prompt”更进一步
很多人第一次看到 SKILL.md 文件时的反应是:这不就是把提示词写进 markdown 吗?我一开始也这么想,直到真正上手才意识到差别很大。普通 prompt 是给模型看的“一次性输入”,只有文本本身;而一个技能包是给模型和解释器共同看的“多文件工程”,除了指令文本,它还包括了文件读取、脚本执行、结果校验等多个环节。
举个例子,我写过一套“Git 仓库变更分析”技能,目录长这样:
git-change-analysis/ ├── SKILL.md ├── scripts/ │ ├── collect_diff.py │ └── detect_risk.py ├── references/ │ └── common_mistakes.md └── tests/ ├── case01_input.txt ├── case01_expected.txt └── run_tests.shSKILL.md 里只写了这个技能在什么场景下使用、应该按什么步骤执行、必须调用 scripts 下哪个脚本、结果的输出格式是什么。而真正的“智能”有一部分落到了 collect_diff.py 和 detect_risk.py 里——脚本负责过滤无效 diff、计算文件变更热度、标记可能存在安全风险的改动点。模型只是个调度员,它调用脚本拿到结构化结果,再基于结果生成结论。这比“请仔细分析以下 git diff”可靠得多,因为脚本的确定性是模型不具备的。
1.2 技能、工作流、MCP 工具到底有什么区别
概念混乱是刚接触 skills 时最大的阻碍。我梳理了自己实际用下来后的理解,三者区别如下:
| 维度 | 技能(Skill) | 工作流(Workflow) | MCP 工具 |
|---|---|---|---|
| 核心载体 | SKILL.md + 引用文件 + 脚本 | 多步骤编排定义文件 | 独立功能性接口 |
| 解决的问题 | 教会模型“怎么做一类任务” | 规定“任务按什么顺序执行” | 提供“模型可调用的外部能力” |
| 是否包含代码 | 可以包含脚本和测试 | 通常是配置文件(如 YAML/JSON) | 是独立服务/函数 |
| 粒度 | 任务级,偏“方法论” | 流程级,偏“流水线” | 原子级,偏“动作” |
| 生命周期 | 随着经验迭代 | 相对稳定 | 按需部署 |
简单说,技能是“做一件事的方法包”,MCP 是“能用的工具”,工作流是“串起多个动作的剧本”。一个技能在运行过程中可以调用多个 MCP 工具,也可以触发一个工作流;反过来,一个工作流也可以在不同步骤加载不同技能。理解了这层关系,再去设计自己的技能库就不容易搞混。
2. 拆解一个完整 Skills 包:SKILL.md、脚本资源与工具绑定
要真正掌握 skills,最直接的办法就是找一个成熟的开源技能库拆开看。我一开始用的是社区里比较知名的几个仓库,把里面的结构从头到尾过了一遍,后来自己也仿照这个体系搭了一套。下面我把一个标准技能包的各个组成部分以及它们各自的作用讲清楚。
2.1 SKILL.md 的构成:Frontmatter、指令体、示例区
SKILL.md 是整个技能包的入口。Agent 加载技能时,首先读取的就是这个文件,所以它的结构会影响模型对整个技能的“第一印象”。一个完善的 SKILL.md 通常分三块:
--- name: frontend-code-review description: 用于前端项目代码审查,重点关注组件性能、可访问性、状态管理设计,适用于 React/Vue 项目提交 MR 前的自检阶段。 license: MIT metadata: version: 0.3.1 priority: high --- # 前端代码审查技能 ## 适用场景 - 提交 MR/PR 前,对本次改动做一轮系统检查 - 针对组件重复渲染、useEffect 依赖缺失、无障碍属性遗漏等问题进行定位 ## 执行步骤 1. 读取目录下的 CHANGES.diff 文件,若不存在则运行 `git diff --staged` 生成 2. 运行 `npx eslint --format json` 获取静态检查结果 3. 调用 scripts/analyze_components.py 解析变更涉及的组件文件 4. 按【输出模板】输出审查结论,不要输出与结论无关的内容 ## 关键约束 - 只关注变更文件本身,不展开全项目的技术债 - 不修改任何源代码,只输出审查报告 - 对严重问题用「严重」「建议」「可选」三级标记 ## 示例输出 - 审查报告输出格式见 references/report_example.mdfrontmatter 里的 description 字段会在模型做“技能选择”的时候被读取,所以这段话必须写得精准:说清楚“什么时候用”“处理什么类型任务”“有什么边界”,别写“这是一个非常好用的技能”这种废话。指令体部分要遵循“少而准”的原则,尽量用可检查的动词来驱动模型,比如“读取”“运行”“解析”,而不是“仔细分析”“认真核对”——后者的自由度太大,输出质量很难收敛。
示例区往往是最容易被忽略的部分,实际上它的作用比指令还大。模型本质上是模式匹配机器,给它一个完整的高质量输出示例,效果远好于用十句话描述“你应该输出什么”。我在设计前端代码审查技能时,把一份真实的、标注完整的审查报告放进了 references/report_example.md,模型照着这个模板输出的结果,直接就可以拿来发给开发同事。
2.2 技能如何调用脚本与外部工具
技能包里的脚本承担的是“确定性智能”的角色。模型擅长归纳总结,但不擅长精确计算、批量文件操作、格式校验这类事。与其让模型用自然语言去“模拟”计算结果,不如写一个 Python 或 Node 脚本把脏活累活接过去。
我习惯把一个技能包里的脚本分成两类:分析型脚本和校验型脚本。分析型脚本负责从原始输入中提取结构化信息,比如从一个 git diff 里提取变更函数列表、从日志文件里统计错误码频次;校验型脚本负责检查模型生成的输出是否符合预期,比如检查生成的 JSON 是否合法、字段是否齐全、引用的文件路径是否存在。
举一个我在“数据血缘分析”技能里的实际例子。一开始我让模型直接读 SQL 建表语句,自己推断字段之间的血缘关系。结果是:字段一多,模型就开始胡编关联关系。后来我在技能包里加了一个参考脚本,专门解析 CREATE TABLE 语句并生成表字段树,模型只需要基于这棵树的 JSON 输出来做间接推理,正确率一下就上来了。这说明一个道理:技能包里能确定的东西,尽量不要留给模型自由发挥。
2.3 同一份技能在不同 Agent 之间的适配差异
Claude Code、Codex、OpenCode 等工具对 skills 的支持并不完全一致,这是我在迁移技能时踩过最多坑的地方。下表是我自己实际迁移后的经验总结:
| 能力点 | Claude Code | Codex(CLI) | OpenCode |
|---|---|---|---|
| SKILL.md 自动发现 | 支持,放在.claude/skills目录 | 支持,通过配置文件声明 | 支持,项目级和全局级分开 |
| 脚本执行许可 | 需要用户授权,支持白名单 | 支持,需注意沙箱策略 | 支持,命令可配置 |
| 变量插值(如输入路径) | 支持 | 支持 | 支持 |
| 技能间互相引用 | 有限支持 | 支持 | 有限支持 |
| 测试钩子(skill 内调用测试) | 可通过命令执行 | 支持 | 支持 |
最需要注意的一点是“脚本执行许可”。在 Claude Code 里,技能包中的脚本不一定能自动运行,首次执行时往往需要用户确认;而在 Codex 里如果沙箱策略设置不当,技能包内的 Python 脚本可能根本无法读取某些路径。我自己的做法是:在技能包内放一个 bootstrap.sh,专门负责检查环境变量和权限,如果脚本没法执行就给出明确的提示说明,避免模型傻乎乎地反复重试同一个失败命令。
3. 从零开发自己的技能:边界拆分、指令设计与回归测试
聊完了结构,接下来是这门手艺的核心:怎么从零开发一个真正好用的技能。开发技能和写代码有点像,前期需求分析做得越透,后期返工越少。下面是我的完整工作流。
3.1 第一步:把任务拆成可验证的“输入-输出”
开发技能最容易犯的错是贪多。想在一个技能里覆盖所有分析场景,最后写出来就是一个啥都管但啥都管不好的巨型指令。正确做法是先把任务拆成“最小可验证单元”。
我当时复盘能力强检技能的思路是这样:从“学习前端项目代码”这个宽泛需求中,拆出“组件重复渲染检测”“复杂条件表达式可读性评估”“事件监听内存泄漏风险排查”“样式类命名与设计规范一致性”四个子任务。然后只针对第三个子任务做第一版技能,因为它边界清晰:输入是一份包含组件创建与销毁逻辑的 JS/TS 文件,输出是一个危险点列表,每一项需要指出风险位置、泄露对象、触发路径。
判断一个子任务适不适合做成技能,我有一个很笨但有效的标准:如果人类专家在没有上下文交流的情况下,拿着你写的说明文档就能完成这个子任务,那么这个子任务就是合格的。如果还需要反复追问细节,说明任务边界根本没拆清楚。
3.2 指令怎么写才不会让模型过度发挥
指令撰写阶段,我的核心原则是“把边界写进约束,把标准写进示例”。约束部分一定要使用否定句,明确指出哪些事情不允许做。比如我这套“内存泄漏排查”技能里写了三条硬约束:
- 不得仅凭 class 名称猜测组件生命周期,必须在代码中定位到对应生命周期方法。
- 不得将第三方库内部逻辑作为风险项报告,除非它被项目代码显式调用。
- 对无法确认的依赖关系,统一标注为“待确认”,不得使用“可能存在风险”这类模糊表述。
这些否定式约束的本质是给模型设置“停止信号”。大模型生成文本时是逐个 token 往外蹦的,如果没有明确的禁止项,它很容易顺着最自然的语言惯性继续写下去,写到最后自己都不知道自己在说什么。有了停止信号,模型在语义相似处会更倾向选择“更安全”的表达。
另外一个容易忽略的细节是“输出长度控制”。我通常会在指令里给出明确的章节上限,比如“每个风险项的描述不超过 80 字”。否则模型会写出一大段绕圈子的话,看似信息丰富,实际对后续阅读者毫无价值。对细节的颗粒度控制,比多写十句鼓励性提示词都有用。
3.3 用测试用例做回归,把技能当成小型软件工程
技能也是会回归的——模型升级、依赖变化、示例改写,任何一个环节变了,同一份技能的输出质量都可能抖动。所以我把测试用例当成技能开发中不可省略的环节。
我的测试策略是准备一组“固定输入”。拿刚才的前端代码审查技能举例,我会准备三个 case:一个包含明显重复渲染问题的 React 文件、一个只有轻微风格问题的 Vue 文件、一个故意制造边角条件(比如未安装依赖)的坏输入文件。测试时我把每个 case 的原样输入喂给技能,对照期望输出,看模型是否按照约束执行了。
刚开始这套测试靠人肉看输出,效率非常低。后来我写了一个测试 runner 脚本,直接把技能生成的报告和期望报告做两个层面的对比:一是结构化字段的缺失检查,比如是否包含“风险等级”字段、是否包含“代码行号”;二是语义相似度打分,低于阈值就标记为疑似回归。虽然语义打分还不能做到完全准确,但至少能快速筛掉那些非常离谱的输出。
试验了多个版本的指令措辞之后,一个很意外的发现是:示例对测试结果的影响最大。有一次我只是在示例报告里增加了一个“影响范围”子字段,所有测试 case 的语义相似度都明显提高。这说明模型非常擅长模仿示例的结构,所以在示例上花时间打磨,性价比远高于在指令描述上反复堆词。
4. 搭建个人技能库:命名、依赖、上下文预算与版本管理
当技能数量超过十个之后,管理成本就开始显现了。技能不是写完就完事,它需要被维护、被组织、被升级,这背后其实是一套知识工程问题。
4.1 技能的命名与层级设计,让它能被快速复用
我先说命名这件事。前面提到 Agent 会读取技能包里的 description 字段来做技能路由。这个字段和技能目录名一起,决定了模型在面临一个新任务时能不能找出这个技能。
我最开始用的是类型化命名,比如“前端技能”“后端技能”“数据分析技能”,听起来很整洁,但实际上模型经常把它们混淆——因为任务类别的边界本来就模糊。后来我换成了“场景 + 动作”命名,比如“react-mr-review”“sql-lineage-analyze”“error-log-triage”。这套命名有两个好处:一是模型的语义搜索能更精准匹配,二是人回头看目录时一眼就清楚这个技能是干嘛的。
对于有多个子能力的技能包,我会在技能内部做能力标签,而不是拆成多个目录。比如“react-mr-review”技能里同时覆盖了性能检查和可访问性检查,我会在 frontmatter 里定义tags: [react, performance, accessibility, mr-review],这样模型可以在技能内部按需选择执行路径,又不会让技能目录爆炸。
4.2 上下文窗口是硬约束:如何控制技能加载体积
另一个真实存在的隐患是上下文被技能包撑爆。技能包里的 SKILL.md 写得太长,参考文件贴得太多,AI Agent 在加载技能后还没开始干活,上下文就已经消耗了三分之一。我的经验是控制技能包体积遵循“三级原则”:
- 第一级,SKILL.md 本体控制在 150 行以内,只放最关键的行为指令和输出模板。
- 第二级,references 目录存放详细规范、示例报告、领域术语表,模型按需读取。
- 第三级,scripts 与 tests 目录存放代码文件,这些不会全部进入上下文,只有执行时才被读取。
三级各自的目标是:“核心指令要让模型一次读懂;扩展知识要能按需取用;代码和测试则不要占用模型的注意力”。通过这个设计,即使是一个功能很复杂的技能,模型在加载时消耗的上下文也保持在可控范围内,留给真正任务处理的窗口就多了很多。
还要提醒一句关于版本管理。技能包实际上是一份可执行的“知识源码”,它应该跟代码一样做版本管理。我现在的要求是:每个技能包必须带 version 字段;修改行为逻辑时务必要同步更新测试用例;发布技能时写上 CHANGELOG,哪怕只有一句话也行。这个习惯帮我避过好几次“技能怎么突然不听话了”的坑——查版本记录发现,原来是自己上次调整示例时引入了格式不一致。
5. 我在真实项目里踩过的技能坑与排查链路
就算你把前面所有原则都做到了,实际跑起来还是会出幺蛾子。这一节我整理了几个我自己的翻车案例,每个都附上完整排查链路。这些坑很有代表性,你可以当成一份避坑清单来用。
5.1 坑一:技能被当成“万金油”,什么任务都往里塞
现象:我的前端技能包在手头三个项目里都跑得很好,到第四个项目时就疯了。明明是个用 Angular 的老项目,模型却按照 React 的组件模式对它做代码建议,给出的方案几乎全都要推翻重构。
排查过程:
- 先检查技能包里的 description,发现只写了“适合前端项目”,没有写“React 为主”,也没有声明“可选支持 Vue 基础,暂不支持 Angular 特殊语法”。
- 继而检查 references 里的现有项目样例,发现样例全是我平时维护的 React/Vue 项目,没有任何 Angular 专属代码作为对比。
- 最终定位:技能没有设置明确的项目框架识别前置门槛,也没有针对不支持框架的输出“停止信号”。
解决方案分两步:第一,在 SKILL.md 最前面增加“适用条件”和“不支持范围”两个小节,并把“非 React/Vue 项目不要执行”写进约束里;第二,在技能包内增加一个frameworks.py检测脚本,进入分析前先读取项目配置文件,识别出项目用的是什么框架,如果不是支持范围就打印提示并直接退出流程。这个补丁之后,技能就老老实实识别自己不认识的框架了。
5.2 坑二:技能输出格式漂移,同一天上下午两个版本
现象:上午生成的分析报告结构很规范,下午同一份技能在同一个仓库里输出的报告格式全变了,章节顺序被打乱,字段名也对不上。
排查链路:
- 看模型日志,发现上午的调用带进了“项目 README 中的表格风格”,下午的调用则带进了“某篇代码注释中的描述语气”。
- 定位问题根源:不是技能变了,而是技能执行期间 Agent 还有别的上下文导入,模型把上下文里的其他格式习惯“转移”到了技能输出上。
- 处理方式:在示例区把输出模板改成了不可辩驳的“代码块式模板”,并给模板文件增加了 JSON Schema 版本,在技能执行完最后一步之前强制调用一次
validate_output.py,格式不对就修正后输出。
教训:技能输出不能只靠模型自觉。越关键的输出,越需要外部校验来“兜底”。格式问题虽然不致命,但会极其影响下游自动化流程。
5.3 坑三:依赖了不存在的命令,技能在 CI 环境直接挂掉
现象:集成到 CI 流水线后,技能包里的脚本调用系统命令jq,但 CI 基础镜像里根本没装 jq,结果下游步骤拿到的 JSON 全是错的。
排查链路:
- 在本地环境复现,一切正常——因为本地开发机装了 jq。
- 检查 CI 镜像,发现是精简版,缺少一系列基础工具。
- 解决办法有两个,我最后两条都做了:一是在技能包中增加 dependency_check.sh,在执行初期检查核心命令是否存在,缺失时给出安装命令;二是把对 jq 的依赖直接从脚本中移除,改用 Python 自带的 json 模块解析,从根源上消除环境差异。
这提醒我在设计技能脚本时的基本原则:尽量用 Python 标准库,或者项目里明确声明的依赖来实现数据处理逻辑,避免依赖外部 shell 工具。做一个能在极端环境里跑起来的技能,比做一个“在你电脑上完美运行”的技能重要得多。
6. 那些真正长期有用的“技能”来源与持续迭代建议
最后分享一点关于技能库整体运营的判断。网上已经有不少开源 skills 仓库,质量参差不齐。我的选择标准有三个:看它有没有 Programmatic API 式的脚本,看它有没有明确的适用边界,看它是否附带测试或验证用例。三个都满足,才值得拖进本地作为起点;缺任意一个,就作为参考看看思路,自己再重写一版。
从我的实际使用经验看,最值得自己动手开发的技能,往往不是那些通用性很强的“代码生成”“代码解释”类,而是那些和你的具体工作场景强绑定的技能。比如我写过“周报自动生成技能”,它读取我这周提交的 commit、合并的 MR、处理的 issue,按公司要求的格式生成周报初稿,再调用校验脚本,确保不包含敏感项目代号。这类的技能官方仓库基本不会给你,但对个人效率的提升是实打实的。
持续迭代的方法上,我现在遵循的是“再多一版就停”原则。技能生命周期里最常见的失败,不是没人用,而是作者陷入无限优化循环——改一版示例,跑一次测试,再改一版指令,再跑一次测试,迟迟不给技能“封版”。给技能定一个明确的发布迭代节奏,例如每月只做一次版本迭代,剩下的时间集中在收集实际使用中的失败案例,比每天微调语言表达要高效得多。只有把技能开发当成知识资产的长期管理,而不再是写一次性脚本,才能真正体会到这套体系的威力。