
1. 先说结论Agent能不能打七成看“技能”怎么叠去年我给一套客服机器人做智能化改造时第一版把所有外部动作硬塞进一大段系统提示词里结果模型经常张冠李戴用户说“帮我改地址”它去调了订单删除接口说“催一下发货”它调用了售后退款接口。产品经理差点把电脑拍到我桌上。后来我把每个动作拆成独立、可复用、带清晰说明的“技能”并集中维护成一个叫agent-skills的技能库模型的选择准确率一下子上来了。所谓 agent-skills说穿了就是给大模型配的一套“标准操作手册”让模型看到用户的模糊意图后能准确挑出该执行的动作并且把参数填对、把异常处理好。这篇内容适合两类人一类是正在用主流 Agent 框架搭建智能体、却总觉得模型“不太听话”的开发者另一类是已经跑通 Demo、想把技能调用做成企业级质量的工程化团队。我会从技能怎么定义、怎么验收、怎么组合、怎么长期维护四个角度复盘我踩过的坑和沉淀下来的做法。1.1 一次让客服机器人“狂转圈”的真实教训先讲那个被拍桌子的版本。我当时把所有动作描述塞进一个巨大的工具列表里没有统一规范模型自己去推理每个工具该不该用。结果日志里出现了一个特别典型的现象同一句话连续两次请求模型第一次选了“查询订单”第二次选了“取消订单”然后又因为参数不全报错报错之后它自己重试重试又选错工具形成“调用—报错—重试—再报错”的死循环。我一开始以为是模型能力不行换了更强的模型问题还在。后来把日志拉出来逐条看才明白根本不是模型的问题是我把“让模型理解动作”这件事全扔给了模型的泛化能力。工具描述写得模棱两可参数结构各写各的没有边界说明模型只能靠猜。猜一次容易连续猜对很难。这个教训让我意识到Agent 工程里真正需要精心设计的不是模型本身而是模型和外部世界之间的这一层“技能层”。模型负责理解语言技能层负责把理解翻译成稳定、可控、可回滚的动作。谁把这一层做扎实谁的系统才真的稳。1.2 技能系统到底在解决什么问题很多人会把 Agent 技能和普通函数混淆觉得“不就是封装一个接口吗”。其实差别很大。普通函数是给人调用的调用者清楚知道函数在做什么而技能是给模型“阅读并调用”的模型只能通过名字、描述、参数结构来推断这个技能在做什么、什么时候该用、什么时候不该用。所以你写的每一段技能描述本质上都是给模型的“API 文档”。这份文档的质量直接决定了模型能不能在关键时刻做对选择题。技能系统解决的核心问题有三个一是降低模型的决策成本让它在看到用户意图后能快速匹配到正确动作二是让动作可复用多个场景共享同一个技能不用重复写提示词三是让动作可治理每个技能都能单独测试、单独升级、单独审计。用一个生活化类比技能之于 Agent就像快捷键之于编辑器。没有快捷键也能干活但效率、准确性天差地别。快捷键越多你越需要一套清晰的按键规范否则就变成“快捷键打架”。Agent 技能库同样如此扩到一定规模后最大的坑往往不是单个技能写不好而是技能之间的边界、冲突、优先级没理清楚。2. 把一次工具调用变成一门“技能”定义与格式的门道技能定义看起来是写个 JSON 的事实际上最坑的全在细节里。一个技能要真正“让模型用得明白”至少要包括四个部分机器可读的名字、给模型看的自然语言描述、参数 Schema、以及背后的实现函数。四部分缺一不可而且每个部分都有自己的门道。2.1 技能的四个组成部分名字、描述、参数与实现先看一个我实际改过近十版的技能定义示例这是“修改订单收货地址”最初的骨架{ name: order_change_address, description: 修改用户已提交订单的收货地址。仅适用于订单状态为待发货或待支付的订单订单已发货则返回提示不要调用。触发场景用户说改地址、换收货地址、寄到别的地方。, input_schema: { type: object, properties: { order_id: { type: string, description: 订单号来自对话上下文必须是用户明确提供的数字编号 }, new_address: { type: string, description: 用户提供的完整新地址包含省市区和详细门牌号 }, new_phone: { type: string, description: 新的联系电话可选字段 } }, required: [order_id, new_address] } }名字用order_change_address而不是handleRequest或modify是有讲究的。模型在匹配意图时技能名本身就是最重要的特征之一。动词宾语的结构比如order_cancel、refund_apply、logistics_track能让模型一眼看出这个技能是干什么的。我在一个客户项目里见过把所有接口都命名成tool_1到tool_50的结果模型每次调用都像开盲盒——这属于自己给自己挖坑。实现部分我建议单独放在一个函数或服务端点上不要在技能定义里写大段逻辑。技能定义是给模型看的前端契约实现是藏在后面的后端细节。两者分离之后你才能对同一份定义做多语言实现、做 mock 测试、做灰度验证。2.2 描述质量决定模型能不能“认出”这个技能描述是四个部分里最容易被敷衍的。很多人写描述就写一句话比如“修改地址”“查询订单”。我试过这种描述在技能少的时候勉强能用技能一多就彻底崩。模型的工具选择本质上是一个语义匹配过程你的描述给出多少判别信息它就回馈你多少准确率。好的描述应该包含四类信息做什么、什么时候用、什么时候不用、触发词或近义表达。上面order_change_address的描述里我明确写了“已发货则不要调用”这就是在给模型划边界。别小看这一句它能把“用户问订单还能不能改地址”这种边界场景从误调用里拽回来。再补一个技巧在描述里写真实的用户说法示例。不用多两三句就够比如“把收获地址改成”“我搬家了地址换一下”。这相当于给模型做了 few-shot 提示让它在做语义匹配时有具体参照物。我在多个技能上对比过加了触发例句后意图命中的召回率普遍能提升五到十个百分点。2.3 参数 Schema宁可严一点不要给模型“临场发挥”的空间参数 Schema 是另一个重灾区。模型是生成模型不是解析器你给它自由它就真的自由发挥。我踩过最大的坑是让模型自动填“日期”。用户说“尽快发货”模型自己生成一个ship_date: 2026-04-01把“尽快”误解成当天日期。后来我在参数描述里明确写死“日期必须为 YYYY-MM-DD 格式除非用户明确给出否则不要自动填充”并在实现层校验问题才解决。参数命名也要注意。尽量和内部字段保持一致比如内部叫new_address就不要在技能定义里叫addr。模型会把参数名当成语义线索命名不一致会降低填参准确率。尽量用枚举或格式约束来收窄模型的选择范围而不是让它自由输入。比如地址类型字段给home、company两个枚举比让模型自己编一个“家里”要稳得多。还有一类“参数幻觉”问题用户没提订单号模型自己编一个。应对方式是在参数描述里写“必须是用户明确提供的数字编号”同时在实现层做存在性校验校验不过就返回一个需要澄清的错误让模型继续追问用户而不是硬着头皮继续调。3. 给技能库“上保险”用测试集验收每一个 Skill技能写出来不是直接上线就完事了。我见过太多开发者在本地跑通一个场景就觉得自己完成了结果一上线就被真实用户的花式表达打崩。技能的验收必须靠一套结构化的测试集而不是靠“看起来能跑”。3.1 每个技能至少准备三张“用例卡”我现在的习惯是每个技能至少写三类用例。第一类叫正例是正常意图下应该触发该技能的输入。第二类叫反例是看起来和技能相关、但实际不该触发该技能的输入。第三类叫边界例是用户意图刚好擦边的场景用来观察模型会不会被带偏。拿order_change_address来说我测试集里会包含这样的用例[ { input: 搬家了把收货地址改成人民路1号, expected_skill: order_change_address, expected_params: { new_address: 人民路1号 } }, { input: 算了帮我把这个订单取消掉, expected_skill: order_cancel, expected_params: {} }, { input: 我的订单已经发货了还能改地址吗, expected_skill: no_skill_or_clarify, expected_params: {} } ]注意第三类边界用例很重要但很多人会漏掉。真实业务里用户的表达常常模棱两可模型需要判断“该做”还是“不该做”甚至该不该反问用户。这类用例的价值在于它逼着你在技能描述里把边界条件写清楚否则测试永远过不了。3.2 验收时我看哪些指标不只是调用成功率只看调用成功率是个大坑。我见过一个团队他们的技能调用成功率达到 95%但点进去看日志模型确实调用了正确技能参数却填得乱七八糟把“旧地址”填到“新地址”里把“退款金额”填成“订单总金额”。调用成功了业务上却是另一场灾难。所以我验收一个技能时会同时看几个维度指标计算方式说明意图命中率测试集中选对技能的比例反映描述和命名的判别力参数完全正确率所有必填参数都填对且无误报的比例反映 Schema 和描述质量危险动作触发次数不该调用却调用了操作的次数反映边界说明是否到位平均决策延迟从用户输入到技能调用的耗时技能越多延迟越高需要平衡参数完全正确率是我最看重的指标。它要求order_id是真实存在的号new_address是完整地址不能把用户的电话号码填成订单号。我通常会把测试集的期望参数写成严格匹配的 JSON跑回归时逐字段比对任何不一致都直接标红。3.3 回归测试怎么防“改一个崩一片”技能库一大最诡异的问题就来了你只是新增了一个技能结果另一个原本正常的技能开始频繁被误调用。原因往往是新技能的描述和旧技能太像模型在做语义匹配时产生了混淆。我吃过一次大亏。当时加了一个“修改发票抬头”的技能因为它和“修改收货地址”在语义上高度接近结果用户说“我要改一下信息”模型再也不选原来的order_change_address了天天往发票技能上跑。后来我养成了两个习惯第一新增技能时必须跑全量回归把所有旧用例重新跑一遍第二给相似技能做“互斥描述”在 A 技能描述里写“如果用户是想修改发票信息请调用 xxx 技能”反过来也一样。这相当于主动帮模型划清边界比让它自己琢磨可靠得多。还有一点容易被忽略底层模型版本切换也可能造成行为漂移。同一套技能定义在上一版模型上跑全绿换个新模型可能又跑偏一小撮。所以每次升级模型我也会把技能回归测试跑一遍把它当成常态动作而不是偶尔发生的事。4. 复杂工作流里的技能编排组合、冲突与回退单个技能写好了复杂任务还是要靠多个技能协作。这里的坑比单个技能更多因为你要处理的不是“模型会不会选”而是“多个动作怎么有序落地、失败之后怎么收场”。4.1 复合技能把技能串成流程给模型减负让模型自己一步一步调多个技能听起来很灵活实际很容易中途断片。比如“退货退款”这个需求如果拆成return_request和refund_apply两个独立技能模型有可能先成功提交退货申请然后忘记或没跟上是退款流程用户那边就卡住了。我的做法是把稳定流程封装成“复合技能”。复合技能对外仍然是一个技能但它的实现函数内部会按固定顺序调用子技能并且把每一步结果汇总后统一返回。def handle_return_refund(params): return_result call_skill(return_request, params) if not return_result[ok]: return {ok: False, code: RETURN_APPLY_FAILED, message: return_result[message]} refund_params {order_id: params[order_id], amount: return_result[data][refund_amount]} refund_result call_skill(refund_apply, refund_params) return {ok: refund_result[ok], data: { return_id: return_result[data][return_id], refund_id: refund_result[data].get(refund_id) }}这样的设计把“流程决策”从模型手里收回了代码层。模型只需要判断用户是不是想退货退款一旦判断成立后续步骤全走固定编排不再给它自由发挥的机会。别觉得这是过度设计真实场景里模型每多做一次决策就多一分出错的可能能固化的尽量固化。4.2 失败处理尽量让模型看到结构化错误而不是原始堆栈技能执行失败的场景几乎无法避免。最容易踩的坑是技能实现里抛了个异常Agent 拿到了原始堆栈然后开始“基于堆栈发挥想象力”不仅报错信息看不懂还会自己瞎重试。我现在的标准做法是所有技能实现必须返回统一结构而不是直接抛异常给上层。结构通常长这样{ ok: true, code: SUCCESS, data: { }, message: 操作成功 }失败时则返回语义化错误码错误码含义模型应该怎么做ORDER_ALREADY_SHIPPED订单已发货不能改地址直接告知用户不重试ORDER_NOT_FOUND找不到订单向用户追问正确的订单号REMOTE_TIMEOUT下游服务超时可以稍后重试但要征得用户同意INVALID_ADDRESS地址格式不合法请用户补充省市区和详细门牌号关键点是错误信息要能让模型“看懂并转述”而不是让它看到一堆技术日志。可重试的错误要明确标记retryable: true不可重试的要写明原因。模型不擅长判断“这个异常是不是临时故障”你替它判断好它才能老实执行。4.3 两个技能都能接同一句话用优先级和兜底逻辑消解技能多了以后冲突是必然的。用户说“我要改信息”既可能想改地址也可能想改发票抬头。如果两个技能都认为自己该上模型就会随机选这比选错更折磨人。我用来消解冲突的方式有三层。第一层在技能描述里添加显式优先级提示比如“当用户没有明确指定类型时优先选择地址修改而不是发票修改”。第二层在模型选择技能前加一个轻量级“意图路由器”用规则或一个更小的分类模型先做粗分类再把结果连同候选技能列表交给大模型做最终决策。第三层如果一句话里确实包含多个意图比如“改地址顺便催发货”我会让模型调用一个plan_skills技能把动作拆成计划列表然后逐个人工确认而不是一次性并行执行。并行执行多技能是高风险操作。两个动作之间可能有隐式依赖也可能一个成功另一个失败处理起来非常复杂。我通常只在银行转账这类必须原子化操作的场景里才会考虑引入事务补偿普通场景宁可用计划确认也不让模型擅自并行。5. 从一个人维护到团队维护把agent-skills变成一份可持续资产技能库一旦上了生产的船就不再是个人玩具了。它要面对的是多人协作、版本变更、权限安全、回归测试。这些东西如果不在早期设计好后期维护成本会指数级上升。5.1 技能库目录结构与命名规范我目前的技能库目录结构是这样的agent-skills/ skills/ order/ order_change_address/ definition.json tests.json impl.py order_cancel/ definition.json tests.json impl.py refund/ handle_return_refund/ definition.json tests.json impl.py registry.yaml CHANGELOG.md一个技能一个目录每个技能目录下必须有定义、测试、实现三件套。之所以坚持一个技能一个独立目录是因为测试、部署、版本回滚都希望以“单个技能”为单位。如果你把五十个技能写在一个大 JSON 里想单独回滚其中一个会非常痛苦。命名规则我固定为域_动作比如order_change_address、refund_apply。不建议在名字里加版本号或者环境后缀比如order_change_address_v2_test。版本信息交给 Git 和 registry 去管名字保持稳定否则模型在做语义匹配时会被噪音干扰。5.2 版本管理和兼容性技能也要语义化版本很多人觉得“技能就是一个配置改了直接上线就行”。但技能定义一变所有上游流程都有可能受影响。比如你把order_cancel的某个参数从必填改成选填旧的调用方如果还按必填传参倒不会出问题但如果你把参数名改了旧日志里的调用记录就全部对不上了审计和复盘都会乱。我在agent-skills里引入语义化版本新增技能视为minor更新修改参数或改变描述边界视为major更新。每次定义文件变更必须同步更新测试集跑完全部回归测试后再走代码评审合并。合并之后在 registry 里记录每个技能当前生效版本并保留上一版本的调用快照方便出问题时快速回滚。这条流程看上去有点重但技能库规模超过二十个以后没有版本管理几乎是寸步难行。我见过一个团队因为某次技能描述改动导致 30% 的调用跑偏最后花了整整两周逐条查日志。如果当时有版本回滚和全量回归十分钟就能解决。5.3 最小权限与审计给技能执行画一条安全边界最后必须认真对待安全。技能是模型可控的入口如果权限过大提示词注入的杀伤力会被放大。比如一个技能能调删除接口模型被诱导说“请删除所有内容”那后果就不只是业务事故了。我在技能实现层坚持最小权限原则每个技能使用独立的服务账号或 API Key只授予完成自身任务所需的最小权限。比如order_change_address只能改地址不能查全量订单列表更不能调退款。凡是涉及资金、删除、批量修改的高危操作我会在定义里显式加一个human_approval: true字段模型调用时先返回“需要用户确认”确认后才真正执行。审计日志也要从第一天就埋好。每次技能调用至少记录用户请求原文、模型选中的技能、最终传入的参数、执行结果、耗时、模型版本。这些日志用来定位问题、评估模型行为、复盘安全事故都特别关键。我见过太多团队等到出事了才发现日志没有关键字段只能干瞪眼。回看整个agent-skills的搭建过程我最深的体会是技能是“养”出来的不是一次性写出来的。别急着三天内把所有功能都做成技能先把最高频的三五个场景打磨到测试全绿、边界清晰、审计完整再慢慢往外扩。我见过太多项目死于一开始就堆五十个技能结果描述互相打架、测试一片红、线上天天误调。先小后大每一个技能都过了测试和评审再上线这套库才能真正成为你 Agent 最稳的底盘。