
1. 为什么“写 Skill”成了 Agent 落地里最被低估的一环先把话说在前面Skill 不是提示词的换皮也不是把一段 SOP 塞进 Markdown 就完事。它更像是给 Agent 装的一块“可插拔能力模块”——有明确的触发条件、有稳定的输入输出契约、有可验证的执行边界。你写得好Agent 在复杂任务里就像老手带徒弟指哪打哪你写得烂Agent 就会在关键时刻“自信地胡说八道”还顺带把你的 token 预算烧穿。我接触过不少团队模型选型讨论得热火朝天Agent 框架对比表格拉了几十行结果真正决定任务成功率的是那个被随手丢在skills/目录下的SKILL.md。原因很朴素框架决定“能不能跑”Skill 决定“跑得对不对”。一个 Agent 可以接十个工具但如果 Skill 描述含糊、边界不清、示例缺失模型在编排时就会反复试探轻则多轮无效调用重则触发错误链路直接终止。这篇内容适合三类人一是正在做 Agent 开发、手里已经有一堆工具但不知道怎么组织成 Skill 的工程师二是想把团队内部流程沉淀成可复用能力的业务同学三是被skill-creator、SKILL.md、codex skill这些词刷屏、想搞清楚到底该怎么下手的实践者。我会围绕“如何写出好的 Skill”这条主线把结构设计、触发机制、参数契约、测试验证、常见坑位全部拆开讲尽量让你看完就能动手改自己手里的那份 Skill。需要先对齐一个认知Skill 的质量不取决于你写了多少字而取决于模型在什么情况下会想起它、想起它之后能不能一次用对。这两件事分别对应“可发现性”和“可执行性”后面所有章节都围绕它们展开。2. 拆开一个 Skill 的骨架SKILL.md 里到底该放什么2.1 从“能力声明”到“执行契约”的四个层次很多人写SKILL.md的习惯是标题、一句话描述、然后一大段自然语言说明。这种写法对人是友好的对模型是灾难。模型在编排阶段需要快速判断“这个 Skill 能不能解决当前子任务”它依赖的是结构化信号而不是散文。我习惯把一份 Skill 拆成四个层次缺一层都会出问题能力声明层这个 Skill 到底能做什么用动词开头一句话说清。比如“将用户提供的原始日志按时间窗口聚合并输出异常摘要”而不是“日志处理相关能力”。触发条件层什么信号出现时应该调用它。包括关键词、输入形态、前置状态。这一层直接决定可发现性。参数契约层输入有哪些字段、类型是什么、哪些必填、默认值是什么、非法值怎么处理。这一层决定可执行性。输出与边界层返回什么结构、失败时返回什么、明确不处理哪些情况。这一层决定可控性。这四层不是让你写成四个 JSON 块而是要在SKILL.md里用模型容易解析的方式表达。我的做法是能力声明用一句话加粗放在最前触发条件用列表参数契约用表格输出与边界用引用块加示例。模型读起来路径清晰人维护起来也不累。2.2 命名与描述模型靠什么“想起”你Skill 的name和description是模型做工具选择时最先看到的信息。这里有个反直觉的点描述不是写给人看的说明书而是写给模型的检索索引。我见过一个失败案例某个 Skill 的 description 写的是“帮助用户处理数据”。结果模型在任何涉及数据的任务里都会犹豫要不要调用它因为“处理数据”太宽泛和别的工具高度重叠。后来改成“读取 CSV 文件并按指定列去重输出去重后的行数与样例”调用准确率立刻上来了。具体怎么写我的经验是三条动词 对象 结果描述里必须出现明确动作和明确产出物。包含同义触发词如果用户可能说“清洗”“去重”“整理”描述里就自然带上这些词提升召回。避免和相邻 Skill 重叠如果两个 Skill 描述里都有“分析”模型就会摇摆。要么合并要么把边界写死。注意description 不是越长越好。超过两行的描述模型在候选列表里反而容易抓不住重点。我的习惯是控制在 40 到 80 个汉字之间把最独特的动作词放前面。2.3 参数设计别让模型猜你的意图参数契约是 Skill 最容易翻车的地方。模型不会读心它只会根据字段名和类型去填。字段名起得含糊填错就是必然。我总结了一个参数设计的检查清单每次写完 Skill 都过一遍检查项合格标准常见错误字段名名词性、无歧义data、input、param1类型明确 string/number/array/object全部写成 string必填标记显式标注 required靠描述里一句话带过默认值有合理默认或明确无默认默认值藏在长段落里枚举值列出全部合法取值让模型自由发挥错误处理说明非法输入返回什么完全不提举个具体例子。假设你要写一个“按时间窗口聚合日志”的 Skill参数可以这样设计{ log_path: string, required, 日志文件的绝对路径, window_minutes: number, optional, 默认 5, 取值范围 1-60, level_filter: string, optional, 枚举值: error/warn/info/debug, 默认 error, output_format: string, optional, 枚举值: summary/json, 默认 summary }这样模型在填参时几乎没有猜测空间。反过来如果你只写“传入日志相关信息和窗口参数”模型大概率会给你编一个字段名然后调用失败。2.4 示例好 Skill 和坏 Skill 的对照光讲原则不够直观我直接放一组对照。假设场景是“从一段文本里抽取待办事项”。坏 Skill 的SKILL.md大概长这样# Todo Extractor 这个 skill 可以从文本里提取待办。 输入是文本输出是待办列表。好 Skill 的写法# extract_todos 从自然语言文本中抽取待办事项输出结构化列表。 ## 触发条件 - 用户输入包含“待办”“todo”“需要做”“别忘了”等信号 - 输入为一段包含多个动作项的文本 ## 参数 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | text | string | 是 | 待抽取的原始文本 | | include_done | boolean | 否 | 是否包含已完成项默认 false | ## 输出 返回 JSON 数组每项包含 content、owner、due_date 三个字段。 due_date 无法识别时为 null。 ## 边界 - 不处理图片输入 - 不推断未在文本中出现的负责人 - 文本为空时返回空数组不报错差别在哪坏 Skill 把判断权全丢给模型好 Skill 把判断路径铺好了。模型不需要聪明只需要照着走。3. 触发机制让 Agent 在正确的时刻想起这个 Skill3.1 可发现性为什么比可执行性更难可执行性可以靠测试兜底可发现性不行。一个 Skill 写得再完美如果模型在需要它的时候没想起来那它等于不存在。这就是为什么很多团队的工具库越堆越大Agent 的表现却越来越差——候选太多描述太像模型选择困难。可发现性的本质是在模型的候选空间里占据一个独特位置。要做到这一点你得先知道模型是怎么做选择的。主流 Agent 框架在编排阶段通常会把所有 Skill 的 name 和 description 拼进上下文让模型输出要调用的 Skill 名。这意味着你的 description 是在和所有其他 Skill 的 description 竞争注意力。3.2 用“负向边界”减少误触发大多数人只写 Skill 能做什么不写不能做什么。这是误触发的主要来源。负向边界的作用是当模型在几个相似 Skill 之间摇摆时一句“不处理 X 场景”就能把它推向正确的那个。比如你有一个summarize_article和一个summarize_log如果两个都只写“总结内容”模型必然混。加上负向边界后summarize_article不处理结构化日志、不处理代码文件summarize_log不处理自然语言长文、不处理营销文案边界一写选择就清晰了。我的经验是负向边界至少写两条覆盖最容易混淆的相邻场景。3.3 触发词不是堆得越多越好有人为了让 Skill 更容易被想起在 description 里堆一大堆同义词。这招在早期有效但堆过头会带来两个问题一是描述变长重点被稀释二是和其他 Skill 的重叠度上升反而增加混淆。我的做法是分层核心触发词放在 description 第一句次要同义词放在触发条件列表里。这样模型第一眼看到的是最独特的信号需要细看时才有更多线索。3.4 多 Skill 共存时的编排策略当你的 Agent 挂了十几个 Skill光靠 description 竞争已经不够了。这时候需要在系统提示或编排层做分组。常见做法是按领域分组比如“数据处理组”“文本生成组”“外部调用组”让模型先选组再选 Skill。这能把单次选择的候选数从十几个降到三四个准确率提升非常明显。还有一种做法是给 Skill 打标签在编排时根据当前任务上下文先过滤一轮。这个对框架有要求但效果最好。如果你的框架不支持退而求其次把 description 写得足够独特也能撑一阵。4. 参数与返回值把不确定性关进笼子4.1 输入校验应该写在 Skill 里还是外面这个问题我被问过很多次。我的答案是能在 Skill 描述里说清的就不要指望外层校验。因为模型是在生成参数的那一刻就需要知道约束而不是等调用失败再重试。具体来说取值范围、枚举值、格式要求全部写进参数表。外层校验只做兜底不做主要防线。我见过太多团队把校验逻辑放在工具实现里结果模型反复用非法参数调用每次都被拒绝白白消耗轮次。4.2 返回值结构要稳定到可以写进测试返回值不稳定是 Agent 调试的噩梦。同一个 Skill这次返回字符串下次返回对象模型解析逻辑就得跟着变。我的原则是返回值结构一旦确定就当成 API 契约来维护任何变更都要走版本。一个稳定的返回结构通常包含三部分status成功/失败/部分成功data主体数据结构固定message人类可读的补充说明模型可选用这样模型在处理结果时路径明确不会因为字段缺失而卡住。4.3 失败路径比成功路径更值得写清楚成功路径大家都爱写失败路径经常被忽略。但 Agent 在真实环境里遇到失败的频率远高于 Demo。失败时返回什么、模型该怎么处理这些必须提前定义。我的习惯是给每类失败定义明确的返回比如参数非法返回status: invalid_input附带哪个字段不合法依赖不可用返回status: dependency_error附带依赖名超时返回status: timeout附带已执行时长模型看到这些结构化失败信息就能决定是重试、换 Skill 还是上报而不是一脸茫然地继续瞎调。4.4 一个完整的参数与返回示例还是用日志聚合那个例子完整契约如下// 输入 { log_path: /var/log/app.log, window_minutes: 5, level_filter: error, output_format: summary } // 成功返回 { status: success, data: { total_lines: 12000, matched_lines: 340, windows: [ {start: 2024-01-01T10:00:00Z, count: 12}, {start: 2024-01-01T10:05:00Z, count: 8} ] }, message: 共匹配 340 条 error 日志分布在 2 个时间窗口 } // 失败返回 { status: invalid_input, data: null, message: window_minutes 超出范围允许 1-60实际收到 120 }这种契约写进SKILL.md模型几乎不会用错。5. 测试与迭代怎么判断一个 Skill 写得好不好5.1 用“触发测试”和“执行测试”分开验证Skill 测试不能混着做否则你分不清是没被想起还是用错了。我习惯分两轮触发测试只给模型任务描述和 Skill 列表看它选不选这个 Skill。选错就是可发现性问题。执行测试强制指定这个 Skill看模型填参和解析返回是否正确。出错就是契约问题。两轮分开跑问题定位快很多。触发测试我一般准备 20 条左右的正例和 20 条负例负例是那些“看起来像但不该触发”的任务。执行测试则覆盖正常参数、边界参数、非法参数三类。5.2 边界用例才是真正的试金石正常用例谁都能过边界用例才见功力。我每次写完 Skill必测这几类边界空输入超长输入参数取最小值、最大值、越界值依赖服务不可用返回数据为空这些场景在 Demo 里遇不到在生产里天天遇到。提前测过上线就少掉一半头发。5.3 迭代时改什么、不改什么Skill 迭代有个原则改描述要谨慎改实现要激进。因为描述一变模型的触发行为就变可能影响其他任务的编排。而实现变更只要契约不变对模型是透明的。所以我的迭代顺序是先看触发测试结果如果触发不准微调 description 和触发条件如果触发准但执行错改实现或补参数说明尽量不动 description。这样能把变更影响控制住。5.4 版本管理别让 Skill 悄悄漂移Skill 也是代码需要版本管理。我见过团队直接在生产目录改SKILL.md改完没记录出问题查半天。最低限度要做到每次变更记录改了什么、为什么改、触发测试结果如何。有条件的话把 Skill 纳入和代码一样的评审流程。6. 那些年我踩过的 Skill 坑6.1 描述太抽象导致模型“想不起来”早期我写过一个 Skilldescription 是“辅助用户完成文档相关工作”。结果模型在任何文档任务里都不调用它因为它太抽象和模型对“文档工作”的直觉对不上。后来改成“将 Markdown 文档转换为带目录的 HTML”调用率立刻正常。教训是描述要具体到动作和产出物抽象词是触发杀手。6.2 参数默认值藏在长段落里被忽略有一次我把默认值写在参数说明的第三句话里模型连续多次没读到每次都传了错误的值。后来把默认值提到参数表单独一列问题消失。模型读表格比读段落靠谱得多这是实践出来的结论。6.3 返回值结构变更导致下游解析崩溃一个 Skill 早期返回数组后来为了加元信息改成对象结果所有依赖它的编排逻辑全挂。这次之后我定了个规矩返回值结构变更必须走新版本旧版本保留至少一个迭代周期。Agent 生态里契约稳定性比功能丰富更重要。6.4 触发词堆太多反而降低准确率有个 Skill 我为了提升召回在 description 里塞了十几个同义词结果它开始在不相关任务里被误触发。删到五个核心词后准确率和召回率都回升。触发词是调味料不是主菜。6.5 忽略失败路径导致 Agent 卡死最惨的一次是一个 Skill 在依赖超时时直接抛异常没有结构化返回。模型收到异常后不知道怎么办反复重试同一个调用直到轮次耗尽。加上status: timeout返回后模型学会了换策略。失败路径不是可选项是必选项。7. 从一份 Skill 到一套 Skill 体系7.1 什么时候该拆什么时候该合Skill 不是越细越好。拆得太细模型选择成本上升合得太粗参数复杂到没人会用。我的判断标准是如果一个 Skill 的参数超过七个或者触发场景跨越两个以上领域就该考虑拆。反过来如果两个 Skill 经常被一起调用且共享大部分参数就该合。7.2 命名规范让体系可维护当 Skill 数量上到几十个命名规范就是生命线。我习惯用“领域_动作”的格式比如log_aggregate、text_extract_todos、file_convert_markdown。这样在候选列表里同领域的 Skill 会自然聚在一起模型选择时也有领域线索。7.3 用 skill-creator 类工具提效但别依赖现在有不少工具能根据一段描述自动生成SKILL.md骨架比如热词里提到的skill-creator。这类工具适合起步能帮你把结构搭出来。但别指望它写出好 Skill因为触发条件和边界这些最关键的判断只有了解业务的人才能定。我的用法是用工具生成骨架然后手工重写触发条件和边界参数表逐项核对。7.4 持续观察真实调用日志Skill 写得好不好最终看真实调用数据。我会定期看三类指标触发率该触发时是否触发、一次成功率触发后是否一次用对、失败分布失败集中在哪类参数或场景。这三个指标能直接告诉你下一个迭代该改哪里。没有数据支撑的优化都是拍脑袋。写 Skill 这件事说到底是在模型和业务之间做翻译。翻译得好Agent 就像懂行的助手翻译得差再强的模型也只能瞎猜。我自己的体会是每写一个 Skill先问自己三个问题模型在什么情况下会需要它它需要哪些信息才能干活干完之后模型怎么知道干成了把这三个问题答清楚Skill 基本就立住了。剩下的就是在真实调用里一遍遍打磨直到它变成团队里那个“不用交代就能办好”的老手。