最近在折腾 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.pySKILL.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 里塞描述解决了。