做Agent开发这一年多,我最大的感受是:模型能力再强,架不住任务边界不清。你以为自己在调教AI,实际上大部分时间都在跟“它不知道什么时候该做什么”较劲。直到我把工作重心从“写更好的提示词”转到“设计更好的Skills”,整个Agent的行为质量才真正开始稳定下来。这篇文章就想聊聊,围绕skills这个词,这几年我摸索出来的设计思路、目录结构、落地步骤和踩坑经验。如果你也在做Agent类应用,或者正被“提示词越写越长但效果越来越差”折磨,这篇应该能帮上忙。
我先把话放前面:Skills不是提示词的换皮,它是一个带触发条件、执行流程、输入输出协议、验证机制和回退策略的完整行为单元。把它当成“给Agent写岗位说明书”来理解,很多设计决策都会变得清晰。
1. 为什么通用Agent搞不定专业任务
1.1 症状:对话能力强,执行能力弱
先看一个典型场景。你让Agent“帮我检查一下这个项目的代码质量”,它大概率会回你一段泛泛的代码风格建议。你让它“把这份周报转成管理层摘要”,它给你的可能是格式漂亮的模板,但核心数据全没对齐。这不是模型笨,是任务目标本身模糊——通用对话模型擅长的是“生成合理文本”,而不是“按照某个专业流程完成一个确定的结果”。
我做个类比:你请一个刚入职的实习生干活,只告诉他“认真一点”,他什么也干不好。你得告诉他:岗位叫什么、服务对象是谁、遇到什么情况该启动、第一步做什么、第二步做什么、做到什么程度算合格、做不了怎么办。Skills干的就是这件事——把Agent从一个“什么都能聊的实习生”,变成一个“知道自己在什么岗位上、按什么规程办事”的专职员工。
1.2 真正意义上的Skills到底是什么
我说的Skills,是指以技能包(Skill Pack)形式存在的一整套定义。它通常包含几个关键部分:
- 技能元数据:名称、版本、描述、触发场景,让调度层知道“什么时候该用这个技能”。
- 执行指令:分步骤的操作说明,告诉Agent每一步具体做什么。
- 输入协议:接受哪些参数、字段格式是什么,避免模型自由发挥。
- 输出协议:要求返回什么结构,可以是JSON、标记语言或固定模板。
- 辅助资源:必要的参考文档、脚本、数据字典,供技能执行时调用。
- 验证与回退规则:怎样算成功、失败后怎么办、能不能降级处理。
你可能注意到了,这很像一个微服务的接口定义,加上一份SOP(标准作业程序)。Skills的本质就是把“模型的能力”和“业务的要求”之间那层模糊地带抹平。它不试图改变模型的智商,而是改变模型的工作方式。
1.3 从“提示词”到“技能包”的转变
很多人一上来就问:Skill和写一个很长的system prompt有什么区别?区别大了。
提示词是一次性的、静态的、面向“对话”的。你写了“请按照以下步骤检查代码”,模型会照做——但前提是它每一步都记得住、不跑偏。而技能包是面向“任务执行”的。它把检查代码这个任务拆解成:先读哪些文件、按什么优先级检查、发现问题时怎么归类、最后输出什么格式。
我自己的经验是,提示词适合解决“模型不知道怎么答才好的问题”,比如风格、语气、知识范围;技能包适合解决“模型不知道怎么拆步骤才好的问题”,比如多环节操作、有标准交付物的任务。这两者不能互相替代,但一旦任务涉及三步以上操作,技能包的优势就会非常明显。一个400字的技能包,往往比2000字的提示词更有效,因为它把话都说在了节点上。
2. 把Skills放对位置:体系设计比写内容更重要
2.1 三种能力形态的分工
在Agent体系里,至少要区分三种能力形态:
| 形态 | 适合解决什么问题 | 典型例子 | 生命周期 |
|---|---|---|---|
| 系统提示词 | 设定总体人设与回答基调 | “你是资深技术顾问,回答要简洁” | 全局常驻 |
| 工具调用 | 执行确定性操作,获取结构化结果 | 查数据库、调API、发请求 | 按需触发 |
| 技能包 | 编排多步骤、有稳定交付物的专业任务 | 代码审查、周报提炼、发布检查 | 按场景加载 |
你会发现前两种大家都很熟了,但真正让Agent具备“岗位能力”的,恰恰是第三种。技能包是连接“模型通用能力”和“业务确定流程”的胶水层。
2.2 技能包与插件、工作流的边界
还有一个容易混淆的概念。插件(Plugin)往往强调“接入外部能力”,比如读文件、访问网络、调用某个云服务。工作流(Workflow)强调的是“多个节点按预定逻辑串联”。而技能包介于两者之间:它可能要调用外部工具,也可能只是内部推理,但它的核心特征是“为一个完整任务定义行为边界”。
举个例子,我做过一个“发布前检查”技能包。它不直接操作发布系统,也不需要用户输入什么复杂参数,它的任务就是读取一段发布描述,然后按十项检查规则逐条核验,最后输出一个检查报告。这中间没调用任何外部API,纯粹靠指令编排。但它依然是个标准的Skill——因为它有触发条件(用户要给出版本描述)、执行流程(十条规则)、产出格式(检查报告)。
技能包的调度,可以靠模型自主判断,也可以靠上层路由规则显式触发。我的经验是:宁可上层多写几条显式路由规则,也不要完全依赖模型自由判断。否则模型会时而想起用Skill,时而直接凭感觉回答。
2.3 标准目录结构
我在项目里常用的结构是这样的:
skills/ ├── review-pr/ │ ├── SKILL.md │ ├── scripts/ │ │ └── collect_diff.py │ ├── references/ │ │ └── review_rules.md │ └── examples/ │ └── output_sample.md ├── weekly-report/ │ ├── SKILL.md │ └── templates/ │ └── exec_summary.md └── ...每个技能包独立目录,互不干扰。SKILL.md是主入口,脚本文件夹放可执行工具,references放参考规则,examples放输出样例。这个结构的好处是:迁移方便(整个目录拷走就能用)、版本清晰(每个技能包自己管理版本)、还可以单独评测。
2.4 技能包与外部工具如何配合
技能包里的工具,尽量做成“单一职责”的小脚本。比如上面collect_diff.py,只做一件事:把当前分支相对主干的所有变更文件列表和关键diff提取成结构化JSON。Skill指令里不要求模型自己去理解Git底层逻辑,只要求它调用这个脚本获取结果,然后基于结果做判断。
这样做有三个好处:减少模型误操作的概率、提高执行速度、结果可复现。凡是涉及确定性计算的东西(取时间、算差异、查状态),全部交给脚本;凡是涉及判断、权衡、归纳的东西,交给模型。这个分工一旦确立,技能包的稳定性会大幅提升。
提示:如果有人让你写一个技能包,先问一句:这个任务里,哪些步骤是“计算”,哪些步骤是“判断”?把“计算”尽量脚本化,把“判断”留给模型。这是整个设计里性价比最高的一步。
3. 实战:手写一个能落地的Review Skill
3.1 需求定义:先写清楚这个技能解决什么问题
我一直强调:动手写SKILL.md之前,先把需求压缩成两句话。拿我常用的“PR代码审查技能”来说,需求是这样定义的:
- 服务对象:需要快速得到结构化代码审查意见的开发者。
- 核心任务:输入一个PR描述和变更文件列表,输出按严重级别分级的审查意见,包括问题定位、原因分析、修改建议。
不要小看这两句话,后面所有的元数据和指令设计都从这两句话推导出来。你要是连服务对象和核心任务都说不清,技能包必然会写成一本毫无重点的百科全书。
3.2 搭建目录与初始化文件
先建目录,然后把核心脚本准备好。我这里的collect_diff.py大概长这样:
#!/usr/bin/env python3 import json import subprocess import sys def main(base="main"): diff_result = subprocess.run( ["git", "diff", "--name-only", base], capture_output=True, text=True ) changed_files = diff_result.stdout.strip().splitlines() payload = {"changed_files": changed_files, "count": len(changed_files)} print(json.dumps(payload, ensure_ascii=False, indent=2)) if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "main")再说一次,这个脚本解决的是“确定性计算”:有哪些文件变了、变化数量是多少。至于“这些变更是否合理”,脚本不负责。
3.3 SKILL.md的核心字段怎么设计
SKILL.md是整个技能包的大脑。我建议用YAML前置元数据加正文指令的结构。看一个实际例子:
--- name: review-pr version: 1.2.0 description: 对给定的PR信息执行结构化代码审查,输出分级意见。 triggers: - 用户要求审查PR - 用户提供diff或变更文件列表并要求检查 - 代码合并前的质量把关 inputs: pr_description: 用户提供的PR描述文本 changed_files: 变更文件列表(可通过脚本获取) outputs: type: structured_report format: markdown_table required_fields: ["severity", "file", "issue", "suggestion"] validation: - 必须包含至少一条高优先级问题(如存在) - 每条建议必须可执行,不得模棱两可 fallback: - 如果无法获取变更列表,询问用户是否提供 - 如果审查目标不明确,先向用户确认范围 --- # Review PR 执行说明 你是一个代码审查专家,请严格按以下步骤执行: 1. 通过脚本获取变更文件列表,若用户已提供则可跳过。 2. 按 references/review_rules.md 中的规则逐项检查。 3. 每个问题必须给出文件路径、严重级别、问题描述、具体修改建议。 4. 输出时按以下格式组织: | 严重级别 | 文件 | 问题描述 | 修改建议 | | --- | --- | --- | --- | 严重级别只允许三个值:高、中、低。 高:可能引起功能异常或安全风险。 中:影响代码可维护性或潜在边界情况。 低:风格、命名、注释等非功能性建议。 如果发现的问题少于三条,请如实输出,不要为了凑数而编造。我特别看重validation和fallback两个字段。validation约束了输出的底线——比如“必须包含问题描述和修改建议”,这能防止模型输出一些正确的废话。fallback则定义了异常兜底路径——比如“拿不到变更列表怎么处理”,它让技能包在真实环境里不至于愣住。这两个字段是你和模型的“契约”,务必写清楚。
3.4 设计可验证的输出格式
输出协议往往被忽略,但它是评测技能包效果的关键。我的规则是:让输出尽量结构化,结构化到可以被脚本直接判分。
举个例子,上面表格里的严重级别字段,我可以用一段小脚本验证:
import re VALID_LEVELS = {"高", "中", "低"} def check_report(text): rows = re.findall(r"^\| (高|中|低) \|", text, re.MULTILINE) bad = [r for r in rows if r not in VALID_LEVELS] return len(bad) == 0有了这种验证脚本,你就能对技能包做回归测试了。改一版Skill后跑一遍测试集,是变好还是变差一目了然。这就是“可验证产出”的价值。可验证,才有资格谈迭代,否则全凭感觉。
3.5 跑通第一个测试用例
技能包写完后,先别急着接入正式流程。用三个测试用例过一遍:
- 理想输入:完整的PR描述加清晰的变更列表,看输出是否符合格式。
- 模糊输入:用户只说“帮我检查一下代码”,看触发和追问是否合理。
- 异常输入:变更文件特别多、超出模型上下文,看是否有截断或降级方案。
我第一次调这个Review Skill时,“模糊输入”测试立刻暴露了问题:模型在没有变更列表的情况下,自行脑补了一堆“可能存在的问题”,输出了一版看似专业、实则全错的审查报告。后来我在fallback里加了“如果无法获取变更列表,先明确告知用户需要哪些信息,不要凭空猜测”,这个问题才被按住。你写技能包时,一定要把这个“宁缺毋滥”原则写进去,否则模型会为了完成任务而编造内容。
4. 调试Skills时踩过的坑与完整排查链路
4.1 现象:模型每次跑到第三步就停下来
有个版本,我的Review Skill在测试时总出现一个诡异问题:前两步执行正常,到第三步“按规则逐项检查”时,模型开始输出“无法完成所有检查项”然后停下来。一开始我以为是模型能力问题,换了更强的模型也没用。
4.2 排查记录:从现象到根因
我把完整排查过程列出来,你可以照着这个思路走:
第一步,锁定范围。单独测试Skill,去掉外部干扰,发现依旧中断。确认不是环境问题。
第二步,隔离变量。临时把references/review_rules.md里的检查规则从15条缩减到5条,结果能跑完了。怀疑是规则数量超出模型有效注意力范围。
第三步,核查上下文。我打印了Skill加载后的完整指令体量,发现SKILL.md正文加辅助文档加起来超过三千字。模型长上下文确实能装下这些文字,但“装得下”不代表“每一步都记得住”。
第四步,读取失败输出。发现模型的最后输出是“已完成检查,未发现高优先级问题”——它跳过了中间推理,直接给了结论。这说明模型为了赶进度,选择了偷懒路径,规则根本没被逐条执行。
第五步,重构指令表达。我把15条规则从“长段落描述”改成“分组的检查清单”,每组明确写清检查标准和产出要求,并加入“必须逐条输出每条检查的结论,不得跳过”。问题立刻消失。
根因总结:不是指令不够多,而是指令的组织方式不符合模型的处理模式。长段落容易被“压缩”,而结构化清单更容易被“逐项执行”。这是一个容易被忽视的认知差异。
4.3 常见问题对照表
我整理了技能包开发中最常踩的几类坑,供你对照:
| 症状 | 可能原因 | 处理手段 |
|---|---|---|
| 完全不触发Skill | 触发条件描述太窄,模型没识别出场景 | 扩充triggers,加入同义说法 |
| 触发了但不按指令走 | SKILL.md正文过长、重点分散 | 精简指令,步骤化、清单化 |
| 输出格式不稳定 | 输出协议不够硬,模型自由发挥了 | 给出模板,并用脚本校验必填字段 |
| 总是编造事实 | 缺少“不确定时如实说明”的兜底规则 | 加入fallback和“宁缺毋滥”原则 |
| 效果时好时坏 | 技能包内依赖了模型自由推理的关键步骤 | 把确定性步骤脚本化、自动化 |
4.4 一个高效技巧:给Skill设计“干跑模式”
所谓干跑模式,就是让模型在不做真实修改、不调外部API的前提下,走一遍完整流程,只输出中间结果和最终报告。我几乎每个技能包都会在SKILL.md里加一个开关字段,比如dry_run: true时,脚本只打印将要执行的动作,不实际执行。
这个模式的价值在于:它把“技能包的逻辑正确性”和“外部系统的真实影响”解耦了。你可以在几分钟内测完一个技能包的所有分支,而不用准备真实数据、不用处理副作用。尤其是那些涉及写文件、发消息、改配置的Skill,干跑模式能救你无数次。
5. 评测与迭代:让一个Skill从“能用”到“好用”
5.1 建立技能包的回归测试集
技能包写出来不难,难的是持续改进。我的做法是给每个技能包建一个test_cases目录,里面放十到二十个历史真实案例。每个案例包含:
- 输入样例(尽量贴近真实用户表达)
- 期望输出特征(比如必须包含三级优先级、必须引用具体文件)
- 禁止事项(比如不得编造文件路径)
每次修改技能包后,跑一遍测试集,记录通过率。通过率不降,说明改动至少没变差;通过率提高,说明优化有效。这个机制听起来简单,但大多数团队根本没给技能包建过测试集,导致技能包永远停留在“好像还行”的状态。
5.2 三档评测维度
我给技能包的每次输出打三个维度的分:
| 评测维度 | 核心问题 | 评分方式 |
|---|---|---|
| 格式合规 | 是否按输出协议返回 | 脚本自动判分 |
| 逻辑完整 | 步骤是否执行完,有没有跳过、编造 | 人工抽查 |
| 业务有效 | 输出能否直接投入使用 | 业务方验收 |
第一个维度必须机器判分,第二个维度可以做抽检,第三个维度就要靠真实用户反馈了。这三档全部合格,我才认为一个技能包是“可用”的,而不是“能跑通”的。
5.3 失败样本驱动的迭代循环
迭代技能包,不需要拍脑袋想新功能,直接从失败样本开始。每次测试集里有不合格的输出,问三个问题:
- 是模型没理解指令,还是指令本身有歧义?
- 是任务超出边界,还是技能包漏了场景?
- 是输出格式问题,还是业务逻辑缺失?
针对问题定向修改SKILL.md或辅助脚本,然后重新跑测试集。我的经验是:迭代十轮之后,技能包的稳定性会有质变。最初的几次修改往往是补规则、加边界,后面几次开始变成精简表达、合并重复项。你会发现,技能包从“每轮加内容”变成“每轮删内容”,这个时候它才真正成熟。
5.4 十二条实操经验清单
最后分享十二个我写技能包几年攒下来的经验,不按优先级排,都是平级有用的:
- 技能包的第一版不用追求大而全,能把一条主线走通就算成功。
- 给技能包起名时,动词开头会比名词开头更容易被调度层识别。
- SKILL.md正文超过1500字,就必须考虑拆分子文档。
- 输出协议里,固定枚举值远比自由文本稳定,尽量让模型从几个候选项里选。
- 永远给技能包一个“无法完成时该说什么”的默认回复模板。
- 脚本出错时,不要让模型“猜”,报错信息里就要给出可执行的下一步。
- 不要把外部系统的账号密码放进技能包脚本里,用环境变量注入。
- 版本号要跟着技能包的规则变化走,而不是跟脚本代码走。
- 定期让业务方直接试用技能包,你会听到很多开发者视角想不到的问题。
- 每个技能包都要有一个负责人,否则它会在无人注意时悄悄腐烂。
- 技能包的日志要记录输入和输出摘要,出问题时才能回溯是哪个环节坏了。
- 如果同一个任务要写两个技能包才能覆盖,优先考虑是不是任务拆分的粒度错了。
我在实际项目里体会最深的是第一条和第十一条。很多人在第一个技能包上就想覆盖所有边界情况,结果写了几天还没跑通;也有人做完了技能包就扔在那里,出了问题也不知道从何查起。把目录搭好、日志留好、测试集建好,这个技能包才有持续进化的底座。
最后再分享一个小技巧:每次调试技能包时,保留一份“失败输出快照”文件夹。把输入、当时技能包版本、错误输出都存下来。等过段时间回头看,你会清晰地看到这个技能包是怎么一步步从不稳定走向稳定的,这些快照比任何文档都更能帮你理解“Skills为什么有效”。