ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent Skills 完全指南:从技能定义到智能体能力编排

Agent Skills 完全指南:从技能定义到智能体能力编排

最近在整理手头的智能体项目时,又翻到不少关于“agent-skills”的设计讨论。这个标题单独拎出来看有点抽象,但在实际工程里,它恰恰是决定一个 Agent 好不好用、能不能长期迭代的关键模块。简单说,agent-skills 就是给智能体配置的“可复用能力集”,不管是调 API、查数据库、读写文件、执行代码,还是做信息检索、多步骤规划,都会以“技能”的形式挂载到 Agent 身上。我见过不少团队把大量逻辑塞进系统提示词里,结果模型一换、场景一变就得重写;而把能力拆成明确的技能库之后,新增需求只需要加一个技能条目,整体可控性完全不一样。

这篇文章我打算从 agent-skills 的核心概念讲起,然后拆解技能分类、定义规范、注册调用、测试评估和常见坑位,最后聊一聊后续可以怎么扩展。无论你是在搭个人 AI 助手,还是在企业级项目里落地多智能体系统,只要涉及到“给 Agent 装能力”,这篇文章都值得你花十分钟读一遍。

1. agent-skills 到底解决什么问题

1.1 为什么不能把所有东西都塞给提示词

有段时间流行“一个 Prompt 搞定一切”的做法,把工具说明、任务流程、输出格式、边界规则全部拼进上下文里。短期 demo 确实能跑,但放到真实业务里问题一大堆。比如你给 Agent 配了 30 个工具的描述,你猜会发生什么?实测下来,模型的选择准确率会明显下降,尤其当描述写得含糊、长得相似时,Agent 会频繁地在“该调 A 还是调 B”之间摇摆。更麻烦的是,模型的上下文窗口是固定的,你塞进去的静态文本越多,留给真实对话和中间推理的空间就越少,回答质量直线下滑。

agent-skills 的思路恰好是反过来的。它把“模型需要知道的提示性信息”和“模型实际能调用的能力”做了解耦。系统层只负责加载技能清单,每个技能自带名称、描述、参数 schema、执行逻辑和返回结果格式。Agent 在运行时才动态感知这些技能,按需调用。这个设计带来的直接好处是:新增能力不用改提示词,删除能力也不用动业务流程,所有技能都是一等公民,用注册机制管理。

1.2 技能化给智能体带来的结构性收益

把能力做成技能,远远不止“整洁”这么简单。第一,技能本身是可测试的单元,每个技能都能单独压测、单独加指标,出问题不用在整个 Agent 流程里大海捞针。第二,技能是可组合的,复杂任务可以被拆成子技能的编排,比如“写周报”可以依赖“查日历”“读项目文档”“生成 Markdown”三个技能,任何一个都能单独替换。第三,技能天然适合做权限控制。企业级场景里,某些技能只能特定角色调用,或者只能在指定环境执行,技能注册表就是一个很自然的管控边界。

我自己还特别看重一点:技能是可观测的。以前追一条 Agent 的决策链路,得去看完整对话日志,费时费力。技能化之后,每一次调用都有独立记录,包括输入参数、输出摘要、耗时、错误信息,问题定位从“猜模型在想什么”变成了“查某个技能的执行记录”。这真的能救命。

2. 技能分类与边界划分

2.1 从能力形态上区分四类技能

技能放在一起,如果不做分类,很快就会变成一个大杂烩。我习惯按能力形态分成四类:

  • 工具类技能:负责与外部系统交互,比如 HTTP 请求、数据库读写、云存储操作、第三方 API 调用。
  • 计算与执行类技能:负责跑代码、算数据、转换格式,比如 Python 执行、公式计算、文件格式解析。
  • 检索类技能:负责从本地或远程知识源获取信息,比如向量检索、文档搜索、网页抓取、SQL 查询。
  • 认知与编排类技能:负责辅助模型完成推理和任务规划,比如任务拆解、信息汇总、决策建议、多步骤状态维护。

这个分类并不严格,但能帮你在设计阶段想清楚“这个能力到底该由谁来负责”。工具类技能强调稳定连通,执行类技能强调安全可控,检索类技能强调召回质量,编排类技能强调状态一致性。每个类别的优先关注点不一样,后面的测试策略也不一样。

2.2 技能粒度应该怎么拿捏

技能粒度是个很玄学的问题,拆太细会导致技能数量爆炸,Agent 选技能时像在逛超市;拆太粗又会导致单个技能内部逻辑过重,复用性变差。我的经验是,用“任务原子性”来判断:如果一个技能完成之后,输出结果可以直接被用户或其他技能使用,而不需要二次加工,那粒度就差不多了。

