☰
Agent Skills 设计指南:从工具调用到可组合技能单元的工程实践
2026/9/25 5:37:47 网站建设 项目流程

最近在折腾 Agent 应用落地,团队里聊得最多的一个东西就是 agent-skills。我们自己的项目从最开始“一个 prompt 里塞一堆工具定义”,慢慢进化到把每个能力拆成独立 Skill 来管理,中间的弯路和踩坑还真不少。这篇就结合我自己实际在项目里拆 Agent 技能的经验,聊聊 agent-skills 是什么、怎么设计、怎么落地,以及真正跑起来之后会遇到哪些坑。

1. Agent Skills 到底是什么,为什么大家都在聊

先明确一下概念。Agent Skills 说白了就是把大模型 Agent 能执行的某项具体能力,单独拆出来做成一个标准化的功能模块。比如让 Agent 能查数据库、能写代码文件、能操作浏览器、能读 PDF,这些都是不同的 Skill。以前我们习惯把这些能力全部堆在 Function Calling 的工具列表里,只要模型能调、能传参数就行。但随着场景越来越复杂,这种一把梭的做法会迅速失控。

我自己最早的项目就是典型反例。当时给一个数据分析 Agent 配了十几个 function,包括查 MySQL、查 ClickHouse、读 Excel、调外部 API、发邮件……所有工具定义全放一个文件里。初版跑起来还行,因为模型只需要从十几个工具里挑一个调用。可后来业务方说“还要支持分析结果自动生成周报”,于是又加了生成 PPT、写飞书文档、拉取日历这几个 function。这时候模型开始频繁选错工具,明明只需要查一下昨天的订单量,它偏偏去调了“生成周报”的工具,把流程带偏。排查半天,发现是工具描述写得不够清晰,而且工具之间职责重叠,模型根本分不清边界。

这就是 agent-skills 要解决的问题。它不只是一个“工具封装”,而是一整套“能力单元”的设计理念:每个 Skill 有明确的职责边界、清晰的输入输出契约、独立的错误处理逻辑,甚至可以有自己单独的一套 prompt 提示词。它的核心价值在于让 Agent 的能力变得可组合、可复用、可测试,而不是一个大杂烩。

我把这个思路跟组里同事聊的时候,打了个比方:以前是把所有工具像螺丝刀、扳手、电钻一样全扔在一个抽屉里,模型要用的时候自己翻;Skills 的玩法是给每个工具配了一个带说明书的收纳盒,还贴好了标签——这个盒子只装内六角螺丝刀,那个盒子只装十字螺丝刀,模型一眼就能找到该拿哪个。

另外,agent-skills 和普通的 Function Calling 有个非常关键的区别:Function 通常只描述“能做什么动作”,而 Skill 往往还包含“怎么把这件事做好”的知识。举个实际例子,我的项目里有一个“查询商家经营数据”的 Skill,它不止是一个 execute_sql 的函数,它内部还包含了该优先查哪些表、哪些字段是核心指标、时间范围应该怎么处理、结果为空时该怎么反馈给用户这些逻辑。这些都是这个 Skill 独有的“技能知识”。而普通的 function 很难承载这类东西。

2. 设计 Skill 时要问自己的三个核心问题

2.1 这个 Skill 服务的场景边界是什么

设计 Skills 第一个容易犯的错就是边界划得太粗。比如“数据分析 Skill”这种说法,看着没毛病,但实际拆的时候根本没法用。是查数?是画图?是算指标?还是出结论?职责不清晰的 Skill,最后多半会被模型用错,或者被其他 Skill 抢活。

所以我现在的习惯是,开始动手前先问:这个 Skill 到底要在什么场景下被调用?它的触发条件是什么?它绝对不能做什么?

