☰
Agent技能库实战:定义、测试与编排的工程方法论
2026/10/8 3:22:33 网站建设 项目流程

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 最稳的底盘。

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

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

立即咨询