举个例子,“查天气”是一个合适的技能粒度,而“连接天气服务”“解析天气 JSON”“格式化天气报告”这种拆分就过度了。反过来,“处理数据”这种描述就太粗了,模型根本不知道入口是什么,参数也不知道该传什么。我通常会写技能时问自己一句话:如果给一个完全不懂内部实现的开发同事用,他看了描述和参数能不能直接调用?能,说明粒度合适。

2.3 技能命名与描述的艺术

技能描述是最被低估的环节。几乎所有 Agent 效果翻车的案例里,技能描述写不好都是核心原因之一。模型选择技能时靠的是语义匹配,描述里没有关键信息,或者多个技能描述高度重叠,Agent 就会“左右为难”。

描述里要包含几个要素:功能边界(做什么、不做什么)、适用场景(什么情况下调用)、关键约束(比如需要哪些前置条件、有哪些副作用)、输出说明(返回什么形态的数据)。不要写“该技能用于处理各种与用户相关的操作”这种废话,模型看了等于没看。要写“根据用户提供的用户 ID 查询用户基本信息,含昵称、头像、注册时间,适用于用户资料展示场景;若需查询消费记录请使用另一个技能”。

数据类技能还要在描述里写明“入参来源”。我见过一个搜索技能,描述里没有说明 query 参数该从对话的哪一部分抽取,结果 Agent 经常把整段对话历史塞进 query,既浪费 token 又拉低召回。实际上只需要加一句“从用户当前问题中提取核心检索意图,去除问候语和无关修饰”。

3. 技能注册与调用的标准工程实践

3.1 用 JSON Schema 定义技能入参和出参

一个技能要能被模型稳定调用,结构化定义是底线。前后端联调时大家都会写接口协议,给 Agent 用其实是一样的道理,甚至要求更严格,因为你没有“双方反复对齐”的机会,模型只读一段描述和一份参数定义,就得做出调用决定。

所有技能我都会用 JSON Schema 来描述参数。它表达能力强、工具链全、模型对它的理解也很成熟。下面这个例子是以“创建待办事项”技能为例:

{ "name": "create_todo", "description": "创建一个新的待办事项。适用于用户说“记一下”“提醒我”“添加任务”等场景。创建成功后返回完整的待办项 ID 和状态。", "parameters": { "type": "object", "properties": { "title": { "type": "string", "description": "待办事项的标题,必须简洁明确,不超过50字" }, "due_date": { "type": "string", "format": "date", "description": "截止日期,格式 YYYY-MM-DD,可选" }, "priority": { "type": "string", "enum": ["high", "medium", "low"], "description": "优先级,可选,默认 medium" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "标签列表,可选" } }, "required": ["title"] } }

注意这里的 description 为什么写得那么细。模型不会自己去猜 title 怎么取,也不会自己判断 due_date 格式,你写得越清楚,它生成的参数就越规范。我在实际项目里统计过,加入细致字段描述后,参数校验失败率降了 60% 以上。

3.2 技能注册中心和动态加载机制

技能不能散落在代码各个角落,需要一个统一注册中心。核心是维护一张技能元数据表,记录每个技能的名称、版本、描述、入参 schema、执行入口、依赖的资源、超时设置、权限角色等。运行时模块周期性地从注册中心拉取技能清单,构建成模型可读的工具列表。

推荐的做法是,Agent 启动时只加载当前会话需要的技能子集。比如一个只处理数据报表的助手,不需要把“日历管理”技能挂上去。倒不是说挂上去会出错,而是技能太多会稀释选择注意力,还可能引发误调用。这就像你桌面上的图标堆了一百个,找个应用反而变慢了。技能也一样,保持精简,按场景动态增删。

动态加载还有个好处是能热更新。技能升级了,注册中心更新元数据,新会话立刻用新描述;老会话还在用旧描述也不受影响,这比把工具清单硬编码在代码里优雅得多。

3.3 技能执行的统一封装与错误处理

技能的执行业务逻辑可以千差万别,但执行外壳必须统一。我习惯用一个 Python 类把所有技能包一层,接口收敛成 load、validate、invoke、format 四个阶段。validate 阶段做参数校验,invoke 阶段跑具体逻辑,format 阶段把结果整理成模型友好的文本。

错误处理是最容易被忽略的环节。技能抛异常不能直接把堆栈丢给模型,模型看到一堆 Traceback 只会一脸懵。设计规范是:捕获异常后,把错误信息转成简短、可行动的文字,比如“数据库连接超时,请稍后重试”“用户 ID 不存在,请检查输入”。更关键的是,要区分“永久性错误”和“临时性错误”,前者让模型直接告诉用户结果不可行,后者可以让模型主动重试或者建议用户再试一次。我见过不少 Agent 因为不区分这两种情况,在错误里反复打转,白白浪费时间和 token。