拿我项目里“查商家经营数据”这个 Skill 举例。它的场景边界定义得很窄:只负责回答“某个指标是多少”“某个时间段涨了还是跌了”这类事实型数据查询。触发条件是用户提到了明确的数据指标、时间范围、业务主体。绝对不能做的包括:不负责分析原因、不负责给运营建议、不负责预测未来。这些是另一个“经营诊断”Skill 的事。

边界定义清楚了,后面的 description 就很好写了。写 description 的时候也不再是拍脑袋,而是直接从边界描述里提炼:当用户想查询具体经营数值时使用;如果用户问的是“为什么下降”“该怎么办”,不要使用本工具。

2.2 输入输出是否足够标准化

Agent Skills 之间经常要互相协作。我项目里一个“生成日报”的 Skill 需要调用“查订单数据”的 Skill 拿到数据,再调“写 Markdown 报告”的 Skill 格式化输出。如果这两个 Skill 的输入输出结构没对齐,协作的时候就要写一堆适配代码。

我在设计的时候会强制给每个 Skill 定义一个严格的输入 schema 和输出 schema。输入字段用什么格式、单位是什么、时间范围用 timestamp 还是字符串,都要写死。输出更是要结构化,统一用 JSON 格式,并且约定好哪些字段一定有、哪些字段可能为空、异常情况下怎么表示。

这里踩过一个大坑:有一次“查订单数据”Skill 返回的时间字段是字符串 "2024-08-01",下游“生成日报”Skill 正好要按天汇总,就直接拿字符串去排序,结果因为格式不统一,有的地方返回的是 "2024-08-01",有的地方返回的是 "2024/08/01",整个日报的分组全乱了。从那以后我就在项目里定了死规矩:所有 Skill 的时间字段统一用 ISO 8601 格式的字符串,内部处理一律先标准化再传递。宁可每个 Skill 多写两行转换代码,也不让格式问题散落在各个地方。

2.3 失败的情况怎么处理

Agent 调 Skill 一定会失败。网络超时、参数不对、下游接口报错、数据查不到……这些在单测里都测不出来,只有在真实跑的时候才会暴露。所以在设计阶段就要想明白:这个 Skill 失败之后,应该返回什么?是抛异常让 Agent 换条路走,还是返回一个特定的错误对象让 Agent 基于这个错误信息做下一步决策?

我的经验是,不要直接抛异常,因为大模型看到异常就不知道怎么处理了。更靠谱的是把错误也当成结构化输出的一部分,返回一个包含错误码、错误说明、可能原因和恢复建议的对象。这样 Agent 收到之后,能基于错误信息自己决定是重试、换参数还是告诉用户发生了什么。

举个例子,我项目里“调用外部天气预报 API”的 Skill,超时后会返回类似这样的结构:

{ "success": false, "error": { "code": "TIMEOUT", "message": "上游接口响应超时", "suggestion": "可稍后重试,或改用城市编码查询" } }

这样 Agent 拿到结果后就知道下一步该怎么操作:它可能会尝试把城市名转成城市编码再查一次,或者明确告诉用户“这个接口暂时不通,建议稍后再试”。这种处理方式比直接 throw 一个 exception 要好用得多,因为异常只能打断流程,但结构化的错误信息能帮助 Agent 继续完成任务。

3. 从零实现一个 Agent Skill 的完整流程

3.1 明确技能定义文件的结构

现在很多 Agent 开发框架都在推“Skill 即文件”的方式,就是每一个 Skill 对应一个独立目录,里面包含一个 skill 定义文件(描述这个技能是干什么的)和若干个执行脚本(实际干活的代码)。我们在项目里也用这套结构,简单说就是一个 Skill 目录长这样:

query_sales/ ├── SKILL.md └── run.py

SKILL.md 是这个技能的门面。模型在决定要不要调用这个技能的时候,主要就看这个文件里的描述。run.py 是真正执行任务的代码入口,接收标准化的输入,返回标准化的输出。这个结构看起来简单,但实际写的时候有很多讲究。

