从"什么都懂一点"到"真的能落地":聊聊Agent Skills的设计与工程化
先说一个我自己的观察:这两年大家做AI应用,从最早调Prompt,到后来接RAG,再到现在恨不得所有东西都交给"智能体(Agent)"自己发挥,思路的演进速度是真快。但跑过几个项目之后你会发现,LLM本身的通用能力越来越强,真正拉开应用效果差距的,反而是你怎么把经验塞给它——这正是Agent Skills(智能体技能)要解决的问题。
简单讲,技能不是一个普通的功能接口,也不是一段Prompt模板,而是一个"带触发条件、带执行流程、带质量约束"的完整能力封装。让Agent遇到某类问题时,不是从零推理,而是像老师傅翻出手感一样,直接照着成熟路径执行。本文把这些东西拆开揉碎了讲:技能和工具到底什么关系、技能声明怎么写才不会被模型误解、技能运行时的路由和回退怎么设计、实测中为什么技能会"翻车"、以及团队里怎么管理技能才不会变成一团乱麻。如果你正在做Agent类应用,或者准备把Agent从Demo推向真实业务,这篇内容应该能帮你少踩不少坑。
1. 为什么说技能是Agent从"能聊"到"能干活"的分水岭
1.1 没有技能的Agent:看似什么都会,实则什么都不敢保证
先说一个反直觉的结论:模型越强大,越需要技能,而不是越不需要。原因很简单,一个什么都懂的通用大模型,恰恰意味着它在具体场景里"不够懂"。
举个例子。你让Agent帮你写一封商务邮件,它写的邮件通常很得体,但如果你希望这封邮件一定符合公司VI规范、一定带特定模板抬头、一定经过合规审查短语,它大概率会"自由发挥"。不是因为模型笨,而是因为它没有你的业务流程上下文。技能本质上就是把这些业务流程、判断标准、执行步骤,标准化成一个可复用的"操作手册"。
我自己最早做客服Agent的时候,用的是最朴素的做法:把FAQ全部灌进提示词,让模型直接回答。结果对话长度爆炸、关键信息老被遗漏、每次上线都要逐条调试。后来我换了一种思路:把"查订单"、"查物流"、"退换货"、"开发票"这些高频服务拆成独立的技能文件,每个技能只做自己那一摊事,Agent负责把用户意图匹配到技能上,再执行技能里的步骤。效果提升非常明显——不仅是回答准确率,更关键的是行为变得可预期了,我知道什么样的输入会触发什么样的流程。
提示:把技能理解成"你给Agent写好的工作SOP"更准确。它不是让模型变得更聪明,而是让模型在特定场子里更有谱。
1.2 技能与工具(Tools)、插件(Plugins)到底什么关系
很多人把技能、工具、插件这三个概念混着用,这在技术评审会上特别容易引发混乱。我自己的区分方式是这样:
**工具(Tool)**是最底层的能力原子,比如"调用某个API"、"执行一段Python代码"、"访问某个数据库查询"。它干的是具体的、单一的活。
**插件(Plugin)**是工具的打包集合,通常是"某个外部系统的完整接入包"。比如你接入了某个代码托管平台,插件里可能包含"创建仓库"、"提交Issue"、"创建PR"等多个工具。
技能(Skill)则更进一步,它是面向业务目标的工作流封装。一个技能内部可能调用多个工具、可能包含判断逻辑、可能有多步顺序。技能关心的不是"调了什么接口",而是"这个问题怎么被解决掉"。
我用一个生活化的类比来理解:工具是一把锯子,插件是一个工具箱,而技能是"老师傅用这套工具箱做出一把椅子"的完整手艺。你给Agent一个工具,它知道怎么用,但不知道什么时候用、用完怎么验收;你给它一个技能,它等于拿到了一个带质检标准的手艺流程。
搞清楚这个概念之后,再设计技能体系时思路就清晰了:工具层尽量薄,技能层尽量厚。工具只管暴露能力,把逻辑判断、步骤编排、异常处理全放到技能层去做,这也方便将来工具升级时技能不用跟着大改。
2. 技能文件的核心设计:一份"人机通读"的说明书
2.1 打开技能文件,你看到的应该不只是Prompt
先给大家看看我实际项目里一个技能文件的结构,这种拆分是反复调试出来的。这里我以"客户工单分类技能"为例,不做任何代码层面的魔法,就是一份文本加JSON的配置文件:
{ "name": "ticket_classifier", "description": "用于将用户提交的工单自动归类到预设服务目录中,适用于售前、售后、技术支持等客服场景。", "when_to_use": [ "用户提交新的工单、服务请求或投诉时", "对话中需要识别用户问题所属类别并分派给对应处理组时" ], "when_not_to_use": [ "用户只是闲聊、询问公司信息", "用户要求修改已有工单的紧急程度" ], "input_requirements": { "ticket_text": "string,必填,用户提交的原始工单内容", "customer_level": "string,可选,用户会员等级,用于优先级判断" }, "steps": [ { "step": 1, "action": "从ticket_text中提取问题主体,去掉问候语和客套话", "criteria": "剩余内容必须能明确表达一个核心诉求" }, { "step": 2, "action": "判断问题类型:故障报修/使用咨询/退换货/发票相关/投诉建议", "criteria": "如果存在二义性,按'投诉建议'处理并转人工" }, { "step": 3, "action": "根据分类结果输出结构化JSON", "criteria": "输出格式为:{category: string, confidence: float, reason: string}" } ], "output_format": { "category": "string,分类名称", "confidence": "float,置信度,0到1之间", "reason": "string,简短分类理由" }, "quality_check": [ "分类必须落在预设目录中,不得自行创造新类别", "当confidence低于0.6时,必须在reason中说明不确定的点并转人工", "ticket_text为空或无法理解时,直接输出需要人工介入的标记" ], "skills": ["text_processing", "intent_recognition"] }这份配置里有几个字段是我觉得特别值得留意的。
第一个是when_to_use 和 when_not_to_use。绝大多数技能失效,不是因为步骤写错了,而是因为触发时机判断错了。模型是概率推理,你给出明确的"什么时候用、什么时候不用",它的路由准确率会提高很多。尤其是"when_not_to_use",很多人不写,但实际调试时你会发现,没有负例约束的技能特别容易被误触发。
第二个是steps里的criteria。我不是只写"提取问题主体"这样一句交代,而是补充"剩余内容必须能明确表达一个核心诉求"这样的验收标准。这是给大模型看的,更是给后续的自动评测脚本看的——没有验收标准的步骤,执行结果就没法断言对错。
第三个是skills字段。这个字段代表该技能依赖的底层技能。比如"工单分类"依赖"文本处理"和"意图识别"这两个基础技能。技能之间可以有依赖关系,这给我们后面做技能编排打好了基础。
2.2 好技能与坏技能:差在"目标感"和"边界感"
我给技能写文件时有个习惯:写完先不着急跑,拿给项目里不太懂技术的业务同事看一遍。如果他们看完能说出"这个技能大概是干什么的、什么情况会用、跑完得出什么东西",说明技能定义基本合格。如果他们一脸困惑,那不管代码怎么实现,这个技能上线后一定问题百出。
一个坏技能是什么样子?我见过不少——描述写了一大段业务背景,但没写清楚"能不能用";步骤写了八步,但每步都是开放式的"分析用户情绪";输出直接写"返回一个友好的回答"。这种技能模型执行的时候非常痛苦,因为缺少约束,每次跑出来的结果都不一样。
一个技能文件的核心,我个人总结就六个字:目标感、边界感。目标感是让Agent清楚"完成这一步之后,我应该能得到一个什么状态";边界感是让Agent清楚"这件事做到什么程度就该停手"。这两个东西写清楚,技能大概率差不到哪儿去。
3. 技能编排与路由:让多个技能协同而不是打架
3.1 技能路由的真相:它不是一个技术问题,而是"决策模型"问题
当技能数量超过十个,新问题就出现了:用户一句话进来,Agent怎么知道该调用哪个技能?
有人说这还不简单,让大模型自己选呗。对,但问题在于大模型选路由靠的是技能描述,不是你内部怎么装配的。所以技能路由的优化,很大程度是在优化"技能的对外描述"——也就是上一节说的 description 和 when_to_use。这一段针对路由的工程细节做展开。
我自己做过一个对比实验。一开始,我把每个技能描述写成一大段"系统能力介绍",类似"该技能利用先进的自然语言处理技术,结合行业最佳实践,为用户提供卓越的服务体验"。结果路由准确率很惨,Agent把什么都往这个技能上套。后来我把同一条描述改成两句话:"用于处理用户关于订单退款的请求。当用户提到退款、退货、金额返还、原路退回等词语时使用。"路由准确率立刻明显上升。
原因也不难理解:大模型做技能匹配时,靠的是描述中的行为线索和触发词,而不是修饰性文字。你想让模型选得准,就要在描述里给它"钩子"——明确的场景词、动词、对象。
我还做过另一个优化:在系统的调度层,不用模型直接选技能,而是先让模型做一步"结构化意图分析",输出一段带固定字段的JSON,比如{primary_task: "退款", related_entities: ["订单号12345"], urgency: "high"},再去匹配技能。这么做多了一步,但多出的延迟大约只有几百毫秒,换来的是路由稳定的提升。尤其在技能重叠度高的场景(比如"退款"和"售后"都能处理同一个问题),结构化意图匹配比纯靠文本相似度的"暴力路由"稳得多。
3.2 技能依赖与执行顺序:你怎么处理"先查再改"这类复合动作
很多真实业务不是单技能能搞定的。比如"咨询订单状态并申请补发发票",你得先查订单,再判断是否允许补发,然后走补发流程。最终动作是"补发发票",但前提是"查询订单"。
这种复合流程我建议不要硬塞进一个超大技能里。更好的做法是:把"查询订单"做成底层技能,把"补发发票"做成上层技能,上层技能在 steps 里显式声明调用底层技能。这个思路和我们工程里的模块化很像,但在Agent技能体系里有一个特殊之处:上层技能可以调用底层技能,但底层技能不应该知道上层技能的存在。一定要防住反向依赖。
为了管理这种依赖,我喜欢搞一张简单的依赖表,上线前过一遍:
| 技能名称 | 依赖技能 | 是否允许并行 | 备注 |
|---|---|---|---|
| 补发发票 | 查询订单、开具发票 | 否 | 必须先查订单,确认状态后再开票 |
| 改签机票 | 查询订单、可用航班查询 | 是 | 两个查询可以同时发起 |
| 生成周报 | 数据汇总、文本生成 | 否 | 必须在数据汇总后基于真实数据生成 |
这张表不复杂,但对排障很有帮助。有时候线上问题不是某个技能本身错了,而是技能依赖的那个技能悄悄换了行为——你查了一下依赖表,立刻能锁定排查范围,不用从头到尾推理。
3.3 技能之间的"串味":状态隔离是大家最爱忽略的事
技能编排里最隐蔽的坑,我把它叫"状态串味"。举个真实踩坑的例子:我们当时有个"用户信息查询"技能和一个"订单查询"技能,两个技能都用了同一个"当前用户上下文"的缓存变量。原本设计的是查询技能只读不写,结果有一天模型在执行"订单查询"时,判断用户"活跃度偏低",顺手往上下文里写了一个"用户可能流失"的标记。这下好了,后面所有对话里,Agent对用户的态度都变了,还老推荐挽留优惠券。
排查了半天,根因就是技能之间共享了可变状态,并且没有约定"谁可以写、谁只能读"。从那以后,我在技能设计规范里加了一条强制要求:技能的输入参数和输出结果要显式声明,技能内部不允许修改公共上下文,除非输出到唯一的"决策结果"字段。
注意:技能之间交互,永远是"上层的输出作为下层的输入",而不是"共用一张草稿纸"。这个规矩守住,能避免至少一半的"技能打架"问题。
4. 技能落地实测:三种"翻车"现场与对应排查思路
4.1 "模型假装调用":反馈机制缺失导致的路由幻觉
先讲一个特别容易骗过人的现象:你给Agent配置了技能,它也确实输出了一个看起来很像"调用技能"的结果,但仔细一看,它根本没走技能的步骤,只是模仿技能的输出格式自己编了一个。这种"假装调用"特别坑,因为它看起来一切正常,直到数据对不上才发现异常。
我当时怎么排查出来的?我把技能步骤里的中间日志全部打开,发现Agent执行"工单分类"技能时,直接跳过了第一步,给出了一个格式完美的结果,但分类理由驴唇不对马嘴。后来我分析,这其实是模型在上下文压力下走了"捷径"——它读到了技能输出格式的示例,就直接照着模板填,跳过了推理过程。
解决办法也不神秘:在技能的关键步骤上加上日志埋点,并且在质量检查里强制校验步骤输出。比如我在第二步后面加了一个校验:分类结果必须出现在预设目录的枚举值里,否则技能直接报错并转人工。模型一旦发现"编造"过不了校验,就会老老实实走完前置步骤。
4.2 技能参数一多,意图识别就开始漂移
第二个坑非常典型:为了让技能更"灵活",我一度在技能的输入参数里加了七八个可选字段,比如"用户等级"、"历史订单"、"地域"、"渠道来源"、"天气"等等。本意是让技能在复杂场景下更有适应力,结果适得其反——参数越多,大模型越不知道哪个是关键的,经常为了填一个次要字段,把主任务的判断都带偏了。
具体表现是,明明用户问的是"这个订单能不能退款",Agent却因为"地域"字段的存在,扯了一堆地区政策,最后还没给出退款判断。
我的调整思路是:能削的参数尽量削,非必须的字段不要出现在技能签名里。信息如果需要的,让技能内部通过内置的工具去查,而不是上来就要求模型一次性提供所有信息。这样做的额外好处是,技能的"开启门槛"更低了——用户只要用一句话就能触发技能,而不是先被问好几个问题。
提示:技能签名设计的最高原则是"最小充分"——参数只保留影响核心决策的那几个,其余的都放到技能内部去解决。
4.3 技能版本升级带来的"隐性破坏"
最后一个坑,我猜做Agent的朋友早晚会遇到:技能升级之后,整体效果不仅没变好,反而在某些case上明显退步。而且最让人难受的是,测试集上指标明明是涨的,线上case却翻车。
我当时有一个"发票信息提取"技能,升级前用正则加模型抽取,升级后换成了纯模型抽取,准确率在200个测试样本上提升明显,结果上线第三天就有人反馈:发票号和日期总是被抽到但金额偶尔抽丢。回头一查,原来是升级时为了处理一种特殊备注格式,把金额提取的那步约束放松了,导致很多金额被归成了"未匹配文本"。
这件事给我的教训是:技能升级必须带回归测试集,而且这个集子里要特意留一些"旧版本能处理好的case",防止修了新问题、砸了老功能。后来我们的技能仓库里强制要求每次改动关联测试用例变更,至少在核心业务技能上,这已经成了硬性红线。
5. 技能维护与团队协作:让技能仓库健康生长的三个机制
5.1 建立"技能TDD"流程:先写测试,再写技能
技能也是一种代码,而且是有一定业务风险的代码。如果你希望它能在团队里被长期维护,一定得把测试前置。我推荐"先例驱动开发":在写技能文件之前,先准备一批"输入-预期输出"对,至少有三种类型的用例:
- 正向用例:典型输入,应该命中该技能,并输出正确结果。
- 负向用例:明显不属于该技能的输入,技能不应该被触发。
- 边界用例:模棱两可的输入,期望按预设规则处理,比如"倾向于人工介入"还是"倾向于按默认分类处理"。
这些用例不需要自动化框架,纯文本表格就能起步。写好用例再写技能,最大的好处是设计技能时你会不自觉地去考虑行为边界,而不是只关注"核心路径能跑通"。而且将来无论谁来维护这个技能,改完之后跑一遍用例表,有变化一眼可查。
5.2 技能文件的命名、version与变更记录
技能多了以后,管理成本会指数级上升。最痛苦的是那种改来改去,最后连项目主管都说不清当前线上跑的是哪个版本的情况。我的解法是给每个技能文件加固定的元信息头:
name: ticket_classifier version: 3.2.0 last_modified: 2025-06-xx status: active changelog: - version: 3.2.0 change: 增加二义性问题默认转人工的分类规则 - version: 3.1.1 change: 修复low confidence时遗漏输出reason字段的问题不要小看这个头,每次技能路由出问题,第一个要查的就是版本变更记录。很多线上问题其实就是"某个技能的某个判断逻辑调了一下,但调用侧不知道",技能文件头里带清晰变更记录,能让排障时间至少少一半。
关于改名,这也是团队协作里容易踩的地雷。技能一旦被上层技能引用,改名等于重构,所有依赖都会断。所以技能的命名最好从一开始就想好,用"动词_对象"的格式,比如"query_order"、"refund_apply",而不是用容易过时的描述性名字。一旦名字固定了,即使实现方式大改,也不要轻易改文件名,新老版本映射表会把人绕晕。
5.3 定期技能体检:把没人用的技能从路由表里清出去
最后分享一个被很多人忽略但特别有价值的动作:清理。Agent的颅内空间不是无限的,路由表里技能越多,模型选择难度越大,互相干扰的概率也越高。而且很多技能上线的时候很热,过俩月业务方向调整了,彻底用不上了,如果一直留在技能仓库里,反而成了噪音。
我建议大概每两个月做一次"技能体检",重点看几个指标:
- 近30天被触发次数低于阈值的技能,考虑下线或与类似技能合并。
- 路由冲突次数高的技能对,考虑在 when_to_use 中增强排他性描述。
- 凡是"没人说得清当初为什么建"的技能,直接下线备份,让路由表始终保持精简。
别舍不得删。技能仓库不是越大越好,而是越清晰越好。每次做完整理之后,我都能明显感知到路由准确率和响应速度的提升,因为模型的决策空间变小了,干扰噪音少了。
写在最后的体会
做了小半年的Agent技能体系下来,我最大的感触是:技能设计的思路,跟我以前接触过的很多工程实践一样——复杂度不在"实现"里,而在"边界"里。你把每一步的验收标准写清楚了,把什么时候能用、什么时候不能用说清楚了,把技能之间的依赖和隔离约定规范了,剩下的技术实现反而都是比较常规的工程量。
还有一点想特别强调:给Agent加技能,不要贪多,更不要一开始就搞一个大而全的技能库。技能应该跟着业务痛点走——哪类问题反复出错、哪类流程需要重复执行,你才为它封装一个技能。从两三个高频动作起步,把小闭环跑通,再逐步扩展。这个过程本身也是在帮团队整理业务流程,你会发现,写技能写到后面,收获的远远不止是Agent变强了,你们的业务流程也会变得更清晰。