3.4 技能返回结果的上下文压缩

技能执行完,返回内容很可能是一大段 JSON、一长篇文章或者几十条数据。如果原封不动地塞回上下文,很快就把模型窗口撑爆。所以在 format 阶段要做上下文压缩,核心思想是:保留决策必需的信息,去掉展示无关的细节。

比如查出来 50 行订单数据,真正让模型判断“该不该提醒用户补款”的可能只需要总金额、逾期天数和订单状态。返回给模型的就应该是这些精简信息,完整明细可以通过另一个“查询订单详情”技能按需获取。还有一种常用手法是分段摘要:把长文本切成若干块,先让模型对每块做摘要,再把摘要合并。虽然多了一轮调用,但在处理超长文档时非常划算。

4. 技能质量的测试与评估机制

4.1 单一技能的单元测试怎么做

技能本质上是一个独立功能模块,那就应该用单元测试的思路去收口质量。每一个技能至少覆盖三类用例:正常路径、边界输入、异常场景。以“获取用户订单列表”为例,正常路径是传合法用户 ID 返回订单数组;边界输入是传空字符串、不存在的 ID、超大整数;异常场景是服务端超时、返回畸形 JSON、权限不足。

这些用例不要只写在测试文件里,还要沉淀成“技能回归集”。因为技能的描述或参数 schema 一旦调整,整个 Agent 对技能的理解都可能变化。跑一遍回归集,能很快发现哪个技能的描述改坏了。真实项目里,我见过一个“改描述导致 Agent 再也不调用该技能”的诡异问题,排查了半天,最后发现是描述里把“适用于”改成“应用于”之后,模型对场景的语义匹配就偏了。玄学归玄学,但回归测试确实是挡住这类问题的最好手段。

4.2 技能组合的集成测试与端到端演练

技能单独没问题,不代表组合起来没问题。集成测试关注的是技能之间的数据流和调用顺序。很多多技能任务是有顺序依赖的,比如“先查日历,再排会议,最后发通知”,一旦 Agent 把顺序搞反,或者中间漏了某个技能,整个流程就断了。

集成测试我会用一个固定的评估集,里面是几十个典型的业务问题。比如“帮我看看这周有哪些空档,安排一场产品评审会,邀请 A、B、C 三人参加,并给所有人发邮件通知”。评估时看三个指标:技能选择的正确率、参数传递的完整率、最终任务的完成率。完成率这个概念比较主观,所以一定要先定义清楚“什么才算完成”。预约成功、通知发出、有明确的会议主题和时间,都满足才算一次完整通过。

4.3 线上可观测性和技能调用日志分析

技能上了线,观测比测试更重要。技能调用的日志至少要包含几类信息:触发场景(用户的原始问题)、调用技能名、输入参数、输出摘要、耗时、错误类型、是否需要重试、是否由模型主动发起。有了这些,就能做很多有意思的分析。

我最常看的一个指标是“技能召回率”,也就是“该调用的时候有没有调用”。如果用户问天气,Agent 却答非所问,那大概率是技能描述与用户表达之间存在语义鸿沟。另一种情况是“误调用率”,不该调用技能时调了,比如用户只是闲聊“今天天气真不错”,Agent 却真的调了查天气的接口。这种问题通常靠调整描述里的“适用场景”和“不适用场景”来解决。线上一旦发现这些偏差,要及时回到注册中心调整描述,而不是重新训练模型,成本完全不在一个量级。

5. 常见问题与排查技巧实录

5.1 模型就是不调用某个技能,怎么办

这是最常见的问题。先别急着怀疑模型能力,按顺序排查:技能是否在会话的技能清单里?技能描述里的关键词和用户的问法是否匹配?技能是否被其他更“显眼”的技能覆盖了?

我用过一个“笨办法”:在技能的 description 里加上“当用户提到 XX 关键词时使用本技能”这种显式触发条件。虽然不太优雅,但实测非常有效。另一个技巧是把常用技能的顺序排在前面。虽然大模型不是按照顺序硬选技能,但排前面的技能在注意力上天然占优势。还有一个思路是把低频技能入口藏起来,引导模型先问用户意愿,再动态加载对应技能,降低一次性选择的复杂度。

5.2 技能参数总是抽不准

参数抽不准,通常是因为描述里没写清楚“参数值从哪里来”。模型需要一个“搬运指引”,比如“将用户提到的日期解析为 YYYY-MM-DD 格式”“如果用户没有明确说明截止时间,不要填写 due_date”。很多参数抽不准,其实是模型不知道该从上下文哪里取、取到什么粒度。

