☰
Agent技能治理:从混乱工具清单到稳定能力体系
2026/10/21 21:51:17 网站建设 项目流程

"agent-skills"这名字,当时只是我们内部仓库的一个代号。谁能想到,这个原本只打算花两周搭个脚手架的小项目,最后把我们整个Agent平台的架构思路都改了。

起因是当时线上的Agent越来越"笨"。不是模型笨,是它根本不知道自己有哪些能力可用。我们早期把所有工具、函数、接口一股脑塞进function calling的声明里,单个业务线几十个函数,多业务线一混,模型经常选错工具,参数瞎传,还特别爱在两个相似工具之间反复横跳。用户一句话,它能跑出七八轮无效调用。我们排查了很久,最后意识到问题根本不在推理,而在"能力治理"——Agent的技能层没有设计。

这也是这篇文章想讲的:一套叫agent-skills的技能管理体系,怎么从混乱的工具清单里长出清晰、稳定、可迭代的Agent能力。适合正在做自主Agent、AI自动化流程,或者准备把大模型接进复杂业务系统的人。如果你也被"模型乱调工具、参数一传就崩、技能改完就失控"折磨过,这篇应该能帮到你。

1. agent-skills是什么:先把技能层从Agent里拆出来

1.1 一直没解决好的"会用"问题

做Agent项目的人大概都有同感:单点能力测试永远完美,一进多轮真实场景就露馅。用户说"帮我查一下上个月订单里有没有超时未发货的,有的话催一下供应商",模型的第一步可能是先去调"查全部订单",而不是先去调"查询异常订单"。它不是不会推理,是压根不知道自己手上的工具哪个是干这个的、哪个参数代表什么、调用完怎么衔接下一步。

这个问题的本质,是说我们把"能力的描述权"交给了模型自己去理解。函数名写得含糊一点、参数说明少一句、接口返回格式一复杂,模型的选择就开始碰运气。更要命的是,工具一多,这种碰运气的概率会被放大。20个工具的时候还好,50个以上的时候,路由准确率掉得让人绝望。

我们当时做了个很粗糙的统计:把线上一次真实会话的调用序列拉出来,发现有接近三成调用是无效的——要么调错了工具,要么参数错,要么调完step没推进,下一轮又调同一个。这不是模型厂商能解决的问题,这是应用层的能力编排问题。模型是"大脑",但大脑得先知道手上有哪些"技能",每个技能怎么用、什么时候用、什么时候不该用。这个"技能集"如果组织得乱七八糟,大脑再强也没用。

所以agent-skills的核心,不是把功能做成"能调用",而是做成"被正确调用"。它要求我们把每一项能力当成一等公民:有身份、有边界、有合同、有测试、有版本。

1.2 技能、工具与工作流:重新分层

很多人会问:这不就是工具管理吗?把工具定义清楚、暴露给模型不就行了?

这里有个关键区别。工具是最小可执行单元,比如"查库存""发消息""创建订单",它通常是单个API或函数。但Agent真正需要的,是能完成"一个完整目标"的能力单元,比如"处理一笔退货申请"。这个目标内部可能要调三四个工具,还要做些判断和组装。单把工具扔给模型,模型得自己拼装,拼装过程就是出错重灾区。

所以我们在体系里分了三个层次:

层次概念典型形态谁来决策
工具层原子能力,单次API/函数调用查库存、发通知、调支付接口技能内部的实现逻辑决定
技能层面向完整目标的可复用能力单元处理退货、创建工单、核对账期大模型在技能层做路由选择
工作流层多个技能的编排,含状态和分支售后全流程:先核验订单、再定责、再发起退款业务规则或编排引擎决定

把决策放在技能层而不是工具层,是这套体系里最重要的一次取舍。模型不需要关心"处理退货"内部是先查库存还是先核验订单,它只需要知道"用户想退货,我有'处理退货'这个技能可用"。技能内部如果需要调用多个工具,那是工程代码层面的确定性逻辑,不走模型推理。这样既降低模型的决策负担,又保证内部步骤可以做到100%可靠。

