写 Agent 的开发者应该都有同感:模型能力再强,如果没有一套好用的"手",也只能是纸上谈兵。我第一次看到 agent-skills 这个命名时,第一反应是"这不就是把 Agent 工具又包装了一层吗",实际深入了解后才发现,它背后是一整套关于技能定义、注册、调度、执行的工程范式。一句话概括,agent-skills 要解决的是"智能体不只是会聊天,而是能稳定、可控地干活"这件事,核心就是把每个能力封装成独立技能,让 Agent 按需调用、自由组合。这篇文章我不聊虚的,直接讲清楚技能层为什么必须单独存在、技能规范怎么定、从零搭一套可用系统需要做什么,以及我实测过程中踩过的坑和最终沉淀下来的做法。
1. 拆解 agent-skills:设计定位与核心思路
1.1 为什么技能必须从提示词里独立出来
很多初学者写 Agent 时,习惯把所有能力说明都塞进系统提示词,比如"当你需要查天气时,调用天气服务的 API,参数是城市名称……"。这样写在小 Demo 里没问题,但一旦能力增加到十个以上,问题立刻暴露:提示词越来越长,模型注意力被稀释,经常该调的工具不调,不该调的乱调,而且每次新增能力都要动提示词,测试成本成倍上升。
agent-skills 的核心思路恰恰是把这些"能力描述"从提示词里彻底剥离,形成独立的技能层。每一个技能拥有自己的定义文件,包括名称、描述、参数结构、执行逻辑。Agent 在运行时先看技能目录,再决定调哪个技能。这样做最直接的好处有三个:第一,提示词变短,模型行为更稳定;第二,技能可以独立测试、独立迭代,不影响其他功能;第三,技能可以复用,同一个技能在不同 Agent 里都能挂载。
这个解耦思路,其实和软件开发里的"模块化""插件化"一脉相承。你写后端时会封装 Service 层,不会把业务逻辑全堆在 Controller 里,Agent 的技能层就是扮演这个 Service 的角色。理解了这一点,后续所有设计决策都有了一条主线:一切为了可复用、可观测、可独立演进。
1.2 技能、工具、插件的边界在哪里
很多人分不清 agent-skills 里的"技能"和市面上常说的"工具调用""插件"有什么区别,我在做方案评审时也反复被问过。其实不用把概念搞得太玄,它们的关系可以理解为不同抽象层级。
工具(Tool/Function)是最底层的能力单元,通常对应一个具体函数或 API 调用,比如"计算两个日期差""调用某个接口查快递"。插件(Plugin)通常是工具的集合,围绕某个场景打包,比如"电商助手插件"内含商品查询、订单查询、物流追踪三个工具。而技能(Skill)在 agent-skills 体系里,往往是更高一层的抽象,它不只是单一调用,还可以包含调用策略、前置条件、后置处理,甚至可以在内部编排多个工具。
举个例子,天气查询是一个工具,"出门建议"是一个技能。技能内部可能先调天气工具拿到温度和降水概率,再结合时间规则生成穿衣建议,最后把结果格式化返回。对 Agent 来说,它只需要理解"出门建议"这一个入口,而不需要关心内部调了哪几个工具。这就是技能层的价值:把复杂度藏在内部,把简单暴露给模型。
1.3 技能设计的三条基本原则
踩过几轮坑之后,我总结出设计技能必须遵守的三条原则,缺一不可。
第一是"单一职责"。一个技能只做一件事,不要把"查天气+写日报+订会议室"揉在一个技能里。技能粒度越细,Agent 越容易理解什么时候该用哪个技能,组合起来也更灵活。
第二是"描述即文档"。在 LLM 场景下,技能描述就是给模型看的说明书,模型判断"该不该用这个技能"几乎完全依赖描述文本。所以描述必须写清楚适用场景、边界条件、典型示例,而不是写实现细节。
第三是"失败要可观测"。技能执行必然有失败场景,比如参数错误、外部服务超时、结果格式异常。设计时就要约定好错误码和错误信息格式,让 Agent 能根据错误信息自行修正,也让开发者能在日志里快速定位问题。
2. 技能定义规范与核心实现细节
2.1 技能描述文件:一份给模型看的说明书
一个技能最核心的资产就是它的描述文件。我目前常用的结构是 YAML 格式,因为可读性好,非开发者也看得懂。描述文件主要包含几个部分:技能 ID 和名称、用途描述、参数定义、内部执行逻辑或者工具调用序列、返回结果格式说明。
下面的示例展示了一个"会议室预订"技能的定义:
skill: id: meeting_room_booking name: 预订会议室 description: > 当用户需要预定会议室时使用本技能。 适用条件:用户明确提到预订、预定、占坑、借用会议室等需求。 不适用条件:用户只是询问会议室位置或设施,此时应使用查询类技能。 parameters: - name: room_type type: string enum: [small, medium, large] required: true description: 会议室规格,small 容纳 2-4 人,medium 容纳 5-10 人,large 容纳 10 人以上 - name: start_time type: string format: datetime required: true description: 会议开始时间,格式 YYYY-MM-DD HH:mm - name: duration type: integer default: 60 description: 会议时长,单位分钟 workflow: - step: query_free_rooms tool: room_service.search input: {room_type: $room_type, start_time: $start_time, duration: $duration} - step: book_room tool: room_service.book input: {room_id: $query_free_rooms.result.room_id} condition: $query_free_rooms.result.available == true注意 description 字段的写法,它不是写给开发者的,而是写给模型看的,所以要明确"什么情况用""什么情况不用",这就是前面说的描述即文档。我在实际测试中发现,把描述写得具体一些,Agent 的决策准确率能提升一大截。
2.2 参数声明和输入校验:别让模型裸奔
模型从用户的话里抽取参数,天然会出错。比如用户说"帮我订明天下午三点的会议室",模型可能把"明天下午三点"解析成各种格式。所以技能参数设计一定要包含几类约束:必填还是选填、类型是什么、取值范围或枚举、格式模板。最好还要提供默认值和缺参时的人工兜底策略。
我的习惯是每个技能内部做两层校验:第一层是结构校验,参数类型、枚举值、必需项是否齐全;第二层是语义校验,比如开始时间不能晚于结束时间、预约人数不能超过会议室容量。校验失败时不要直接抛异常,而是返回一个标准化的参数错误对象,里面包含错误字段名、期望格式、示例值,让 Agent 能根据提示重新询问用户或者自己修正参数。
这里有个容易被忽视的细节:技能层校验通过的输入,不一定是模型真正需要的输入。比如日期时间这类参数,模型可能会输出"2026-03-15 15:00:00"这种格式,但业务系统只接受 Unix 时间戳,那应该在技能层做一次转换,而不是把格式责任推给模型。技能层作为"翻译官",负责把模型语言转换成系统语言,这一步做扎实了,后面的执行才能顺畅。
2.3 技能组合与编排:从小积木到大能力
单个技能解决单点问题,但真实场景往往是复合的。用户说"帮我订一间能容纳十人的会议室,顺便通知项目组所有人",这里至少涉及两个技能:订会议室、发通知。agent-skills 体系里,技能组合有两种主要方式。
第一种是"顺序编排",在技能的 workflow 字段里显式声明步骤,每一步输出都可以变成下一步的输入,我用上面的 YAML 示例就是这个模式,典型的使用场景就是订会议室之后再同步日历。
第二种是"动态编排",即让 Agent 自己决定调用顺序。这种模式适合开放式任务,比如"帮我安排一场项目启动会",Agent 可能会先调用"查询参会人空闲时间",再调用"预订会议室",再调用"创建会议邀请"。但动态编排的可靠性弱一些,必须给 Agent 提供清晰的技能描述和错误恢复机制,否则一旦中间某一步失败,整个链路就断了。我的建议是:固定流程用顺序编排,开放流程才用动态编排,能写死的流程不要依赖模型临场发挥。
3. 实操过程:搭建一套可用的技能系统
3.1 技能目录结构与注册中心
动手搭建前,先把目录结构定好。我用的结构如下:
agent-skills/ ├── registry.yaml # 技能注册表(所有可用技能的索引) ├── skills/ │ ├── meeting_room_booking/ # 一个技能一个文件夹 │ │ ├── skill.yaml # 描述文件 │ │ └── executor.py # 执行逻辑 │ ├── weather_query/ │ │ ├── skill.yaml │ │ └── executor.py │ └── ... └── runtime/ ├── loader.py # 技能加载器 ├── executor.py # 技能执行器 └── context.py # 共享上下文管理registry.yaml 是大脑,Agent 在运行时只读取这个文件来了解自己有哪些能力。它维护的是"技能名 -> 技能目录路径 -> 版本号"的映射关系,加载器启动时会扫描这个注册表,把所有技能的描述信息加载到内存中供模型调用。新技能上线不需要重启整个服务,只要更新注册表并触发热加载即可。
3.2 技能执行器与共享上下文
执行器是技能的调度中心,它负责接收模型传来的技能名称和参数,找到对应的执行函数,完成参数校验、执行逻辑、结果返回。我的执行器采用了一个统一的 AbstractExecutor 基类,每个技能继承后实现自己的 run 方法。
class AbstractExecutor: def validate(self, params: dict) -> dict: """参数校验,返回标准化错误或清洗后的参数""" raise NotImplementedError def execute(self, params: dict, context: dict) -> dict: """执行技能逻辑,返回标准化结果""" raise NotImplementedError共享上下文 context 是这个系统里比较关键的设计。很多技能在执行时需要公共信息,比如当前用户身份、当前时间、会话历史摘要。这些信息如果每次都要模型传参,既浪费 token 又容易出错。我的做法是:上下文分为两层,全局上下文在服务启动时初始化,比如当前地区、默认语言、服务调用凭据;会话上下文按用户会话隔离,比如用户 ID、会话中已经提取到的偏好信息。执行器调用技能时会把这两层上下文合并传入,技能内部只需要声明自己需要哪些上下文字段,不需要关心数据从哪里来。
3.3 一个完整技能从编写到上线的全流程
纸上谈兵没意思,我拿"工资查询"这个高频率技能为例,完整走一遍开发流程。
第一步,明确需求。用户会问"这个月发了多少钱""十月的补贴是多少",这类问题核心是查工资单和补贴明细。我拆成三个技能:查基本工资、查补贴明细、查实发金额,因为模型根据用户问法会选不同的技能入口。
第二步,写描述文件。这是关键,描述决定了模型能不能在正确时机选中它:
skill: id: salary_query name: 查询工资明细 description: > 当用户询问本月或历史月份工资、薪资、税后金额、补贴明细时使用。 用户问法示例:"我这个月工资多少""十月发了多少钱""补贴包含哪些项"。 如果用户问的是考勤、休假、报销,不要使用本技能。 parameters: - name: month type: string format: YYYY-MM required: false description: 查询月份,默认为当前月份。注意用户说"上月"时需要向前推算一个月注意"补贴包含哪些项"这类问法,它其实不是标准工资单查询,但语义上是技能范畴,我把它写进适用示例,就是告诉模型这个技能也能回答补贴结构问题,实测效果不错。
第三步,写执行逻辑。调用薪资系统的接口,把模型传来的月份转换成业务系统需要的格式,请求数据,过滤敏感字段,最后把结果包装成结构化返回。这里有一个重点:返回给模型的内容不要包含内部字段名,要用自然语言拼接,让模型能直接念给用户听。
第四步,注册与测试。把技能加进 registry.yaml,写单元测试覆盖正常参数、缺参、非法月份三种情况,然后用几组真实用户问法跑一遍全链路,确认模型能正确选中技能。上线前我还会跑一轮"负样本测试",故意用无关问题测试,确认模型不会误调用。
4. 常见问题与排查实录
4.1 典型问题速查表
系统跑久了一定会遇到各种奇怪问题,我把频率最高的几类整理成了速查表,遇到问题先按表排查。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型始终不调用某个技能 | 技能描述与用户问法语义差距大 | 查看模型决策日志,看哪些技能参与了排序 | 重写描述,加入真实用户问法示例 |
| 模型频繁误调用某个技能 | 描述里的适用范围写得太宽 | 检查触发日志,统计命中场景 | 收紧描述,补充"不适用条件" |
| 参数总是抽取不完整 | 参数缺少默认值或描述不清晰 | 查看参数抽取阶段的日志 | 为必填参数补充示例值,增加默认值兜底 |
| 技能执行成功但结果错误 | 上下文传入错误或参数转换有误 | 对比输入参数与业务系统实际接收值 | 在技能层增加日志,记录入参和出参 |
| 多个技能互相冲突 | 技能职责边界模糊,多个技能都能响应用户问题 | 分析技能决策热力图 | 明确技能边界,把冲突场景合并到单一技能 |
有一个很有代表性的案例:某次上线后,用户问"我在哪一层办公",模型居然调用了会议室预订技能,因为会议室技能描述里有"地点"这个词。后来我把技能描述里的地点相关表述全部删除,并在会议室预订技能的"不适用条件"里明确写了"用户询问办公位置时不要使用本技能",问题立刻消失。这个教训说明,模型是词面理解的高手,但语义边界完全靠你给它圈定,每一句描述都在画边界。
4.2 实测心得与避坑建议
最后分享几个我积累的实操心得,这些不是文档里能查到的,都是真金白银踩出来的。
关于描述文件,我强烈建议把"用户可能怎么说"写成示例放进描述里。纯抽象描述对模型不友好,比如"查询工资"远不如"用户问法示例:这个月工资多少、十月发了多少钱、年终奖什么时候到账"有效。模型见过相似的表达,决策才会稳。
关于参数校验,不要相信模型第一次给的参数就正确。时间、日期、金额这类参数,模型经常理解错,比如"下周三"到底指哪天,不同时候问结果可能不同。技能层必须做语义校正,不能只做格式校验。我一般会把时间解析逻辑写清楚,用当前日期作为基准推算,而不是把"下周三"这样的字符串直接传给业务系统。
关于上下文传递,最隐蔽的坑是共享上下文被多个技能并发修改。某次我发现一个用户查工资时,另一个会话的查询结果串进来了,排查了半天才发现是全局上下文里存了上一个会话的查询结果。后来我强制规定:会话级数据一律按会话 ID 隔离,技能内部产生的临时数据不得写入全局上下文。
关于技能数量,一个常见误区是能力越多越好。实际上技能超过 20 个之后,模型的决策准确率会明显下降,因为每次任务都要从大量候选中挑选。我做过一组对比实验:技能描述总体量相近的情况下,10 个技能时的决策准确率比 25 个技能时高十个百分点以上。所以每隔一段时间要主动做技能精简,合并低频率技能,删除重复技能,让目录保持精简。
关于热加载,技能更新频率高的时候一定要有版本概念。我踩过一版更新后跑了一半旧逻辑、一半新逻辑的坑,因为 executor 文件被部分刷新了。现在所有技能都带版本号,注册表里只记录最新版本,加载器会对每次加载做完整校验,校验不过就回滚到上一个版本,宁可失败一次也不带病运行。
我个人在实际操作中体会最深的一点是:agent-skills 的工程难点不在写执行逻辑,而在定义边界和做决策优化。技能描述写得好,Agent 就像一个训练有素的员工,你说一句话它就知道该动哪只手;技能描述写得差,再强的模型也会手忙脚乱。所以每次新增或修改技能,我都要求自己至少花一半时间在描述上,而不是急着写代码。这个方法我推荐你也试试,先不管实现,把"这个技能解决什么问题、什么场景不该用它"写清楚,再动手,整个系统会稳很多。