SKILL.md 里的内容,最重要的是开头的描述字段。注意:它不只是给人看的,更是给模型看的。你写“查询销售额”,模型只能知道这是个查数的功能,但如果你写“当用户想了解指定商户在指定时间段内的销售额、订单量、客单价等经营指标时使用,注意时间范围默认最近30天,若用户未指定商户ID则需先向用户确认”,模型就能准确的知道该在什么场景下激活这个技能,以及激活后怎么跟用户对话。这部分写得好不好,直接决定了技能被调用的准确率。

3.2 用 SKILL.md 写清楚“什么时候用”和“什么时候不用”

写 SKILL.md 这件事,是我在多个项目里反复迭代出来的经验。早先我写得特别简单,就一句话“查询销售数据”,结果模型在用户问“帮我看看哪个品类的退货率高”的时候也调它,在用户问“解释一下为什么这个月销量下滑”的时候也调它。前者它明明不会算退货率,后者它明明不该背分析的锅,但模型不管,它觉得既然要查数据,调这个工具总没错。

后来我把 SKILL.md 里的描述改成了带明确 when to use / when not to use 的结构,效果立刻好了很多。每次模型在纠结要不要用这个技能的时候,这个文件就是它的决策依据。现在我的 SKILL.md 长这样:

--- name: query_sales description: 查询商家的经营数据,包括销售额、订单量、客单价、退款金额等核心指标。仅用于回答“某指标是多少”这类事实性问题。 when_to_use: 用户明确提到了一个或多个经营指标,并且给出了具体的时间范围或商户范围。 when_not_to_use: - 用户询问数据变化的原因,需要归因分析。 - 用户希望基于历史数据做预测,这属于 forecasting 技能的职责。 - 用户只是想聊聊天,没有明确的数据诉求。 input: 商户ID: string, 必填, 商户的唯一标识 指标列表: string[], 必填, 要查询的指标名,可多选 开始日期: string, 选填, 格式YYYY-MM-DD, 默认30天前 结束日期: string, 选填, 格式YYYY-MM-DD, 默认今天 output_format: JSON,包含status、data、error三个字段

写清楚 when_not_to_use 让我学到了一个很重要的点:告诉模型“不要做什么”往往比告诉它“要做什么”更有效。因为 Agent 调错 Skill 的原因,绝大多数不是因为它不知道该调哪个,而是因为它以为自己调的那个“也能顺便做这件事”。

3.3 执行脚本的输入校验与结果标准化

SKILL.md 定义完之后,就是实现 run.py。这段代码不复杂,但有个点必须重视:输入校验。因为大模型传参数不会像人那么老实,它会自己发挥,比如把日期写成“昨天”,把商户ID写成商户名称。所以入口处一定要做一层严格的校验和修正。

我在项目里的习惯是,run.py 的开头就做三件事:检查必填参数是否都存在、检查参数类型是否符合预期、检查格式是否规范。如果发现某个参数缺失,能根据上下文推测的就补全,比如日期没传就用默认值,不能推测的就直接返回一个带错误码的 JSON,让 Agent 自己跟用户确认。

参数校验通过之后,就是真正的执行逻辑。这里我的建议是,不要在这个脚本里写太多业务逻辑。它的职责就是从外部数据源拿数据、做简单的清洗计算、然后按标准格式返回。至于这些数据接下来要怎么解读、怎么生成结论,那是 Agent 大模型要做的事,不需要也不应该在 Skill 里做。

回到我刚才说的销售查询技能,run.py 的执行逻辑就是:接收商户ID和指标列表,去数据库里查对应时间段的汇总数据,算好环比变化,然后返回标准化 JSON。整个过程不掺任何分析判断,就是干净的数据查询。这样设计的好处是,这个 Skill 可以被任何 Agent 复用,不绑定具体的业务场景。

4. 给 Agent 装配 Skills 的工程化实践