有人担心技能太"粗"会丢失灵活性。我们实践下来的结论是:技能粒度要以"用户可感知的任务边界"为标准。用户问"我的订单到哪了",对应"查询物流"技能;用户说"我要退货",对应"发起退货"技能。粒度太细,用户一句话要拆三个技能;粒度太粗,技能内部变成一个大杂烩,改一个分支就要全量回归。这个平衡后面第五节还会细讲。

2. 技能怎么定义:一份能让模型"看懂"的注册规范

2.1 技能描述:模型靠它而不是靠函数名做决策

技能定义里最先写、也最容易写砸的,是描述(description)。我见过很多团队把描述写成"用于查询订单信息",这种描述和没写一样。

大模型做技能路由是语义匹配,不是关键词匹配。它会把用户当前这段话的整体语义,和你给的描述放在同一个空间里比对。如果你的描述说的是"查询订单信息",那用户说"帮我看看我昨天买的东西到哪了",模型可能觉得"查订单"和"看物流"差不多,于是选了这个技能去查订单状态,结果返回里没有物流信息,模型又不知道接下来该干嘛。

我们后来把描述模板固定成了三段式:

技能:query_logistics 描述: - 触发场景:当用户询问包裹当前位置、配送进度、预计送达时间、物流异常时使用 - 目标任务:根据订单号或物流单号查询物流轨迹,返回当前位置和预计到达时间 - 反例边界:仅查询商品本身信息(价格、规格)时不要使用;用户没有提供订单号时,应引导用户提供后再调用

这个"反例边界"是重点。写描述的人总觉得要告诉模型"什么时候用",却忘了很重要的另一半:"什么时候别用"。加了反例之后,路由准确率提升非常明显。因为模型遇到相似场景时,反例能给一个负向约束,避免它凭感觉碰运气。

描述还有几个实操经验:

  • 每条描述控制在100~200字,太短语义信息不够,太长会挤占上下文窗口。
  • 描述里尽量出现用户会说的口语词,比如"收到""到哪了""签收""派送中",模型更容易命中。
  • 别把执行细节写进描述,比如"内部会调用某某接口",这些信息对路由没有帮助,反而干扰它理解技能边界。

2.2 参数Schema:先签合同再干活

技能要不要被调用,模型说了算;但参数怎么传,绝不能全交给模型自由发挥。我们是用JSON Schema把入参出参都做成"合同",模型生成的结构化参数必须通过校验才能进入执行阶段。

这里给一个简化版示例:

