做AI应用这一两年,agent-skills这个词的出现频率越来越高。我最初注意到它,是在整理团队Agent代码仓库的时候——一堆散落的工具函数、提示词模板、调用逻辑互相纠缠,每次新项目都要重新拼一遍,改一处接口能牵出一串报错。后来我们把“技能”当作一等公民来设计,开发节奏立刻不一样了。这篇从我的实操经验出发,聊聊agent-skills到底在解决什么问题、技能怎么拆怎么设计、落地时有哪些坑,适合正在做Agent产品的开发者,也适合想把自己项目里“能跑但不好维护”的智能体重构一遍的朋友。
1. agent-skills到底在解决什么问题
1.1 “万能Agent”是个伪命题,能力边界才是真需求
我见过太多人一开始搭Agent时,脑子里想的是“一句话让模型把所有事都干了”。这个想法本身没有错,但落到工程实现上几乎必炸。
原因很简单:真实任务不是只有一个,而是一堆粒度不同、接口不同、权限不同的小任务。你今天希望Agent帮你查天气,明天希望它写SQL,后天希望它调内部CRM系统导出报表。如果你把这些需求全部塞进系统提示词里,prompt很快会膨胀到几千行,模型每次请求都要消化大量无关信息,响应变慢、token变贵,更麻烦的是改一个细节就要重新调优,因为提示词之间会互相干扰。
我踩过最典型的坑是:把十几个工具(tools)的说明全部写进system prompt,结果模型在简单任务上反而开始“选择困难”,明明只需要调一个查询函数,它非要先调用一个分析函数兜圈子。
核心问题不是模型不够聪明,而是能力没有结构化。这也是agent-skills出现的背景——它试图把“Agent会什么”这件事从模型的自由发挥里解放出来,变成一套显式、可管理、可复用的能力单元。
1.2 技能层是什么:把经验、工具、规则打包成可复用单元
先给一个我比较常用的定义:技能(skill)是一个能让Agent稳定完成某个子任务的能力包,它内部封装了触发条件、输入输出结构、执行逻辑、异常处理,以及必要的提示词片段。
它跟工具(tool)的区别在于,工具通常指单个函数或API,比如“查询订单接口”;技能更接近任务语义,比如“查询订单并生成用户可读的进度摘要”,里面可能包含两个工具调用和一段结果整理逻辑。
它跟工作流(workflow)的区别在于,工作流是固定的多步编排,适合流程不变的任务;技能更轻量,强调可组合、可复用,同一个技能可以被多个Agent在多种场景下调用。
举个例子。我之前做一个会议纪要产品,早期是“调接口→拿转写文本→直接把文本丢给模型总结”,效果很差。后来我们封装了一个“会议纪要结构化”技能,内部做了三件事:第一,清洗转写文本,删除语气词和重复片段;第二,按说话人分离内容;第三,抽取待办事项和决策结论。对外它只暴露一个函数:generate_meeting_minutes(transcript_text, duration)。
这个技能被产品里多个入口复用——实时会议、录音上传、历史会议补录,全部走同一个技能。后来要改摘要风格,只改技能内部的提示词片段,所有入口同步生效。这就是技能层的价值:它让能力有了边界,也让能力可以沉淀。
2. 技能拆解与设计:怎么把任务变成一套技能
2.1 从真实任务倒推技能清单,而不是拍脑袋设计
技能体系最容易犯的错误是“先定义后验证”——拿着一个理想化列表,觉得Agent应该会写邮件、应该会做数据分析、应该会排查服务器日志,然后照着清单吭哧吭哧写,写完发现一半技能用不上,另一半真正高频的需求压根没覆盖。
我的做法是反过来的:先收集真实任务,再做聚类。
具体分四步走:
第一步,把过去一段时间里Agent收到的用户请求全部导出来,没有线上数据就用测试用例凑,至少准备两百条。
第二步,按“动作+对象”拆解请求,比如“帮我查一下昨天订单为什么失败”可以拆成“查询订单状态”和“分析失败原因”;“把这份报告用英文发给客户”拆成“翻译报告”和“生成英文邮件”。
第三步,把同类的动作归并成一个候选技能,统计每个候选技能出现频次,用频次排序。
第四步,给每个候选技能标注三个属性:调用频率、复用范围、实现成本。频率高、复用广、成本低的优先做,频次低但成本高的先记入 backlog,不急着动手。
我当时用这个方法,从产品入口的几千条真实请求里筛出一个技能清单,最后第一版只做了六个技能,覆盖了大约八成的请求量。剩下的长尾需求,靠模型通用能力去兜底就够了。
这个“二八原则”很重要。技能不是越多越好,而是覆盖最核心的重复劳动。每一个技能都有维护成本,技能数量失控会让索引和检索都变成负担。
2.2 技能粒度怎么定:拆到多大算正好
技能粒度是整个设计里最难拿捏的部分。拆太粗,技能内部会塞进一堆if-else,每个分支都是不同业务场景,模型根本分辨不清;拆太细,调用链变长,Agent需要在一次任务里串五六个技能,编排失败率直线上升。
我通常用三条经验规则来判断粒度是否合适:
规则一:一个技能应该能在一次调用中完成一个可验收的子任务。“可验收”的意思是,执行完你明确知道结果对不对。比如“解析PDF并提取表格”是可验收的,“分析这份合同”不是一个好技能,因为“分析”太模糊,要拆成“抽取合同条款”“识别风险点”“生成合同摘要”三个技能。
规则二:如果实现一个技能时需要大段条件分支来区分业务场景,说明粒度偏大,应该拆开。比如“处理文件”这个技能,里面的逻辑可能分散成“文件转换格式”“文件压缩”“文件内容提取”,它们不应该混在一起。
规则三:如果调用完一个技能,还需要接着做三步以上的后续处理,才能得到用户要的结果,说明粒度偏小,应该考虑合并,或者提供上层组合技能。
我做电商客服Agent时有一个典型案例。“查单”一开始拆成了三个技能:查订单状态、查物流轨迹、查退款进度。看起来没问题,实际上用户问“我的快递到哪了”时,Agent要先判断属于哪类查询,再决定调哪个技能,这个判断本身就会出错。后来我合并成一个“查询订单全流程信息”技能,内部自动路由到不同数据源,对外参数只保留order_id和query_type,效果反而稳了。
粒度的判断不是纯理论问题,最终标准是你希望模型在哪个层面做决策。把决策留在模型擅长的地方,把执行收敛在技能内部。
2.3 技能描述与参数设计:让模型“一看就知道什么时候该用”
技能设计里最容易忽视、但影响最大的其实是描述文字。因为在大多数Agent架构里,不是你的代码决定什么时候调用技能,而是模型根据技能描述做决定。描述写得差,技能写得再好也没用。
一个合格的技能描述至少要包含三层信息:
第一层,什么时候用。直接写清楚触发场景,比如“当用户询问订单配送进度、物流轨迹或预计送达时间时使用”。
第二层,什么时候不用。这一点很多人会漏掉,但它非常关键。比如查物流技能里写明“不要用于查询历史订单状态,历史订单请使用查询订单历史技能”,能有效减少误调用。
第三层,需要哪些关键参数。描述里说明入参的格式和边界,比如order_id必须是数字字符串,query_type只能取status、logistics、refund三个枚举值。
参数设计上我坚持一条:尽量结构化,拒绝自由文本。自由文本看起来灵活,实际上会让参数抽取变成新的故障点。比如技能要接收“时间范围”,我会定义成start_time和end_time两个ISO格式字符串,而不是传一句“上周一到周三”,让模型自己猜。
下面是我常用的一个技能描述模板,可以直接参考:
{ "skill_name": "query_logistics", "description": "查询订单的物流轨迹与配送进度。当用户询问快递到哪了、什么时候送达、物流是否停滞时使用。不要用于查询订单金额、退款状态或历史订单信息。", "parameters": { "order_id": "string, 必填, 订单编号,纯数字字符串", "query_type": "string, 必填, 枚举值:logistics / status / tracking_number" } }注意description里的否定句,我后面会专门讲为什么它值回票价。
3. 技能实现与运行机制:从定义到真实跑起来
3.1 技能落地的三种常见形态,怎么选
技能定义只是设计文档,真正落地主要有三种形态,我分别踩过以后说说它们的适用范围。
第一种,纯Prompt技能。这种技能的“执行体”其实是一段指令模板,模型拿到输入后按照模板处理文本,不调用任何外部系统。适合文本分类、摘要、信息抽取、格式转换这类任务。优点是实现成本极低,缺点是结果不稳定,并且每次执行都会消耗token。我通常只用它来做轻量级文本加工,比如清洗转录文本、把半结构化文本转成JSON。
第二种,代码技能。一个函数或API的封装,内部可能有网络请求、数据库查询、外部SDK调用。Agent识别到需要这个技能后,提取参数,代码执行,返回结构化结果。适合有明确数据源或系统接口的任务。这是最稳定、最可控的形态,也是我主力使用的形态。缺点是需要写代码、需要维护,技能变多之后工作量不小。
第三种,混合技能。内部既有代码逻辑,也有Prompt调用。典型场景是:先用代码调取原始数据,再用模型做智能处理,最后用代码格式化结果。比如前面说的“会议纪要结构化”就是混合技能,代码负责转写清洗,模型负责抽取待办和决策。这种形态能力最强,但也最复杂,建议等前两种跑通之后再上。
选择标准只有一个:看任务里“智能”的部分占多少。几乎没有智能含量、逻辑固定,就写代码技能;完全靠理解力和表达力,就写Prompt技能;既有数据操作又有语义理解,就写混合技能。
3.2 技能库的组织与检索:技能超过20个之后怎么办
技能少的时候,怎么存都行,全塞在一个文件夹里也能找到。但当技能数量超过二十个,你一定会遇到“模型在决策时看不出该用哪个技能”的问题。我见过一个项目在系统提示词里列了四十多个技能说明,结果是模型频繁选错,甚至自创技能名。
解决这个问题,我在工程上分了三步走。
第一步,给技能建立目录结构,按领域分域。比如:
skills/ ├── order/ │ ├── query_logistics.json │ ├── build_order_report.json │ └── handle_refund.json ├── content/ │ ├── generate_meeting_minutes.json │ ├── extract_action_items.json │ └── summarize_document.json └── data/ ├── query_sales_stats.json └── detect_anomaly.json分域不只是为了好看,是为了后续检索。领域本身就是一种天然的标签。
第二步,写一个技能索引文件,每个技能维护4个字段:技能名、一句话说明、参数摘要、运行入口。这个索引文件可以在Agent初始化时只加载一次,替代把全文塞进prompt的做法。
第三步,按需加载。不要让Agent一次性看到所有技能,而是根据用户请求做语义检索,只把最相关的3到5个技能注入上下文。这样可以大幅降低token消耗,也降低模型选错技能的概率。
我在技能检索上做过一个对比实验:全量注入30个技能描述,选对技能的成功率大概七成;改成按请求检索后再注入相关技能,成功率提升到九成以上。原因不难理解,候选集越小,决策越集中。
3.3 一次技能调用的完整链路,每一步都在做什么
理解了技能形态和检索,再看一次技能调用背后到底发生了什么。我用一个客服Agent“查物流”的例子走一遍完整链路。
第一步,用户输入“我昨天买的那个手机到哪了”。
第二步,Agent做意图识别。这一步没有调用技能,只是让模型判断用户意图,同时从用户上下文或数据库里补全可能缺失的参数。
第三步,技能匹配。系统用用户意图去技能库做检索,召回query_logistics和query_order_status两个候选项,经过评分后选出query_logistics。
第四步,参数抽取。模型从用户输入里提取order_id。这里有个细节:用户可能没直接给订单号,系统要先查询用户最近一笔订单,拿到订单号,才能进行下一步。
第五步,技能执行。代码函数被调用,去物流系统拉取轨迹数据,返回结构化JSON。
第六步,结果整理。原始物流数据是一串运单节点,直接丢给用户显然不行。系统会把结构化结果拼成一段摘要,再把摘要交给模型生成口语化回答。
第七步,上下文压缩。技能执行产生的原始JSON可能很长,如果全量塞回对话上下文,下一轮请求就会很臃肿。我的做法是只保留一个精简版本的执行结果摘要,比如最终状态、当前位置、预计送达时间,原始数据丢弃。
这七步里,最容易出问题的不是技能本身,而是第四步参数抽取和第七步上下文压缩。参数抽取出错,后面的技能执行根本跑不到;上下文压缩做得太狠,模型会丢掉回答所需的细节。压缩力度要反复试,我当时是按“回答质量不下降”为基准一点点压缩出来的下限。
4. 常见问题与排查技巧实录
4.1 高频问题的排查思路速查
技能系统跑起来以后,真正磨人的不是写代码,是问题排查。我把实际工作中遇到的高频问题整理成一个速查表,每个问题都标注了排查顺序。
| 常见问题 | 现象 | 优先排查方向 | 常用解法 |
|---|---|---|---|
| 模型不调用该调用的技能 | 回答正确但绕过了技能,走通用能力 | 技能描述是否写清了触发时机 | 在描述中补充明确的“当……时使用”句式 |
| 模型调用了错误的技能 | 逻辑通顺但执行错了动作 | 候选技能之间描述是否有重叠 | 在描述中写明“不要用于……”;缩小检索候选集 |
| 参数抽取错误 | 技能执行返回报错或空结果 | 参数schema是否足够结构化 | 把自由文本参数改为枚举值;增加参数校验 |
| 技能执行超时 | 技能卡住或接口迟迟不返回 | 技能内部是否有重试和熔断机制 | 给每个外部调用加超时和熔断,失败时返回兜底文案 |
| 模型不会使用技能返回结果 | 技能执行成功但回答牛头不对马嘴 | 返回结果是否过于原始、缺乏摘要 | 在技能内部增加结果格式化步骤,输出带语义摘要的结果 |
| 技能调用链断裂 | 一个技能的输出无法作为下一个技能的输入 | 相邻技能的输入输出schema是否对齐 | 设计技能时统一上下文数据模型,用同一个中间格式传参 |
这里我说一下表格里几个容易忽略的点。
“模型不调用该调用的技能”这一条,很多人第一反应是模型不行,换更大模型。但绝大多数情况是技能描述里没有给出足够强的触发信号。模型决策时是读文字的,它不会因为你起的技能名里有“query”就认为该调用它。描述里明确写“当用户询问……时使用”,比什么技巧都管用。
“不会使用技能返回结果”这一条,根因通常是技能返回结果太重。我有一次让技能返回了一整张几十行的表格,模型拿到之后找不到重点,回答里开始编数据。后来改成在技能内部预先算好汇总指标,模型只拿汇总数字去生成回答,问题立刻消失。
4.2 几个值得复用的工程技巧
排查之外,我再分享几个在真实项目中验证过、收益比较稳定的工程技巧。
第一个技巧,给每个技能写一个固定的“结果摘要器”。无论技能内部做了多复杂的处理,对外统一输出三层结构:status(成功/失败/异常)、summary(供模型直接阅读的执行摘要)、raw_data(原始数据,可选)。模型优先读summary,需要细节再下沉到raw_data。这个设计解决了大部分“模型不会用结果”的问题。
第二个技巧,权限校验放到技能层,而不是UI层或Prompt层。我在一个内部工具里吃过亏:当时权限校验写在页面上,绕过页面直接调接口就能越权。后来把权限判断下沉到技能执行函数内部,每次调用都校验用户身份和操作许可。技能不只是能力单元,也应该是安全边界。
第三个技巧,为每个技能准备一个冒烟测试用例。所谓冒烟测试,就是一组固定输入和期望输出,每次技能改动后先跑一遍,确保没有破坏核心链路。技能看似独立,实际经常被多个场景复用,一个细节改动可能影响所有入口。没有冒烟测试,等于裸奔。我建了一个很大的回归用例集,跑完一遍也就几分钟,但拦住了大量线上问题。
第四个技巧,记录技能调用的token消耗。Prompt技能和混合技能会消耗模型token,这个成本很容易被忽视。我在技能运行时挂了计数逻辑,每调一次都会累加token数。一个月后看数据发现,某个低频技能消耗的token比高频代码技能高出几倍,因为它的输入输出很冗长。根据数据做优化,比凭感觉砍功能靠谱。
4.3 一个让我记忆深刻的踩坑案例
说一个具体的案例。前段时间做一个报表生成技能,需求是让Agent根据用户的一句话生成销售报表。初版设计得很简单:技能接收时间范围和维度参数,调数据接口,返回表格。
上线后问题不断。用户说“看下上周华东区的销售情况”,模型正确调用了技能,但返回结果是一张原始明细表,整整几十行,模型根本不知道用户想看的是汇总还是明细。用户又追问“为什么不总结一下”,Agent才补了一段话。
后来我重构了这个技能:内部先自动生成三层数据——汇总指标、按维度拆解、明细列表——然后用Prompt挑选当前问题对应的展示层级,最终输出一个带结论的摘要。重构之后,用户问“上周华东区怎么样”,技能直接回一句“华东区上周销售额环比增长8%,主要是上海和杭州两个城市拉动”,附上维度拆解表格。
这个案例给我的教训很直接:技能不能只做“取数”,还要做“理解任务语义”。同样一个数据接口,面对“怎么样”和“列出每条订单明细”,要输出的内容完全不同。技能设计时就要考虑用户提出这个请求背后真正想要的东西。
5. 一点真实体会与后续扩展
技能体系不是一次设计出来的,是跟着项目迭代长出来的。我第一版技能框架只有三个技能,问题一大堆,但因为骨架搭得对,后面加技能只是往目录里放新文件、写清描述和参数,整套机制不需要重写。这个“先搭骨架、再做深一两个技能、再横向扩展”的顺序,我建议你直接复用。
紧接着说一个一直想做的扩展:技能市场。我们的技能库目前还是项目内共享,但如果把技能描述、参数schema、测试用例打包成一个标准格式,不同团队、不同项目之间就能互相交换技能。一个团队打磨好的物流查询技能,另一个团队下载即可用,这种复用效率会非常惊人。
另一个扩展方向是跨Agent共享技能。我们现在每个Agent有独立的技能列表,但很多基础技能是重叠的,比如“时间格式化”“文本摘要”“数据校验”。把这些通用技能单独抽出,做成一个共享层,所有Agent按需引用,维护成本能再降一截。
多模态技能也在规划里。目前技能大多处理文本和结构化数据,但实际需求里有很多图像理解、语音合成的场景。把多模态能力也封装成技能,整个Agent能覆盖的任务范围会大很多。
最后分享一个小技巧收尾。我要求团队每次新增技能时,必须在描述里写一段“不要在什么时候使用”。这个约束看起来是多写几句话,实际上逼着你去思考技能的边界到底在哪。写不出这段,说明你自己都没想清楚这个技能的适用范围,这种情况下,等想清楚了再写代码也不迟。