4.1 技能注册机制:不是塞进 prompt 就完事

当项目里的 Skill 数量超过 20 个之后,一个新的问题就来了:这些 Skill 到底怎么“交给” Agent?如果全塞进系统 prompt,先不说能不能塞得下,就算塞得下,模型也会被一堆工具描述淹没,注意力根本分配不过来。

我试过几种方案,最后稳定下来的是“注册 + 动态加载 + 路由”的方式。每个 Skill 先注册到一个中心化的注册表里,注册信息包括技能名、描述、依赖关系、启停状态。Agent 启动的时候不会一次性加载所有 Skill,而是根据当前用户的会话场景、历史对话、以及用户最近几次的意图,动态决定要加载哪几个候选 Skill。

这里的动态加载,我最初是从参数层面开刀的——按关键词匹配描述,命中就加载。后来发现光靠关键词不够,因为用户的表述太灵活了。比如我的销售查询技能,描述里写了“销售额”“订单量”“客单价”,但用户可能问“昨天赚了多少”“这个月卖得怎么样”,这时候关键词基本匹配不上,技能就不会被加载。后来我把匹配逻辑改成了两步:第一步仍然用关键词粗筛,筛出一个候选集合;第二步把这个集合里的技能描述全部塞给模型,让模型自己判断哪个跟当前用户意图最匹配。这个方法实践下来,召回率和准确率都有明显提升。

另外,注册表里我之前还设计过优先级字段。两个 Skill 描述相近、职责交叉的时候,比如“日报生成”和“周报生成”,模型可能会犹豫。这时候优先级字段就起作用了,让它默认优先选生成日报的那个,除非用户明确说了“周报”。

4.2 技能的状态管理:技能之间怎么配合

Skill 不是孤立的,它们经常要串起来跑。我项目里最典型的一个场景是用户问“帮我看一下昨天的经营情况,然后总结一下有什么问题”。这个需求需要两个 Skill 配合:先用“查经营数据”拿到昨天的核心指标,再用“经营诊断”基于指标做归因分析。

这里就涉及 Skill 编排的问题。目前我在项目里没有做太复杂的编排引擎,而是先把“编排”这个任务交给了 Agent 大模型自己。也就是说,主 Agent 负责理解用户意图,然后自己决定先调哪个、后调哪个。但是为了让它在多步调用的时候不迷路,我采取了一个比较朴素的办法,就是给每个 Skill 增加一个“输出上下文”字段,它会明确告诉模型“我返回的数据里哪些字段可以直接透传给下一个 Skill”。

回到刚才那个例子,查经营数据这个 Skill 返回的结果里,会专门加一个 handoff 字段,里面是整理好的指标摘要 JSON,设计上就是为了直接变成经营诊断 Skill 的输入。这样做的好处是,主 Agent 在中间环节不需要做太多的信息转换和取舍,下游技能拿到的数据就是干净、可用的。它不需要理解数据库表结构,不需要知道哪个指标对应哪个字段,整个链路会更省 token,也不容易出错。

这种“手递手”的设计有一个隐藏的好处:后续如果要换掉其中某一个 Skill,只要保证 handoff 字段的格式不变,整个链路就不用大改。有一次我们想换掉“查经营数据”的底层数据源,从直接查业务库改成查数仓,只改了对应的 run.py,上层 Agent 和下游 Skill 完全没有感知,替换成本非常低。

4.3 测试 Skill 的实用方法:从单测到仿真测试

Skill 写完之后必须测。我项目里的测试分三层。

第一层是最基础的单元测试:给定一段输入,检查 run.py 的返回是否符合 schema。比如日期传“2024-08-01”和传“昨天”,最后拿到的 SQL 是否一样,返回的 JSON 结构是否一致。这一层主要测的是代码本身的健壮性。