{ "name": "query_logistics", "description": "根据订单号或物流单号查询包裹物流轨迹", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,通常是字母和数字组合,必填,例如 PO20240101001", "pattern": "^[A-Za-z0-9]{12,20}$" }, "logistics_no": { "type": "string", "description": "物流单号,承运商提供的运单号,与order_id二选一" } }, "required": ["order_id"], "additionalProperties": false } }

注意几个细节。

参数的description里一定要写清格式和单位。别写"订单号",要写"订单号,格式为字母加数字共12到20位"。模型不是业务系统,它不知道订单号长什么样,你不说它就可能编一个。

additionalProperties设为false相当关键。不设的话,模型偶尔会多传一些根本不在schema里的字段,比如莫名其妙传个"remark"进去,轻则被业务系统忽略,重则直接报错。

入参校验还不够,出参也要有约定。我们早期只校验入参,结果技能内部调用第三方接口,返回的数据格式漂移了(比如"status"字段从"DELIVERED"变成了数字1),模型拿到这个结果再往后推理,整个链路就开始胡说。后来我们规定每个技能的返回必须经过一个"规范化"步骤:把外部接口的脏格式洗成统一的、语义明确的JSON对象,再喂给模型。

2.3 注册中心:技能不是散装代码,是受管资产

当技能数量超过20个,你会发现自己面临的不只是模型的路由问题,还有团队协作问题:A同学新加了个技能,B同学不知道;C同学改了技能的参数,D同学的流程还在按老参数传。于是技能注册中心成为体系里必须有的东西。

我们的注册表很简单,一开始就是一个Git仓库里的目录:

skills/ order/ create_order/ SKILL.md schema.json executor.py query_logistics/ SKILL.md schema.json executor.py after_sale/ refund_request/ SKILL.md schema.json executor.py

SKILL.md是给人看的说明,schema.json是给引擎校验用的,executor.py是执行逻辑。版本变更全部走代码评审,合并到主分支后自动发布到配置中心,引擎实时加载。

每份SKILL.md里的元数据字段,我们用了很长时间才稳定下来:

字段说明
name技能唯一标识,用"动词_领域_对象"命名,如create_order
version语义化版本号,如1.3.0
owner技能负责人,出了事找得到人
dependencies依赖的外部服务或上游技能
tags业务线标签,用于路由召回和权限控制
update_time最后一次变更时间

关于命名,强烈建议用动词开头。模型在语义空间里对"动词+名词"的匹配效果,远好于纯名词短语。create_order比order_creation在路由测试中准确率高出一截,这不是玄学,是LLM对动作型短语的理解天然更有指向性。

3. 技能引擎与编排:把一次调用变成一条稳定链路

3.1 引擎分层:路由、校验、执行、返回

有了技能定义,还需要一个引擎来承接"模型想调"到"真实跑通"之间的所有脏活。引擎我们做了四层:路由层、契约校验层、执行层、返回层。

路由层负责回答一个问题:用户当前这句话,到底想用哪个技能。早期我们把所有技能都塞进模型上下文,让模型输出一个tool_call。技能少的时候没问题,技能多了之后,光技能描述就有三四千个token,既浪费窗口,又互相干扰。后来我们改成"候选召回+再排序":先用轻量语义模型把所有技能描述做向量化,用户每轮请求来时算相似度,召回Top 5作为候选,再把候选技能的描述和用户消息一起交给主模型做精确选择。路由准确率反而更高了。

契约校验层要做的事,是把模型输出的结构化参数拿来和schema对撞,不合格就当场返回一个"参数错误"消息,告诉模型哪里错了、应该怎么改正,而不是直接抛异常让整个会话崩掉。我们会在校验层记录"参数拒绝率",这个指标后期成了技能质量的晴雨表。

执行层就是跑技能内部逻辑。这里有个早期踩过的坑:所有技能执行都硬编码在Python进程里,一旦技能逻辑里有耗时操作,整个服务就堵住。后来我们把耗时技能拆成异步任务,引擎收到"技能已受理"就返回给模型,等异步任务完成后再回填结果。

返回层的任务是把技能结果"提炼"成模型可用的摘要。不是把原始JSON丢回去,而是提取关键字段,组成一段简洁的文本或者结构化对象。比如物流查询返回了几百条轨迹记录,我们只提炼"最新状态、当前城市、预计送达时间、异常提示",其他全丢掉,上下文干净得多。

3.2 多轮交互与上下文控制

多轮是Agent技能体系建设里最容易被低估的部分。单轮调用模型表现好,不代表多轮能稳。我们观察到几个高频问题:模型调完一个技能之后,下一轮又调一次同样的技能(忘记结果已经拿到了);模型在技能返回异常时,会凭空捏造一个返回结果继续推理;模型在对话轮数变多后,会忘记之前已经确定的参数。

引擎层面我们做了三件事来缓解。

第一,技能调用结果要有"记忆标记"。每个技能返回时带上结果的短摘要,存入本轮会话的上下文摘要区,后续轮次模型能看到"上一次你已经查过物流,状态是派送中"这样的精简信息,就不用重复调用。

第二,异常返回时强制"纠正-确认"循环。校验层一旦发现参数错误或执行异常,返回的消息必须是结构化的:错误类型、可能原因、建议动作。模型被要求只能按这些信息来修正,不能自由发挥。

第三,限制单轮技能调用次数。我们给每轮用户请求设置了技能调用上限(默认5次),超过之后引擎会主动询问用户"当前问题可能需要更长时间处理,是否继续",防止模型在一个问题上自嗨式地反复调用。这个限制上线后,无效调用率明显下降,用户体验反而更好了。

3.3 技能编排:组合能力的两条路径

单个技能做得再好,用户的真实需求往往是多个技能的组合。技能编排有两种路径,我们两边都走过,各有适用场景。

一条路径是"代码编排",也叫做确定性工作流。比如"售后处理"流程:先调用核验订单技能,判断是否在售后期内;然后调用定责技能;最后根据定责结果调用退款或换货技能。这个流程里的分支判断全部由代码控制,模型只在和用户对话的最外层做决策。优点是稳定、可测试、好排查,缺点是写死了,流程一变就要改代码。

另一条路径是"模型驱动的动态编排",让模型自己决定技能调用顺序。这个更灵活,也能处理一些没有预设分支的开放需求,但代价是要把技能的输入输出设计成"正交"——上一个技能的输出字段,能自然映射为下一个技能的入参。否则模型把物流查询结果传给退款技能,两个技能的schema对不上,整个链路就断了。

我们现在的策略是混合使用:高频、流程固定的需求走工作流编排,追求稳定;低频、开放性的需求走动态编排,让模型自由组合。两者共用一个技能注册中心和校验层,所以无论走哪条路径,技能的质量保障都走同一套契约。

4. 技能库迭代:质量、版本与可观测性

4.1 给技能建一套测试金字塔

技能不能"能用就行",技能必须"变更可控"。我们把软件测试金字塔的思想搬进了技能库管理,分四层:

第一层是单元测试。针对技能内部的纯函数、参数Schema规则、边界条件做测试。比如订单号的pattern是否正确、必填参数缺了会不会正确拒绝。这层用最简单、跑得最快的用例覆盖,主要防止低级错误。

第二层是路由测试。这是技能体系特有的测试层。我们会构造一批"用户请求+期望技能"的样本集,每一条都写明:用户说了什么、历史上下文是什么、期望选中的技能是哪个、期望的参数是什么。路由测试跑的是"模型选技能"这一步,评估选得对不对。这层必须和模型版本绑定,因为不同模型的语义理解能力不同,可能A模型擅长这个路由,B模型不擅长。

第三层是集成测试。把技能的外部依赖mock掉,跑完整的技能调用链路,验证入参校验、执行逻辑、返回规范化都符合预期。这层不依赖真实第三方服务,所以可以在CI里稳定执行。

第四层是回归评估。我们会攒一个真实的用户需求集,覆盖各业务线的主要场景,定期跑一遍全技能链路,看整体成功率、路由准确率、参数正确率有没有下滑。这层最接近线上效果,但成本也最高,我们大概每周跑一次。

我的建议是技能库必须做到:没有单元测试不准合并主分支,路由测试样本量不少于100条。

4.2 语义化版本与灰度发布

技能是有生命周期的,它需要版本号、灰度发布、回滚机制。我们用语义化版本号,格式是主版本号.次版本号.补丁号:

变更类型版本号变化示例
修复内部逻辑,对外参数不变补丁号+11.2.0 -> 1.2.1
新增参数,或新增可选能力,向后兼容次版本号+11.2.0 -> 1.3.0
删除参数、改变返回值、改变触发语义主版本号+11.2.0 -> 2.0.0

主版本变更在系统里会被特殊对待:路由召回时必须排除新版技能,直到业务方确认旧版本彻底下线。我们见过一次事故,某技能把"status"字段的枚举值从"open/closed"改成了布尔值,下游五个工作流全跑飞了。从那以后,主版本变更一律要求同步更新所有引用方,少一个都不放行。

灰度发布我们做得比较简单:在路由层按比例把技能流量切到新版本。比如先放5%的实体流量,观察路由准确率和执行成功率,稳定后逐步放大到30%、80%、100%。出问题就一键回退到上一个版本,因为技能注册表是按版本存储的,回滚只是把路由权重切回去,几秒钟就能完成,不需要重新发布服务。

这里有个细节:技能的灰度不能只看"新旧版本都有调用记录",还要看"新旧版本的上下文一致性"。因为路由层的候选召回是语义匹配,如果新版本描述改了,可能导致原本会命中这个技能的用户不再被召回,流量就悄悄流失了。我们会在灰度期对比召回率变化,而不是只看成功率。

4.3 可观测性:从日志里发现技能问题

技能体系跑了一段时间,最容易积累的不是功能代码,而是"行为数据"。我们把技能的可观测性数据分为三类。

第一类是调用量指标:每个技能的调用次数、占总调用的比例、单会话平均调用轮数。这些数据告诉你用户真实需求集中在哪,也告诉你哪些技能可能是"僵尸技能"(定义了没人用)。

第二类是质量指标:执行成功率(技能内部逻辑没有异常)、参数校验拒绝率(模型传参不合规的比例)、路由准确率(人工抽样或用标注数据评估模型选技能是否正确)。参数拒绝率是很有意思的信号,它突然变高往往说明:要么模型的工具调用能力变弱了,要么技能描述和用户请求的语义对不上了。

第三类是问题样本:我们把每一条执行失败或参数被拒的记录都落库,包含用户原始消息、历史摘要、模型决策记录、技能返回结果。每周抽一批出来人工核对,整理成"技能改进清单"。这个过程虽然费人,但效率极高——很多技能描述的问题、schema的问题,都是这样被发现的。

可观测性做得好的另一个好处,是技能之间可以横向对比。我们曾经比较两个类似的技能,发现其中一个的调用成功率只有另一个的一半,点开问题样本发现,是它的描述里漏了一个高频触发词,用户说"看看货到没到"时模型总不选它。补上触发词后,成功率直接拉平。这种问题不做数据回放,根本定位不到。

5. 实战中踩过的坑:技能体系落地时的高频事故

5.1 描述写得太"像说明书"

技能描述最常见的坑,是写作者站在系统视角,而不是用户视角。我们早期有个"查询账单"技能,描述写的是"查询用户的账单明细及消费分类汇总",结果用户说"我这个月怎么花了这么多钱"时,模型就是不调用它,反而去调了一个"用户画像"的技能,返回了一堆完全无关的数据。

后来把描述改成"当用户询问某时间段内的消费金额、消费明细、钱花在哪了、为什么花费较多时使用",路由一次就中。这背后的逻辑是:模型匹配的是语义意图,不是业务名词。你必须把它翻译成用户真正会说的话。建议每个技能描述写完,先丢给不熟悉业务的人看,让他模拟用户问几个问题,看看能不能自然联想到这个技能。

5.2 参数校验只在入口做,忘了出口

这是另一类高频事故。技能入参校验通过,执行也正常,但返回结果直接喂给模型后,模型跟根据返回内容编造下一步。比如某个查询接口在某个用户场景下返回的"remark"字段是空字符串,模型的后续推理就会把"没有备注"脑补成"该用户没有特殊需求",然后自作主张走了一步不该走的流程。

我们的解决方案是给技能返回做"语义结构化"。每个技能必须返回三个固定字段:status(明确状态:SUCCESS/FAILED/PARTIAL)、summary(一句话结果摘要)、detail(结构化详情,没有值时用null而不是空字符串)。这套规范让模型接收到的永远是干净、无歧义的结果,不会再自己脑补。

5.3 技能粒度没收敛,路由就崩了

技能体系刚上线时,我们的技能数量一度达到80多个,因为每个开发者都习惯按"接口"建技能:查用户信息建一个,查用户地址建一个,查用户等级又建一个。结果模型看到这么多相似技能,路由开始疯狂出错。

后来我们做了一次技能合并。把"查用户信息、查用户地址、查用户等级"合并成一个"查询用户资料"技能,内部返回包含所有维度,由技能内部按参数决定查哪些。技能数量收缩到40个以内之后,路由准确率出现了肉眼可见的回升。这和搜索系统很像:索引太大、噪声太多,召回精度就会掉。技能的粒度应当瞄准"用户能感知的任务类型",而不是"后台的接口边界"。

5.4 外部依赖一变,技能行为就漂移

技能内部调用第三方接口时,最大的不稳定因素不在代码,而在上游接口。我们遇到过一次,某物流查询接口把"派送中"的返回码从"DELIVERING"改成了"OUT_FOR_DELIVERY",技能没有做映射,模型拿到新码直接不认识了,导致整条客服流程断掉。

这里给个硬性建议:任何技能访问外部依赖时,必须在技能内部做一次"领域模型转换",把第三方返回转成自己定义的统一枚举,而不是原样透传。还要给外部调用配置超时和降级策略。超时时间不要设太长,我们一般取500毫秒到1秒,超过直接返回"技能暂不可用",并让Agent告知用户稍后再试。这听起来是常识,但真的很多技能是把第三方返回直接喂给模型的,出问题只是时间问题。

6. 再往前走:技能自动发现与控制面

6.1 轻量路由召回,减少主模型负担

技能数量增长到一定规模后,把所有技能描述都塞给主模型做路由选择,已经不是最优解了。我们在第三节提过"候选召回+再排序"的方案,这里再展开讲一下实现细节。

召回我们用embedding向量相似度,就是把每个技能的描述文本编码成向量,用户的当前请求、历史摘要拼起来也编码成向量,然后用余弦相似度取Top N候选。这一步可以完全脱离主模型,token消耗极低。再排序阶段,把候选技能名单、技能描述、以及用户消息一起提交给主模型,让它输出最终选择的技能ID和结构化参数。

这个方案还有个额外的好处:可以在召回阶段做权限控制。比如某个技能只允许特定用户群体使用,就在召回结果里直接过滤掉,主模型根本不知道有这技能存在,比让它选完再拒绝要干净得多。

6.2 受控的"技能自生成",与人工审批结合

最后聊一个我们正在探索的方向:让Agent自己从对话中总结出可复用的技能。场景很常见:用户反复让Agent做"把某个报表导出、加个表头、再发给收件人"这类固定动作,每次都是几步调用的组合。如果能让Agent把这种多步操作抽象成一个新技能,下次用户一句话就能触发,效率会高很多。

但我们始终没有做全自动的技能生成。原因是技能一旦进入注册表,就会成为公共资产,会影响所有用户的会话。如果Agent自生成的技能有bug、有安全漏洞、或者参数设计得不合理,后果会被放大很多倍。我们现在的做法是:Agent只负责提出技能草案(描述、Schema、执行步骤),进入一个"待审批"队列,由开发人员审核、补测试、跑回归,通过后才上线。沙箱验证和人工审批两道卡控,缺一不可。

这其实也回到agent-skills最初的设计理念:技能是体系的一等公民,它的全生命周期都需要被管理。别让未经管理的技能悄悄混入生产环境,这是我对所有做Agent体系的人最想说的一句话。

做agent-skills这套东西到现在,我最大的体会是:大模型的能力会越来越强,但应用层的"能力治理"不会自动变好。工具和技能的组织方式,决定了一个Agent的上限。你把技能的定义、校验、测试、版本、观测都管好了,模型的聪明才有地方使;这些管不好,再大的模型也只是把错误犯得更流畅。如果你也在搭自己Agent的技能库,建议先把描述模板、Schema校验、路由日志这三件事做扎实,然后再去考虑那些更花哨的编排和生成能力。这三件事,是整套体系的承重墙。

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

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

立即咨询