最近两年,身边讨论“Skill”的人越来越多。在智能体、助手工具甚至自动办公场景里,Skill 已经成了很有意思的能力单元——你可以理解为给智能体装上的一块乐高积木:一个负责查快递,一个负责写周报,一个负责提醒喝水。很多人觉得这东西很专业,得懂编程才能写。但我的观点恰恰相反:写 Skill 的门槛比你想象的低得多,真正难的,不是技术,而是你有没有把一个需求想清楚。我自己从写废几十个 Skill 到能稳定地产出高质量 Skill,经历不算曲折,但每一步都很有代表性。这篇就专门聊聊,怎么让人人都能写出优秀的 Skill。
1. 先定义清楚:什么样的Skill才配叫“优秀”
在讨论“怎么写好”之前,得先统一一个标准,否则大家吵半天,标准都不一样。我见过很多人把“能跑通”当成优秀,这其实是底线,不是上限。
1.1 一个Skill的三层结构
最底层的Skill其实只有三层:
- 第一层是“身份层”:名字、描述、触发条件。它回答“我是谁、什么时候该用我”。
- 第二层是“协议层”:输入参数、输出格式。它回答“对方要给我什么,我该还给对方什么”。
- 第三层是“执行层”:具体的处理逻辑、异常分支、话术模板。它回答“我拿到东西后怎么办”。
这三层缺一不可。我见过很多初学者的Skill,名字和描述写得花团锦簇,但参数跟描述对不上;也见过参数定义得极其详细,但执行逻辑只有一句“调用模型自己理解”,导致结果完全不可控。
把这三层写清楚,Skill才算是有了骨架。写之前,先问自己:我这个能力单元,到底是做什么的?输入输出是什么?不满足条件时怎么处理?如果这三个问题都能用一句话回答,说明你已经跨过了第一道坎。
1.2 优秀Skill的四个特征
基于实际测试大量案例,我总结出好Skill的四个特征,可以用来自检:
第一,意图覆盖度高。用户用不同的说法提出同一类需求时,Skill都能识别。比如“帮我看看我的快递到哪了”和“查一下快递物流信息”是同一意图。如果一个Skill只能识别一种问法,那不算优秀。
第二,调用成本低。参数越少越好,用户不需要填写一堆表格。真正的好Skill是“只问我缺的那一个信息”,其他都能从上下文里推断。我见过一个“日程安排”Skill,居然要用户填优先级、提醒方式、重复周期、地点、参与人,结果几乎没有人愿意调用它。
第三,失败可预期。超出范围时,它不会胡说八道,而是明确告诉你“这个我不会”或者“请补充XX信息”。可预期的失败比无法预期的成功更重要。
第四,结构可复述。把它拿给另一个人看,那个人在30秒内能看懂它是干什么的。如果一个Skill只有写它的人能理解,说明描述不够清晰。
还有一个附加特征,就是可测试。也就是说,可以有一组固定的输入样本,每次修改后都拿同样的样本跑一遍,看结果是否稳定。这一点后文会展开。
1.3 我用来“3秒判断”的快速检验法
我在评审其他人的Skill时,一般只用3秒。方法很简单:把一个Skill的名字、描述、参数列表放在面前,假装自己是一个完全没参与编写的新用户,用三句话问自己:一看名字,我知道它帮我解决什么问题吗?二看描述,我知道什么时候我会用到它吗?三看参数,我知道每个参数该怎么填吗?
如果任何一个回答是“不确定”,这个Skill大概率还需要打磨。这个判断法不需要专业知识,人人都能掌握。别小看这3秒,它对产品质量的影响非常大。因为最终调用这个Skill的不只是人类用户,还包括智能体本身。智能体在对话中会基于描述判断“此时是否该调用Skill”,如果描述含糊,它可能在有更合适选择时不调用,或者在无关问题上去调用。
这个“3秒判断法”帮我筛掉了很多看着很棒、实际没用的Skill。你可以现在就拿出自己写的Skill试一遍,大概率能发现之前没注意到的问题。
2. 动手写之前:先把需求翻译成边界清单
很多人一上来就打开编辑器写描述、写参数,这是本末倒置。我在写同一个Skill前,至少会花10分钟做一件事:把需求“翻译”成一张边界清单。这张清单不需要给用户看,纯粹是给自己理思路用的。
2.1 为什么我总是建议先写边界而不是先写流程
原因是,模型的“模糊容忍度”比你想象的高,但“歧义容忍度”非常低。如果边界不清晰,它会自己在误解里找个方向跑,最后给你的结果南辕北辙。边界清单的价值,就是先把“哪些归我管、哪些不归我管”钉死,之后写流程时不用反复修改。
举个例子,你要写一个“每周工作总结”Skill。如果不先定边界,它可能看到任何跟“总结”沾边的请求都会响应,包括“总结一下这篇文章”。但如果边界清单里写了“只处理周/月工作总结,不处理文章总结”,这个问题就提前被规避了。
2.2 边界清单的五个要素
我常用的边界清单包含五个要素:
- 对象:这个Skill服务的主体对象是什么?比如“客服工单”“快递运单”“待办清单”。
- 动作:用户希望我最终完成什么动作?输出一份报告、更新一条记录,还是返回一个查询结果?
- 场景:触发场景包括哪些?排除哪些?比如“在工作时间,用户明确要求生成周报时触发,日常闲聊不触发”。
- 禁忌:绝对不能做的事。比如“不要预约过去的日期”“不要读取未授权的数据”。
- 默认行为:当条件不完整时,Skill默认做什么?是主动询问补全,还是采用默认值,还是直接放弃?
把这五个要素写下来之后,你写描述时心里就有底了。很多参数其实是在“默认行为”这一步确定的。比如“查天气”Skill,如果用户没提供城市,默认行为可以是“先获取定位”,也可以是“明确询问城市”。这个决策不写清楚,Skill就会时而询问、时而默认,表现极其不稳定。
2.3 用正反例把“说不清”的需求锁死
边界清单写完后,我还会做一件事:为每个Skill准备至少三个“肯定该管”的正例和三个“肯定不该管”的反例。不用多,但一定要真实。
比如“查快递”Skill:
- 正例A:“我的快递到哪了?”
- 正例B:“帮忙查一下这个单号:SF123456789”
- 正例C:“昨天买的手机发货了吗?”
- 反例A:“我想退快递”(这是另一回事,应该走退货流程)
- 反例B:“快递什么时候才能被签收?”(如果用户只是问常识,不该触发)
- 反例C:“你觉得这个快递站点是不是很坑?”(闲聊,不触发)
这些正反例不只是用来写Test Case,更重要的是可以反过来“检索”你的描述有没有歧义。哪怕模型最终理解能力的上限很高,你也要尽可能让下限不低。正反例其实就是给“下限”上的保险。
3. 普通人也能上手的五步写作法
如果你觉得上面那些有点抽象,那这一段可以直接照着做。我把写Skill的过程压缩成了五步,每一步都有明确产出。这五步基本是我在几十次迭代后形成的“肌肉记忆”。
3.1 第一步:给Skill起一个能“望文生义”的名字
名字要遵循“动词+名词”的结构,比如“查快递”“生成周报”“添加待办”。不要用“智能助手”“便捷工具”这种毫无信息量的名字,也不要用“查询功能V3”这种版本号当名字。名字是智能体判断是否调用它的第一环。名字起得好,后面描述的压力就小。
我见过一个叫“信息处理”的Skill,打开一看是专门做PDF转Markdown的。这个起名就失败了,因为“信息处理”太广义,到了触发阶段,模型很难把它跟PDF绑定起来。改名为“转换PDF为Markdown”之后,调用频率明显上升。
3.2 第二步:用两句话写好功能描述
描述不是越长越好。我推荐两句话结构:第一句讲“这个Skill做什么”,第二句讲“什么时候用、什么时候不用”。
举例:
- 第一句:根据用户提供的快递单号查询最新物流状态。
- 第二句:当用户询问快递物流、运单进度、包裹到哪时使用;不处理退货、投诉或快递价格咨询。
这两句话写完后,尽量保持在50字以内。别小看这个字数限制,它逼着你想清楚真正核心的东西。如果这两句话你憋了半小时都写不出来,说明你还没想清楚这个Skill是干嘛的。
3.3 第三步:参数越少越好,每个参数都要自解释
新手最容易犯的错,就是把所有可能用到的信息全部列成参数,恨不得做一张完整表单。但Skill不是表单,它是对话场景里的“快速助手”。多一个参数,就多一次让用户补充输入的机会,也就多一分流失率。
参数设计有三个原则:
- 最少必要参数:只保留“没有它就无法执行”的字段。
- 能从上下文推断的参数不设:比如时间,用户说“明天上午”,就不要让用户再填一次日期。
- 每个参数必须有说明和示例:方便智能体和用户共同理解。
下面这个例子可以当作参考:
Skill: 查快递 描述: 根据快递单号查询最新物流状态。 触发条件: 用户询问快递物流、运单进度、包裹位置;不处理退货和投诉。 参数: tracking_no: 类型: string 说明: 快递单号,通常由数字和字母组成 示例: SF123456789 执行逻辑: 1. 提取参数 tracking_no。 2. 调用物流查询接口。 3. 如果接口返回成功,将最近一条流转信息整理成一句话。 4. 如果接口无记录,回复“未查询到该运单,请核对单号”。这个结构非常简洁,任何人看一遍都能照抄。越是简洁,越容易被后续维护。要知道,Skill写出来不是一次性的,它会在半年后被你自己或者同事再次修改。如果参数定了一堆,任何一次改动都可能引起连锁错误。
3.4 第四步:把分支逻辑写进文字里,不用图也能表达
很多人一提到逻辑就想着画流程图。其实在Skill场景里,简单的分支用“如果/否则”句式就够了。我的经验是:前三个分支一定要写清楚,后面的分支能合并就合并。超过五六个分支就直接改成调用另一个子流程。
分支逻辑主要考虑两类:
- 正常分支:输入完整且可执行时,做什么。
- 异常分支:缺参数、查不到、用户反悔、超出范围时,分别做什么。
异常分支比正常分支更能决定用户体验。一个Skill如果只处理正常路径,就像一条没有路肩的高速公路,看起来很快,出一点意外就彻底瘫痪。
3.5 第五步:给“失败”留好退路
最后一步是写失败话术。这里说的失败不只是技术上的失败,还包括“Skill被错误调用”和“用户需求与Skill初衷不完全一致”的情况。比如用户本来只想问“快递能不能明天到”,你的查快递Skill却直接给了物流接口结果,这不算失败,但用户体验很生硬。
我通常会在执行逻辑里加一步判断:如果用户的问题明显超出本Skill范围,先回复“我可以帮你查物流状态,但无法估算具体到达时间”,而不是直接执行查询。这一步叫“能力边界声明”。边界声明写得好,用户会觉得很被尊重,而不是觉得自己被一个程序拒绝了。
五步写完,一个Skill的初稿已经完成了。接下来进入最折磨人也最提身的阶段:测试。
4. 我反复踩过的坑:写Skill时的五个隐蔽问题
下面这些坑,是我自己用实际交付换来的教训。每一条都很基础,但它们造成的后果往往要到量产后才爆发,修复成本极高。
4.1 把自然语言当编程语言,期待它绝对精确
有一次我写“查快递”Skill,为了省事,执行逻辑只写了“调用接口查询物流并回复用户,如果失败就提示用户”。结果测试时发现,每当用户提到“快递”两个字,无论后面跟什么,它都会触发查询。哪怕用户只是说“快递放门口了,谢谢”,它也会去调用一次查询接口。原因在于,我写的触发条件太宽泛,描述里没有明确“查询”这个动作。
后来我在触发条件里加了一条:“只有当用户表达‘查询/查看/到了吗/到哪了’等状态询问意图时触发;已送达的陈述、感谢、指令性短语不触发。”问题立刻缓解。这件事给我的教训是:自然语言描述虽然看起来是给人看的,但其实最终是给模型看的,你必须像写测试用例一样去校准它的边界。
4.2 默认值设计不当造成“静默失败”
这事发生在一个“今天是几号”的Skill上。我感觉这种问题简单,就把“回答日期”作为默认动作。结果用户问“今天星期几”,它也会回答日期,虽然没报错,但答案根本不符合需求。这就是典型的默认值把错误吞掉了。
正确的做法是,在默认行为里先判断“用户问的是日期还是星期”,如果两者都有可能,就应该追问。设计默认值的原则很简单:当信息不全时,宁可多说一句澄清,也不要替用户假设。因为假设一旦错了,用户对你的信任损失远比多一次对话大得多。
4.3 多个功能揉在一起,导致意图互相打架
我曾经把“查天气”和“查空气质量”揉进同一个Skill,以为它们是同一类需求。结果测试发现,当用户说“今天空气怎么样”,Skill会先查天气,然后附带一句空气质量,甚至有时候空气质量数据源失败,它就把整段回复变成“查不到”。用户根本不知道它是想查天气还是查空气。
后来我把它们拆成两个独立Skill,各自维护参数和执行逻辑,问题才消失。这里要记住一个原则:一个Skill只做一件用户能一眼看懂的事。如果一件事需要加“或者”才能说清楚,它的功能就太复杂了。
4.4 改了描述忘了改参数,版本悄然漂移
这个坑更隐蔽。有一次我对某个Skill的描述做了优化,把“根据用户选择的快递公司筛选物流信息”改成“自动识别所有快递公司”,但参数列表里的company字段完全没动。结果智能体在调用时,既会尝试从用户话里提取company参数,又不会传给接口,导致总是查询失败。
从那以后,我养成了一个习惯:每次调整描述或逻辑后,必须把参数列表完整过一遍,并把所有相关测试样本重跑一次。版本漂移通常不是故意的,而是因为我们只看到局部,忽略了全局。后来我引入一个简单的版本记录表,每次变更都记下一行。没有这个记录,你很难复盘到底是从哪一版开始变差的。
4.5 没有考虑上下文被清理后的表现
最后一个坑是关于多轮对话的。很多Skill依赖上下文里的信息,比如用户之前在“订酒店”流程里已经提供了城市,后续的“查天气”Skill就不该再问一次城市。但上下文是有上限的,一旦被清理或切换场景,Skill就会拿不到之前的信息。
我遇到的情况是,用户在订酒店时提到了“北京”,随后问“明天穿什么”,我的“查天气”Skill显然需要城市,但它直接用了默认城市,导致结果完全错误。后来我在所有依赖上下文信息的Skill里,都加了一个“信息缺失检查”分支:如果必须的城市参数不存在,就主动询问一次,而不是静默假设。这个分支虽然增加了对话轮次,但避免了更严重的错误。
五个坑讲完,你会发现它们都有一个共同点:都是因为“边界模糊”造成的。所以前面花那么大力气做边界清单,是非常值得的。
5. 怎么科学地知道自己的Skill好不好
好Skill不是写出来的,是改出来的。但怎么改?靠感觉不行,需要一套轻量级的度量方法。
5.1 五个比“感觉”靠谱的量化信号
我会持续关注五个信号:
- 调用成功率:Skill被调用后,是否在预期内完成任务并返回结果。
- 澄清率:遇到信息不全时,Skill是主动追问,还是直接瞎猜。高澄清率不见得是坏事,但如果高到用户反感,说明描述前摄性不够。
- 用户修正率:用户是否经常用第二句话纠正Skill的答案。例如用户问“明天去杭州穿什么”,Skill回复后,用户又补充一句“我是指在户外徒步,不是室内”。修正率越高,意图覆盖率越低。
- 复访率:用户是否愿意再次使用这个Skill。低复访率往往意味着体验不好。
- 可复用性:换一个场景、换一批用户,Skill是否还能正常工作。可复用性越强,说明你设计得越通用。
5.2 建立你的专属回归测试集
别等出问题再测试,建议在写Skill的当天就建立一个回归测试集。方法很简单:从之前准备的正反例里挑20条典型输入,包含5条正常输入、5条边界输入、5条反例输入、5条异常输入。每改一次就全部跑一遍。
回归测试集的价值在于:它能让你在发布新版本前,提前发现“改了A却坏了B”这类问题。我吃过不止一次这样的亏。有了测试集之后,每次改动的底气都足了很多。而且这套方法不需要任何自动化工具,即使你只是人工在对话框里逐条跑,也能发现大部分问题。
5.3 从失败记录里反推是描述问题还是逻辑问题
当一条测试输入失败时,不要急着改代码。先判断,失败发生在哪个层面:
如果失败发生在触发阶段,比如应该触发却没触发,或者不该触发却触发了,那多半是描述和触发条件的锅。
如果触发没问题,但结果不对,那多半是执行逻辑或参数映射的锅。
我见过很多人一看到结果不对就重写描述,结果把本来正确的触发条件也给毁了。正确的做法是,记录下失败输入、预期输出、实际输出、失败层,然后对症下药。
5.4 一个简单的定位表格
这里整理一个简单的表格,方便定位问题:
| 现象 | 可能原因 | 优先检查方向 |
|---|---|---|
| 该调用不调用 | 描述过于宽泛/触发条件过严 | 功能描述、分级触发词 |
| 不该调用却调用 | 描述中缺少边界 | 反例、排除条件 |
| 调用后返回错误结果 | 参数映射错误或默认分支缺失 | 参数列表、异常分支 |
| 总是要求澄清 | 想覆盖的意图过宽 | 拆Skill、收窄描述 |
| 回应生硬 | 缺少“能力边界声明”或兜底话术 | 失败话术 |
这张表不用背,把它贴在工位旁边也行。核心思路是:出现问题时,先定位到层,再动手改。很多人写Skill靠“手感”,但持续稳定输出靠的是“框架感”。度量本身就是框架感的一部分。
6. 写给普通人的进阶路径:从抄到创
最后,我想回到标题:“人人都能写出”。这句话不是鸡汤。我觉得只要路径正确,一个完全没写过Skill的人也能在很短的时间内做到。关键在于不要一上来就搞原创。
6.1 第一阶段:拆解你身边最好用的那个Skill
找一个你天天用的、体验特别好的Skill,把它当成教材。不要只看结果,要倒推它的设计:它解决什么问题?参数为什么只有这几个?它会在什么情况下拒绝你?它用什么样的语气回复你?
比如你可以把一个“周报生成”Skill拆开看:它是不是先问本周关键字?它是不是自动把上周内容全部清空?它回复你的格式是不是有固定模板?把这些点记下来,你会发现自己已经能看出门道了。
6.2 第二阶段:用固定模板做你的第一个原创Skill
别急着追求独特,先用模板写一个简单的,例如“查XX”“记XX”“提醒XX”。照着前面说的五步走:名字、两句话描述、最少参数、异常分支、失败话术。第一次写完之后,不要发出去,先用自己的测试集跑一遍。哪怕是人工跑,你也会发现描述里的模糊点,然后改掉。
6.3 第三阶段:做场景迁移和组合
当你能独立写出一个稳定Skill后,可以开始玩迁移和组合。把一个场景里的Skill搬到另一个场景,比如“查快递”的逻辑可以迁移到“查订单”或“查核酸报告”上。组合是更有趣的事,比如“天气Skill+穿衣建议Skill”可以合成一个“出行建议Skill”。组合时要注意,每个子Skill仍然保持独立,不要为了组合而把它们揉成一团。
6.4 第四阶段:建立你的个人技能库
写多了之后,你会发现很多逻辑是可以复用的。这时候可以把它们沉淀成模板、片段、测试集,形成自己的技能库。这个库的价值,比单个Skill要大得多。此后每次新写Skill,你都是在原有素材库上做增量,而不是从零开始。
我在建立个人技能库之后,写一个新Skill的时间缩短了大概三分之一。因为边界清单、参数规则、失败话术这些东西都是可以复用的,需要设计的只有真正差异化的部分。
最后再分享一个小技巧:写完Skill后先放一放,隔一天再以用户的口吻读一遍描述。如果连自己都读不懂,或者不确定它会怎么行动,那用户只会更困惑。很多我没把握的Skill,都是在这个环节被淘汰掉的。优秀Skill的门槛没有你想象的那么高,它更多是“把一件事想清楚”的能力。只要你能想清楚,你就能写出来。