第二层是虚机仿真测试:在一个模拟的环境里,用不同的用户 prompt 去触发 Agent,看它是否会正确挑选我们期望的 Skill。这一层能暴露大量 description 写得不明确的问题。我甚至做过一个回归集,里面有好几十条历史用户问题,每跑一次新版本都要全量回归一遍,确保某次改动没有让模型在某个场景下错误地放弃调用某个 Skill。

第三层就是真实环境的小流量测试。选一小部分用户,把新加的 Skill 放进去跑,人工看对话记录,确认模型有没有在合适的场景调用它,调用后返回的结果用户认不认可,用户有没有反问或投诉。

这套测试流程听起来麻烦,但因为有了第一层的代码测试兜底,第二、三层的失败反而更集中在“语义理解”和“用户价值”的问题上,能反馈回 Skill 描述和技能边界的优化,形成正向迭代。

5. 我的踩坑记录:6 个不值得你再踩的坑

5.1 坑一:描述写得太短,模型抓不住触发时机

我最早写的 skill 描述就一句话。比如“计算订单退款率”,实际模型使用时经常在用户问“多少订单被退了”的时候不去调它,而是去调了更泛的“查经营数据”。原因很简单,模型并不知道“订单退款率”这个词和“多少订单被退了”是同一个意思。

后来我把描述扩展成了包括“等价触发词”的版本,比如“当用户提及退款、退货、取消订单、售后率等与订单退款相关的内容时使用”,效果立刻好了很多。写描述这件事,本质上是在给模型做“同义替换”训练,而且是开箱即用的,不需要额外微调。

5.2 坑二:工具返回值太大,把上下文塞爆

有一次我做一个“数据导出”Skill,它会把查询结果完整返回给主 Agent。用户要求导一周的数据,结果返回了几千行 JSON,直接把上下文窗口塞满了,后续对话模型开始胡言乱语。

解决方案是给 Skill 的输出做截断和摘要。查询明细返回给模型之前,先压缩成概要信息:多少行、关键指标聚合值、异常点列表。原始详细数据写到临时文件里,给模型返回一个文件路径。用户真要明细的时候,再走一个单独的文件读取 Skill。现在我对所有可能返回大数据量的 Skill 都强制做两层输出:一层是给模型看的结构化摘要,一层是给用户/系统用的原始文件。

5.3 坑三:把多个动作揉进同一个 Skill,导致复用困难

有个“处理订单”的 Skill,里面同时做了查订单、改状态、发通知三件事。结果下游另一个业务场景只需要“查订单”这一小步,却被迫把整个 Skill 拿来用,还得想方案绕过“发通知”这块,设了个开关来控制。很别扭。

后来我把这个 Skill 拆成了三个独立技能:查订单、更新订单状态、发送通知。每个技能的职责单一了,描述清晰了,复用率也更高了。拆完之后,整条链路的代码反而更简单,因为每个 Skill 的输入输出都是齐全的一进一出,不需要再为特殊的组合场景做各种 flag。

5.4 坑四:对 Skill 的耗时毫不在意,用户早跑了

低估耗时这个问题,是在线上被用户吐槽才重视起来的。有的 Skill 调用外部接口就需要 3~5 秒,如果 Agent 还要连续调用两三个 Skill,总耗时轻松上 10~15 秒。用户早就等不及了。

我的优化思路分为两条线。一条是尽量让 Skill 并发执行,比如日报生成需要的数据之间没有依赖关系,就并行去查,省掉一半以上的时间;另一条是给每个 Skill 都设置了超时上限,比如外部 API 超过 3 秒就返回超时错误,不无限等下去。此外,对于耗时明显较长的操作,Skill 会先返回一个“任务已提交”的状态,后续再通过另一个“查询任务结果”的 Skill 来获取最终输出,用户体验会好很多。

5.5 坑五:没有版本管理,技能迭代乱成一锅粥