解决方式是在每个参数的 description 里都写明“数据来源”和“解析规则”。多花二十秒写清楚,比上线后反复修漏洞省事太多。另外,对于枚举类参数,一定要把可选枚举值写全,比如“提醒方式:应用内推送、短信、邮件”,模型才会在框架内选择,而不是自己造一个。碰到模型经常性填错值的情况,可以加一步“参数标准化”后处理,把模型输出映射到严格枚举上。

5.3 技能执行正常但用户觉得答非所问

这类问题最隐蔽,锅往往不在任务执行层,而在把执行结果转成自然语言回答的环节。模型拿到了正确数据,但不知道用户真正关心什么,就会把次要信息放在回答前面。比如用户只想知道“快递什么时候到”,技能返回了包裹轨迹明细,结果回答先讲了一堆“您的包裹已从广州分拨中心发出”,重点完全错位。

给模型返回数据时,最好附带一个“告知策略”:告诉模型用户的核心诉求是什么,应该优先汇报哪些字段,哪些信息如果用户不追问就不用展开。这相当于给模型画了一条回答的路标,效果显著。另一种情况是技能返回结果太多,模型没有做取舍,把所有内容都念了一遍。这时候要回到上下文压缩环节,从源头减少返回的字段。

5.4 技能间的权限冲突和资源争抢

企业级场景下,一个 Agent 往往挂着多个技能,背后连接着不同的系统,权限模型也可能不一致。最常见的问题是“技能 A 能拿到数据,技能 B 拿不到”,但 Agent 并不清楚这个差异,导致它在编排时用了 B 去尝试访问 A 的数据,然后报错。

解决思路是在技能的元数据里增加权限域的字段,比如“该技能仅能访问用户本人数据”“该技能仅限企业内部网络”。定义好之后,Agent 在规划阶段就能避开不可达的资源。另一个经验是给每个技能设置独立的超时和重试策略,避免一个慢技能拖垮整个 Agent 的响应时间。我用过的服务里,不同技能的超时从 2 秒到 30 秒都有,统一用一个值反而容易出问题。

5.5 技能更新的灰度与回滚机制

技能不像代码那样有明确版本边界,它的一部分行为在模型的选择逻辑里,一部分在执行代码里,灰度起来很别扭。我的做法是:技能描述和参数 schema 都打上版本,注册中心里保留历史版本,新会话默认用最新版本,但可以通过配置强制指定某类会话使用特定版本。

回滚就更简单了,直接把注册中心里生效版本指回上一个。不要小看这个机制,在一次技能描述调整引发大面积误调用事故时,能一键回滚就是救命稻草。执行代码的灰度也是一样,新逻辑先只放给一定比例流量,观察调用成功率、耗时和用户反馈,没问题再全量放。技能改动看起来是小改动,但它直接影响模型行为,必须当生产事故来对待。

6. agent-skills 的后续扩展方向

技能库成型之后,下一步往往是让技能具备自我进化的能力。一个比较实用的方向是“调用结果反馈闭环”:每次技能调用后,记录用户的后续行为,比如用户是否纠正了结果、是否追问了更多信息、是否直接复制了回答去使用。这些信号聚合起来,就能给每个技能打一个“实际效用分”,指导后续的技能优化优先级。

另一个方向是“技能的跨 Agent 共享”。不同业务线的 Agent 常常需要用到相同的原子能力,与其每个 Agent 各维护一套技能,不如统一建设一个组织级技能市场。团队 A 开发了一个好用的“智能排期”技能,发布到市场,团队 B 直接订阅就能用,前提是做好权限隔离和版本兼容。这个模式在稍大一点的团队里价值非常明显,能省掉大量重复劳动。

再往远了说,技能描述和模型之间的适配也有优化空间。有人开始尝试用“技能选择的偏好学习”来替代手工写描述,通过历史调用数据训练一个小型模型或者微调排序器,让 Agent 更精准地判断什么时候该选哪个技能。这条路还在早期,但我认为是一个值得关注的方向。毕竟,技能的终极形态不只是“能被调用”,而是“知道什么时候该被调用”。

我在实际项目里的体会是,agent-skills 设计得越清晰,整个智能体系统的确定性就越强。AI 本身有很大的不确定性,但我们可以通过工程手段,把尽可能多的环节变得可描述、可测试、可回滚。技能的注册化、结构化、可观测化,就是这个思路里最重要的一块基石。如果你也在搭 Agent,我建议从今天开始,把散落在提示词里的工具描述整理成一个真正的技能库,你会立刻感受到差别。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进