首页 / 资讯中心 / 文章详情

渐进式技能管理系统:Agent工具层的统一描述与调用实践

渐进式技能管理系统:Agent工具层的统一描述与调用实践 ★ FEATURED ARTICLE
几个月前我接手了一个内部项目的重构工作状况可以说相当典型团队里多套 Agent 服务各自维护着一套工具调用逻辑有的用 JSON 声明输入输出有的直接在提示词里塞自然语言描述还有的干脆把配置文件写死在代码仓库里。功能明明都在代码库里却没人敢复用新人想接入一个新能力光读文档就要花掉半天。这个混乱的局面试了一段时间后我彻底意识到我们缺的不是更多工具而是一套能让所有技能被统一描述、统一发现、统一调用的组织方式。于是就有了 Hermes Skills System 这套围绕“渐进式技能”和“Skills Hub 开放标准”搭建的技能管理方案。这篇文章我会从设计动机、技能分级、开放协议、实操接入和踩坑记录几个角度展开如果你也在搭建 Agent 工具层、插件系统或者正在为团队的技能库做规范化应该能从中找到一些可以落地的东西。1. 为什么需要一套“会自我成长”的技能系统先聊一个很多人容易忽略的问题工具、技能、插件、API这些词在日常沟通里经常被混着用但在一套系统设计里它们的含义完全不同。API 是接口层面的东西插件是代码打包和分发的单元而技能更像是一个“能被 Agent 理解并完成某类目标的能力描述”。我们必须先把技能当成一等公民来看待否则后续所有设计都会走形。我见过太多的技能系统本质上就是把一堆 API 封装层堆在一起加个注册中心和鉴权就对外宣称是“技能平台”。这样的系统有个致命的毛病它只能承载已经完整实现好的能力没有办法描述一个能力从简单到复杂的演进过程。实际项目里一个技能很少是一蹴而就的它往往先是一个单点能力用着用着发现需要组合其他技能再跑一段时间又发现应该根据上下文调整行为。传统插件体系对这种演进非常不友好因为每一次升级都意味着重新设计接口、重新发布版本、重新让调用方适配。Hermes 的出发点就是想解决这个问题。我们把技能定义为一种具备生命周期的实体它允许同一个技能在不同的成熟度等级上被描述、被调用、被共享。这就是“渐进式技能”的核心含义不是让所有技能在一开始就追求大而全而是允许它从最小可用状态开始随着需求和反馈逐步升级。与之配套的 Skills Hub 则负责解决开放共享的问题一个团队内部可能有几十个技能散落在不同的代码库和服务里怎么让别人发现它们、信任它们、组合它们靠口头沟通是不行的必须有一套机器可读的开放标准。我自己比较喜欢用生活化的类比来解释这套设计传统插件系统像一本印刷好的产品目录每一页都固定了内容你要新增一个功能就得重新印一页而 Hermes 更像是给每个产品配了一张可以不断更新的“能力卡片”卡片上的信息有统一的格式任何人在任何时间都能查看这张卡片当前的状态并且知道下一步它可以长成什么样。这个差别看起来很细微实际使用中体验完全不同。当然定义一套技能系统很容易陷入过度设计的泥潭。我们在落地 Hermes 的时候有一条铁律所有概念必须能在一个下午之内用代码验证完。如果某个机制光讲清楚就要花超过半小时那就说明它不够渐进需要拆得更细。这条铁律帮我们挡掉了不少华而不实的设计。2. 渐进式技能的三个层级从原子操作到自适应能力要理解渐进式技能最关键的是看它的分级方式。我们最早参考过一些编排引擎的做法试图用复杂的状态机来覆盖所有场景结果发现维护成本极高大多数贡献者根本不愿意按那么重的规范写技能。后来团队内部反复讨论了几轮终于收敛出一个足够简单、又能覆盖绝大多数场景的三级模型。2.1 Level 0原子技能唯一职责的独立能力Level 0 是技能的初始形态也是整个系统的基础单元。它只做一件事输入输出足够明确不依赖其他技能。比如“查询订单状态”“计算两个日期之间的天数”“把文本翻译成英文”这类能力天然适合作为 Level 0。一个 Level 0 技能的核心特征就是它能够独立被调用并且调用它不需要理解任何外部流程。我们用一份轻量级的技能描述文件来声明一个 Level 0 技能文件核心字段包括技能 ID、唯一名称、用途摘要、输入参数 Schema、输出结果 Schema、权限要求、运行入口。下面是一个简化到极致的示例{ id: skill-order-status, name: 查询订单状态, version: 1.2.0, level: 0, description: 根据订单号查询订单当前的处理状态, input_schema: { type: object, properties: { order_id: { type: string, description: 订单号格式为 14 位数字 } }, required: [order_id] }, output_schema: { type: object, properties: { status: { type: string, enum: [pending, paid, shipped, completed, cancelled] }, updated_at: { type: string, format: date-time } } }, permissions: [order.read], endpoint: api://order-service/status }这里有个容易被新手忽略的点所有技能描述文件都必须放在 Skills Hub 统一管理不能散落在各自服务的仓库里。因为只有集中管理检索、版本对比和权限审计才有意义。我们当时把散布在五个仓库里的工具描述收集到同一个 Hub光是这一步就发现了至少三个“同名不同义”的接口整理完发现有些技能直接被重复实现了两遍。2.2 Level 1组合技能把原子技能编排成可用流程Level 1 技能指的是由多个技能组合而成的编排能力它体现的是“流程复用”。单看 Level 0 技能能解决的是一个个孤立的问题但在真实业务场景里Agent 很少只需要调用一个函数。举个例子一个“处理退货申请”的能力可能需要“查订单状态”“校验退货窗口”“计算退款金额”“生成退货单”四个步骤依次执行。如果你在每次对话逻辑里都重新写一遍这个流程代码很快就会烂掉。Level 1 技能在描述文件里多了一个workflow字段用来声明子技能之间的调用顺序和数据传递关系。为了保证简单性我们没有引入完整的状态机语言而是约束为一种线性流程加条件分支的 DSL类似这样id: skill-return-process name: 退货申请处理 version: 0.9.0 level: 1 workflow: - step: query_status skill: skill-order-status with: order_id: ${input.order_id} - step: check_window skill: skill-return-window with: status: ${steps.query_status.output.status} purchase_date: ${input.purchase_date} if: steps.query_status.output.status completed - step: calculate_refund skill: skill-refund-calculator with: order_id: ${input.order_id} reason: ${input.reason}这个 DSL 极其克制它只支持顺序执行、条件判断、变量引用和简单的数据转换。我们刻意不支持循环结构因为在技能层面引入循环会让调试难度陡增而且大部分真实流程用顺序加分支就足够了。如果某个流程确实需要复杂的循环和异常重试那通常意味着它应该被实现成一个原子技能而不是在编排层硬堆。在 Skills Hub 里Level 1 技能会被标记为组合技能并且 Hub 会自动建立一张依赖图。发布的时候系统会检查你引用的子技能是否存在、版本是否兼容以及有没有形成循环依赖。这个自动检查机制是 Hub 开放标准里非常实用的一个功能后面踩坑章节我还会专门展开。2.3 Level 2自适应技能基于反馈持续调整行为Level 2 是渐进式技能里最“进阶”的形态也是设计争议最大的部分。简单来说Level 2 技能允许技能在执行过程中根据历史反馈数据动态调整自己的参数、策略、甚至是内部编排路径。这一层想表达的理念是技能不应该永远是死的它可以慢慢变得聪明起来。但必须泼一盆冷水Level 2 不是让你在技能里塞一个模型跑推理那样会带来不可控的延迟和成本。我们在 Hermes 里对 Level 2 做了严格的约束——技能只能自带一个可插拔的“策略函数”策略函数的输入是历史执行摘要输出是对某个参数或流程分支的调整建议而且建议必须在白名单范围内生效。例如一个“商品推荐”技能初始时固定返回 10 条结果跑了一段时间后策略函数发现低评分用户更偏好简短列表于是可以动态把结果条数调整为 5。这不是让技能突然变成另一个技能而是让它在既定范围内自我优化。Level 2 技能的描述文件多了一个adaptation字段里面声明策略函数的入口和允许调整的参数边界{ id: skill-recommend, name: 商品推荐, level: 2, adaptation: { strategy_endpoint: api://recommend-service/strategy, tunable_params: { top_k: { type: integer, min: 3, max: 20, default: 10 }, sort: { type: string, enum: [default, price_asc, rating_desc], default: default } }, feedback_window_days: 7 } }实际运行的时候Skills Hub 会把历史调用结果中脱敏的反馈信号比如用户是否采纳、执行是否成功、耗时指标汇总起来周期性地喂给策略函数然后由策略函数在tunable_params允许的范围内产出新的参数建议。由于边界被提前锁死即使策略函数写得再激进也不可能把技能行为改得面目全非。在团队内部讨论时有人提议过要不要加 Level 3比如“自学习技能”——让技能自己写代码生成新技能。我当时投了反对票因为以目前工程体系的成熟度这种开放会带来几乎无法控制的安全和审计风险。渐进式不代表无限演进定义好天花板本身也是开放标准的一部分。3. Skills Hub 的开放标准一份技能描述文件解决发现与调用聊完技能本身的层级接下来是 Skills Hub 这个开放标准真正发力的地方。很多人以为 Hub 就是一个技能列表页面加上传按钮其实开放标准的精髓在协议设计而不在界面。3.1 一份机器可读的 Skill ManifestHermes 的技能描述文件也就是 Skill Manifest是整个开放标准里最核心的部分。它有四个设计原则自描述、可验证、可发现、可演进。自描述的意思是任何人拿到一份 Manifest不需要额外文档就能知道这个技能接受什么输入、返回什么结果、需要什么权限。可验证是系统层面的要求Hub 在接收 Manifest 时会执行校验包括字段格式校验、引用完整性校验、依赖版本校验。可发现解决的是检索问题Hub 维护着基于用途摘要和参数描述的索引支持按领域标签、技能等级、依赖关系进行过滤。可演进就是我们前面说到的渐进式一个技能可以在同一个 ID 下升级 level而不需要换一个新的技能名。以我们的经验来看Manifest 里最容易写烂的字段是description和input_schema里的参数描述。很多人会用“处理数据”这种毫无信息量的话当摘要或者把参数描述写成“任意值”结果就是检索时搜不到调用时 Agent 也不知道该怎么填参数。我们在 Hub 的校验逻辑里专门加了一条软性检查描述文本长度小于 15 个字符、参数描述缺失超过一半时Manifest 会被标记为“低质量”并退回给贡献者。3.2 统一调用协议让不同语言的技能插拔即用开放标准要解决的另一件事是调用协议。团队内部有 Python 写的服务也有 Node.js 写的服务还有一部分是 Go 写的。如果每个服务都用自己的风格暴露接口Hub 就无法做统一调度。所以我们约定了一套基于 HTTP JSON 的调用协议要求所有技能通过 POST 方式暴露统一入口POST /v1/invoke Content-Type: application/json { manifest_id: skill-order-status1.2.0, trace_id: abc123, arguments: { order_id: 20250701000123 } }响应格式也做了统一规定所有技能不管内部逻辑如何返回结构必须包含success、output、error三个顶层字段。这样上层 Agent 在编排时就不需要关心每个技能各自的返回格式差异大幅简化了调用层的代码。有人可能觉得这太死板不够“开放”。但我的体会是开放标准的第一要义不是自由而是互操作。如果没有一个共同的协议底线每加一个新技能调用方都要写一段适配代码那 Hub 的规模越大维护成本反而越高。把底线约束好才能让更大范围的自由成为可能。3.3 发现机制的隐藏细节能力声明标签体系如果只是靠检索文本框技能复用还是不够顺畅。我们后来在 Hub 里加了一套标签体系比起全文搜索标签体系的效率高很多。每份 Manifest 都必须声明领域标签比如订单、库存、支付、风控、能力动词查询、计算、生成、转译、运行环境要求延时敏感、批处理、GPU 依赖这三组标签。这里想提醒一个细节很多人设计标签体系时喜欢直接复用业务部门的组织架构命名结果标签和业务线强耦合跨业务搜索时完全失效。我们的做法是让标签尽量描述能力本身而不是描述归属部门。比如“订单”标签对应的是“订单领域的数据能力”而不是“订单部门的内部工具”。这个语义区别如果不在一开始讲清楚后面标签会越打越乱。4. 半小时接入实战从零到一挂载一个技能理论讲得再多最终都要落到代码上。我拿一个真实场景演示一遍把团队里一个本来就存在的“汇率换算”服务接入 Hermes让它变成一个可以在 Skills Hub 上被检索、被调用的 Level 0 技能。整个过程在一段时间反复操作之后基本可以稳定在半小时左右我把它拆成五个步骤。4.1 准备技能运行入口第一步不是写 Manifest而是先确保技能本身可以被统一协议调用。我们的“汇率换算”服务原本暴露的接口是GET /api/convert?amount100fromUSDtoCNY这不符合 Hermes 的调用协议。所以我在服务里加了一个适配层把原来的接口包成统一入口app.post(/v1/invoke, async (req, res) { const { manifest_id, arguments: args } req.body; if (manifest_id ! skill-currency-convert1.0.0) { return res.status(404).json({ success: false, output: null, error: skill not found }); } try { const result await legacyConvert(args.amount, args.from, args.to); res.json({ success: true, output: { value: result.value, rate: result.rate }, error: null }); } catch (err) { res.json({ success: false, output: null, error: err.message }); } });这一步的思路是老业务代码不要动用适配层隔离协议差异。后面就算 Hermes 的协议升级我们也只需要改适配层业务逻辑不受影响。这个隔离思想是我特别想强调的很多团队接入时喜欢顺手重构业务代码结果把风险面扩得很大。4.2 编写并提交 Skill Manifest接着写 Manifest 文件。因为这个技能逻辑简单我把 level 设成 0输入参数定义了amount、from、to三个字段输出则包含换算后的金额和当时的汇率。提交到 Hub 之后Hub 会跑一遍自动校验有一次我忘记声明permissions字段直接被校验器拦住了提示“缺少权限声明默认拒绝所有调用”。这个机制一开始觉得麻烦后面发现它确实能防止很多安全问题。Manifest 提交后Hub 会给这个技能分配一个唯一 ID我用的是skill-currency-convert1.0.0。这里后面的版本号是必需的因为同一个技能可以渐进式地升级版本是区分不同形态的锚点。4.3 声明 Skill ID 与权限边界权限这一块展开说一下。Hermes 的技能权限模型非常简单每个技能只能声明它确实需要的最小权限集合比如order.read、payment.refund。Hub 有一个权限审批流超过一定敏感级别的权限需要团队技术负责人确认而不是开发者自己勾选就行。不过要注意技能权限和调用者的身份权限是两套系统。技能可以拥有读取某个数据的权限但并不意味着任何 Agent 都能借这个技能去读数据。调用者在请求时还需要带上自己的身份凭证由网关做一次统一的鉴权映射。这个双层鉴权机制一开始让团队不太适应但经历过一次误操作之后大家就明白它的价值了有个测试用的技能被误配了写权限但由于调用侧的身份校验先拦住了没有造成数据污染。4.4 在 Skills Hub 发布并测试调用Manifest 通过校验和权限审批后就可以在 Hub 里发布上线。发布不是简单地把 Manifest 状态改成 activeHub 还会做一次“试调用”用一个无副作用的测试用例跑一遍真实接口确认返回结构符合统一协议。如果试调用失败Hub 会拒绝上线。我建议每个人的接入清单里都加上这一步发布之后先用 Hub 的在线调试工具跑几个边界参数比如金额为 0、币种代码不存在、精度极高的小数。这些边角情况最容易暴露出适配层的隐藏问题。我就在调试时发现原服务的汇率接口在传入非法币种时会返回 200 状态码加一个业务错误码这个行为在接入适配层时很容易丢失导致 Hermes 层误判为成功。4.5 通过 Hub 检索并验证对外可见最后一步是确认技能能被正常检索到。我用另一个账号模拟“新同事”的角色在 Hub 搜索框里输入“汇率”看能否返回skill-currency-convert。如果搜索结果里没出现多半是标签或摘要写得不好。好的摘要应该直接表明能力而不是写“提供货币相关的辅助功能”。做完这五步一个技能就算是正式挂载完成。整条链路走通之后后续再接入新技能基本属于复制粘贴改参数级别的重复劳动。5. 落地三个月后踩过的坑版本、校验与兼容性问题任何系统跑起来之后才会暴露真实问题。Hermes 上线运行三个月后我们把遇到的问题整理了一遍发现大部分坑都不在概念设计而在工程落地细节。挑几个典型的说一下如果你在参考这套思路大概率也会遇到类似情况。5.1 Manifest 与实现代码脱钩最大的坑是这个Manifest 声明的是 A 行为实际代码实现的是 A 加一点 B。有一次排查线上问题一个“订单批量导出”技能突然开始超时查了半天发现 Manifest 里声明预计执行时间不超过 10 秒但底层实现代码已经被改了逻辑新增了一个子查询导致真实执行时间飙升到 40 秒。由于 Manifest 没有被同步更新调度器对它的预期完全错了。这个问题一次两次靠人工盯还能扛但量大了之后必须靠机制解决。我们后来设计了一套“Manifest 路由回访”策略Hub 定期拿 Manifest 里的测试用例跑真实接口把响应结构和声明的 Schema 做一致性比对只要不匹配就自动告警。这个机制不能解决所有歪斜但确实把脱钩的暴露时间从“用户发现”提前到了“系统发现”。5.2 输入输出 Schema 定义太“宽容”另一个高频问题是 Schema 写得过于宽松尤其是输出字段的可选性。很多贡献者抱着“多给点信息总没错”的心态把大量字段设成 optional导致调用方无法确定某个关键字段是否一定存在。最经典的例子是某个“用户画像查询”技能声明输出里的risk_level是可选字段上层 Agent 拿不到这个字段时会发生空指针异常但错误的根子在技能方的 Schema 声明。为了缓解这个问题我们在 Hub 的规范里加了一条硬性规则所有对业务分支逻辑有影响的字段必须设为 required真正的 optional 字段只能用于附加信息。同时 Skill Manifest 校验器增加了一个 lint 规则如果 required 字段在描述里出现“可能为空”“不一定有”等字样会直接标记为定义矛盾。5.3 组合技能的循环依赖检测不够严格组合技能的依赖图在初期规模不大时看不出问题但当技能数量超过 100 个时循环依赖就开始滋生了。我们遇到最隐蔽的情况是 A 技能依赖 BB 依赖 C而 C 又通过一个间接的 Level 2 自适应策略回调了 A。由于自适应策略的调用发生在运行时静态的依赖扫描根本检测不到。这促使我们的 Hub 增加了一个“运行时依赖上报”机制每个技能在执行时要把实际调用过的其他技能 ID 记录进 trace 日志Hub 定期做离线分析构建运行时依赖图并和静态声明做对比。第一次跑分析的时候系统列出来的隐含依赖让我们所有人都吃了一惊。从那以后每次发布组合技能挂掉的概率大幅下降。5.4 自适应技能的反馈信号噪声过大Level 2 技能在真实业务里遇到的直接问题是反馈信号太脏。我们原本的设想是收集“用户是否采纳结果”这类信号但实际落地上发现用户不采纳很可能不是因为结果不好而是因为会话上下文已经变化。把这种噪声信号喂给策略函数会导致参数建议来回抖动技能表现反而更不稳定。我们的应对方案很朴素给反馈信号加置信度过滤只有连续出现多次同向反馈时才允许策略函数调整参数单次波动只记录不动作。同时给tunable_params里每个参数增加了步长限制每次调整幅度不能超过默认值的 20%。加了这个约束之后自适应技能才真正从“振荡器”变成了“缓斜坡调节器”。5.5 版本兼容策略的缺失期最后一个是版本管理问题。Hermes 的早期设计里版本号只是用来区分 Manifest 的没有考虑调用方的兼容性。后来有一次某个技能从 Level 1 升到 Level 2Manifest 里新增了adaptation字段导致所有调用方的客户端在解析时直接报了 Schema 校验错误。问题本质是调用方代码里用了严格的“未知字段即报错”策略。这次事故之后我们明确了一条版本演进规范同一技能在升级级别时新增字段必须标记为 optional禁止删除或改变已有字段的语义涉及破坏性变化时必须通过新版本号发布并在 Hub 上保留旧版本的至少 30 天兼容运行期。如果你也在做类似系统这一点建议从一开始就写进规范里别等出了问题再补。6. 留给未来的一点开放空间如果让我总结 Hermes Skills System 这套设计里最值得借鉴的东西我会说不是某个具体的协议字段而是“渐进式”这三个字背后的工程取舍允许技能从最小可用状态开始但给它一条清晰的成长路径开放标准的表面是格式统一深处是对兼容性和可演进的尊重。这套设计思路并不局限于某个具体技术栈你在自己的项目里完全可以裁剪出适合团队规模的最小版本。我自己的体会是这类系统最难的从来不是写代码而是让团队里每个技能贡献者都理解并认同这套规则。技术手段只能约束机械行为真正让标准活起来的是大家的共同维护。好在 Skills Hub 的反馈机制跑顺之后贡献者能明显感觉到自己的技能被更多人复用了这种正向反馈比任何行政命令都管用。你如果也在搭类似体系不妨从一个小范围的试点开始先把三个 Level 0 技能完整跑通再慢慢往上搭建。路是一步步走出来的系统也是。
阅读完成 · 觉得有帮助?
咨询建站