Skill 和普通代码一样要版本管理。我的项目早期对 Skill 的改动很随意,某次调了“查经营数据”的技能描述,导致线上 Agent 行为突变,用户问“昨天卖了多少”它不查数了,反而开始编一个模糊描述。查了很久才发现是新的 SKILL.md 里误删了一个关键触发词。

现在我把所有 Skill 放在独立的 Git 仓库里,每个 Skill 目录下有 CHANGELOG,任何一次修改都要记录改了什么、为什么改、验证结果是什么。发布的时候按版本走,线上环境锁定版本号,只有经过回归测试的新版本才能升级。

5.6 坑六:技能没有监控,出错全靠用户反馈

Agent 调用了哪个 Skill、每次调用成功还是失败、失败的原因是什么,这些数据如果不埋点,出了问题只能等用户投诉。我后来给每个 Skill 的统一入口加了一条日志埋点,记录调用时间、输入参数、返回状态、耗时、错误信息。这些日志每天会汇成一份报表,让我能直观看到当前哪些 Skill 调用量高、哪些 Skill 老出错。

事实证明,这个监控非常值得做。上线第一天就发现有一个 Skill 调用量特别大,但成功率特别低,原因是这个 Skill 的输入校验写得有 bug,把合法参数也给挡了。没有这些监控日志,我可能要好几天之后才能从零零散散的用户反馈里发现,损失会大很多。

顺带一个实用小技巧:我还在监控报表里加了“误导率”指标——就是统计存在多少会话是调用了 Skill A,但用户后续的追问明显和 A 无关的。这个指标能变相暴露 Skill 的触发边界写得不准,比事后翻聊天记录高效得多。

6. 几个可以立刻用上的编排设计思路

写到这里,再分享几个我自己认为对思路启发性很大的设计套路。

第一个是“输入漏斗”思想。一个 Skill 的输入,不要一开始就暴露给 Agent 一堆自由填写的参数。先把参数分等级:必填的参数放最前,选填的放后面,能自动推断出的让 Skill 自己补全。比如“查经营数据”这个技能,必填的就商户ID和指标,时间范围完全可以默认“最近30天”。这样 Agent 调用技能的时候不容易因为参数太多而纠结传什么。

第二个是“技能互不可见”原则。同层的 Skill 之间不要互相调用,更不要自己调用自己,避免出现循环链。如果一个操作需要多步完成,应该由主 Agent 来串联,而不是在 Skill 内部再偷偷调用另一个 Skill。保持技能之间互相独立,这样测试和维护的复杂度都会显著下降。

第三个是“技能版本灰度”机制。新优化了一个 Skill 之后,不要直接全量替换老版本。我目前的做法是按用户维度灰度,比如先让新版本只对 5% 的请求生效,跑一两天看看调用成功率、用户反馈有没有变差,再逐步放开流量。Agent 行为的变化往往很微妙,有时候从指标上看不出来,但用户已经觉得不对劲了,灰度机制能帮我把风险压到最小。

这些思路谈不上多高深,但在工程落地上非常管用。Agent Skills 的设计本质上是“把复杂不可控的大模型行为,用工程手段拆解成可控的小单元”。这句话我是在自己把项目从十几个 function 重构为 skill 体系之后才真正体会到的。每次被模型误调用、误传参折腾到没脾气,我都会回来再看看是不是哪个技能的描述边界又没划清楚。工具不会自己变聪明,但设计它的方式,可以让模型的表现更接近“聪明”。

最后再补一句个人心得:如果你的 Agent 项目里工具数量还停留在个位数,那么 function calling 就够用了,不一定非要引入 skills 这套复杂度。但一旦突破了十几二十个工具,或者你发现模型频繁在不该调用的时候去调用了某些工具,那就是时候考虑用 agent-skills 的思路来重新组织你的能力体系了。我目前做下来的体感是,重构之后整个系统的可控性和扩展性都明显上了一个台阶,而且新接业务方需求的时候再也不用靠不停往 prompt 里塞描述解决了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询