☰
Agent技能系统设计指南:从元数据到编排的工程实践
2026/10/13 4:28:52 网站建设 项目流程

思考了一下,我决定将“agent-skills”解读为智能体技能系统的设计与工程实践。这个方向在当下Agent应用爆发阶段非常核心,很多人做 Agent 都是从“写一堆函数让LLM调用”开始,但做到一定规模就会撞上描述混乱、检索不准、编排困难、难复用这些问题。所以我把这篇博文定位为:从零开始设计一套Agent技能系统的实操总结,覆盖元数据建模、描述规范、检索、编排、评估、上线监控与复用,最后给出扩展方向。内容围绕“技能”(Skill)展开,直接、工程化、可落地。 最早接触“agent-skills”这个概念,是在做某智能客服系统的时候。当时我们给大模型接了几十个工具函数,从查订单到改地址,从算运费到申请发票,看起来什么都能干,可真一跑起来就乱了:模型经常把“改收货地址”和“查历史订单”搞混,参数传错、描述冲突、功能重叠,改了这个坏了那个。后来我们彻底重构,把散落的工具函数统一封装成一套带元数据、带校验、带版本、带检索的“技能系统”,问题才真正解决。

这个“agent-skills”,简单说就是把智能体要执行的原子能力——查天气、算价格、发消息、调API——从裸函数升级为标准化、可注册、可检索、可编排的结构化“技能”。它解决的痛点是:当智能体的能力从三五个工具膨胀到几十上百个时,光靠提示词“告诉模型有哪些函数”已经彻底行不通了。你要么提供一套能让模型精准发现、理解、组合这些能力的机制,要么眼睁睁看着智能体在工具海洋里迷失方向。

对于正在做Agent类应用,尤其是ChatBot、自动化工作流、AI助手类产品的开发者来说,这套技能系统的设计思路和落地避坑经验都有直接参考价值。哪怕你只接了五六个工具,提前用技能的思路去组织,也能避免后续大面积返工。下面我按我们项目从设计到上线的实际顺序,把整套方案拆开讲。

1. 为什么Agent需要一套技能系统

1.1 技能与普通工具调用的本质区别

很多团队起步阶段就是简单粗暴:把每个功能写成一个Python函数,然后在System Prompt里列一行“函数名 + 一句话说明”,再配上JSON参数格式。这种“工具调用”方式在小规模下确实能跑,但当函数列表变长,问题就暴露了:

  • 函数说明只能写在Prompt里,每增加一个函数都会占用宝贵的上下文长度;
  • 说明和参数Schema混在一起,模型既要用它理解功能,又要用它生成调用,负担极重,准确率明显下降;
  • 功能相似的函数(比如“取消订单”和“申请退款”)在描述上稍有含糊,模型就会选错;
  • 函数之间没有办法组合,遇到复杂任务只能靠模型多次调用硬撑,一旦中途失败,整个对话就断了。

而技能系统,是把这些散兵游勇变成正规军。每一个技能不再只是一个函数,而是一个完整的“能力单元”:有唯一标识、有面向模型优化的描述文本、有严格的入参出参Schema、有版本号、有标签、有归属权限,甚至还有独立的测试用例。它把“工具”从代码层面提升到了“产品/系统”层面。

这里借用一个人力资源领域的类比,特别贴切:裸工具调用就像你发了个招聘帖,只写了岗位名称;而技能系统是完整的JD——岗位职责、任职要求、考核标准、汇报关系都写得清清楚楚。HR(也就是大模型)拿到JD,才能准确筛简历、定薪资、谈offer,而不是靠猜。

1.2 技能系统要解决的核心问题

我们的目标非常明确,就是解决三大问题:

第一,可发现性。让大模型在几十上百个技能中快速、准确地找到最合适的那一个。这要求技能描述像电梯演讲一样精炼、准确、有区分度,而不是长篇大论的技术文档。

第二,可复用性。同一套技能既能在聊天机器人里被调用,也能在自动化流程里被编排,还能被不同的Agent共享。技能不能和某个业务流程写死,要足够独立。

第三,可治理性。谁注册了技能、哪个版本在线、调用成功率和失败原因是什么、某次线上故障是不是某个技能参数变更引起的,这些都要能追溯、能监控、能回滚。

这三点,是普通工具函数完全不具备的。在没有技能系统的时候,我们调试线上问题靠翻日志;有了技能系统后,直接在注册中心里查技能版本、查调用链、查相似度匹配结果,几分钟就能定位根因。

1.3 一个技能的本质是什么

一个正式注册的技能,包含三个核心部分:技能描述(Skill Description)、执行体(Implementation)和校验规则(Validation)。

技能描述是给大模型看的“说明书”,决定了模型能不能在正确时机选中它;执行体是真正干活的代码或API封装;校验规则则是执行前后的双保险——执行前校验参数合法性,执行后校验返回结构完整性。这三个部分缺一不可:只有描述没有执行体,那是PPT;只有执行体没有描述,模型发现不了;两者都有但没有校验,出错只能靠人擦屁股。

这么多项目做下来,我愈发觉得,技能系统的设计本质上是信息架构设计。你得先想清楚模型的“认知负担”有多大,然后通过标准化、元数据化、检索化的手段,把这个负担降到最低。这不是写代码,这是做信息的组织艺术。

2. 技能注册中心与描述规范设计

2.1 技能元数据模型

技能系统落地的第一步,是定义元数据模型。我们经过多轮迭代,最终定型为以下核心字段:

字段类型作用示例
skill_idstring全局唯一标识order.query_v2
namestring技能简称订单查询
descriptionstring面向LLM的自然语言描述按订单号或手机号查询用户的订单状态、物流信息及商品明细
versionstring语义化版本号2.1.0
tagsstring[]支持标签检索["订单", "售后", "只读"]
params_schemaobject入参JSON Schema见下文
returns_schemaobject出参JSON Schema见下文
rate_limitinteger每分钟最大调用次数120
timeout_msinteger超时时间3000
enabledboolean是否在线true
owner_teamstring负责团队/开发者订单组

这个模型里,最关键也最容易写烂的,就是description和两个schema。

description要避免两类常见错误。一类是过度抽象:“该技能用于处理订单相关业务需求”——完全等于没说,模型根本分不清它和取消订单、修改订单有什么区别。另一类是写成长篇小说,把内部实现细节、数据库字段、微服务架构全塞进去,这既浪费token,又干扰模型判断。我们的经验是,好的描述应该遵循“目标用户 + 触发条件 + 核心功能 + 关键限制”的四段式结构。

举一个标准样例:

当用户需要查询自己已下订单的当前状态、收货进度或预计送达时间时使用。支持通过订单号、手机号、用户ID三种方式定位订单。此技能为只读操作,不会修改任何订单数据。若用户未提供任何定位信息,请先向用户询问订单号或手机号。

这里尤其要写明“不做什么”,这能极大减少乱用误用。模型在模糊意图时会优先选择限制描述清晰的技能,这是一个非常实用的经验。

2.2 技能描述怎么写才不会被LLM误选

我们在线对比测试过两个版本的description,效果差异让人震惊。

初版这样写:“查询订单相关数据,包括订单状态、物流信息、商品列表、支付金额,支持各种条件查询,如订单号、手机号、用户ID,返回结果特别丰富,非常全面。”

这个描述导致模型频繁把“我要改收货地址”也调用了查询订单技能,因为描述里包含了太多“覆盖面广”的词,模型误以为这是一个万能的订单技能。后来改成了狭窄、精确的写法:“仅在用户需要查询(非修改)订单现状时使用。可用于:状态查询、物流进度查询、预计送达查询。不可用于:地址修改、订单取消、支付退款、申请售后。” 改完后的召回准确率直接从78%升到了96%。

这里面有个反直觉的规律:给技能写上明确的“不可用于”清单,比堆砌“可用于”更有效。因为LLM生成调用时,本质上是在做排除法,你帮它把边界划清楚,它选择时就不纠结了。

另外还有一个小技巧:技能描述不要重复技能名。如果你把技能名order.query_v2又原样写进描述里,就等于浪费描述空间换一堆无效token。真正有价值的是那些“技能名看不出来”的信息:适用场景、触发条件、行为边界。

2.3 参数Schema设计要点

入参参数Schema直接决定模型能不能生成合法调用,这一块容易踩的坑比代码还多。核心原则是:参数Schema要像一份结构化的用户需求收集问卷,而不是一个后端接口的入参复制粘贴。

比如,某订单查询技能初版的参数Schema是:

{ "order_id": {"type": "string", "description": "订单号"}, "user_id": {"type": "string", "description": "用户ID"}, "phone": {"type": "string", "description": "手机号"}, "page_num": {"type": "integer", "description": "页码"}, "page_size": {"type": "integer", "description": "每页数量"} }

看起来没什么问题,但实际调用时模型经常犯一个错:当用户只说了“帮我查一下最近买的那个充电宝到哪了”,模型不知道该填哪个字段,于是随机填了一个user_id,而这个user_id在上下文里根本不存在。后来我们调整了两处:

  • 增加required标记,并声明“至少提供一个:order_id / user_id / phone”;
  • 将description改写成“当用户提及订单号时填order_id,提及手机号后四位时填phone,已登录场景优先用user_id”。

换句话说,参数Schema的description不是给后端工程师看的,是给大模型看的。你要在每一个字段描述里写清“怎么从对话里提取这个参数”,而不是写“该字段表示订单号,长度20位”。

出参Schema的设计也影响体验。我们遇到过模型成功调用了技能,但返回结果的字段名和用户问题对不上,导致模型误判“查不到”。把returns_schema里的字段也写成“自然语言检索式”,比如在返回字段eta的描述中写“预计送达时间,格式为YYYY-MM-DD HH:mm,若延迟送达请标注slow”,模型才能准确组织回复语言。

2.4 技能版本与权限治理

技能是有生命周期的。某个技能上线后,老板说订单号格式从“ORD-”改成“OD-”,这个改动如果不能平滑切换,线上就会出现一小时的混乱。

我们的做法是每次修改都发出新版本,保留旧版本可回滚,同时在注册中心记录切换时间。线上Agent在检索时默认取enabled=true的最高版本,只有出现问题时,运营人员一键切回上一版本,整个过程不用改代码。

另外,多团队协作时,权限治理是硬要求。技能系统面向的不是一个人,而可能是多个业务团队。我们用“发布审批”机制:开发者提交技能变更后,要经过管理员验证测试用例通过,才同步到预发环境,再通过灰度链路逐步对线上开放。这一步看起来增加了流程,但实际上大幅减少了线上故障的总时长。

3. 技能调度与编排实战

3.1 单技能调用链路

有了注册中心和描述规范,接下来就是运行时调度的重头戏了。整个单技能调用的流程,我们从设计到上线经历了多个版本的迭代,最后稳定为链路:

用户意图 → 技能检索 → 候选排序 → 参数映射 → Schema校验 → 执行体调用 → 返回结构化结果 → 模型组织回复

第一步是技能检索。这里用到的不是让模型全量读一遍技能列表,而是先用embedding做粗召回。我们把所有技能描述和标签转成向量,用户问题进来后也转成向量,先做余弦相似度召回top10技能,再把这10个技能的完整描述拼进Prompt,让模型做精排。这个方案比把50个技能全部塞进Prompt,token消耗降低了约70%,选型准确率反而提升了5%。

这里有一个关键参数:粗召回数量到底取多少。我们试过5、10、20,最终定型在10。太少了容易漏召(召回集里没有正确技能,模型再厉害也白搭),太多了精排时分心,准确率反而下降。这个参数和技能库规模有关,经验公式大致是“技能总数开根号乘以3”,60个技能时取10左右是合理的,技能库超过200个时建议做二级检索。

第二步是参数映射。模型选好技能后,要按上下文填充参数,这步发生在精排之后。我们的实现方式是:把技能参数Schema转成约束解码格式,让模型只输出JSON,然后立刻做本地Schema校验,不合法的直接返回错误并重试一次。如果重试还失败,就抛给兜底话术“我还需要您的订单号才能查询”。

3.2 多技能编排:链式调用与分支路由

真实业务场景里,单个技能很难独立完成任务。“帮我查一下昨天买的东西有没有发货,没发的话就申请取消”这个请求,需要先调用订单查询技能,再判断状态,如果未发货则调用取消订单技能——这是典型的链式编排。

我们实现了一个轻量级的DAG编排内核,每个节点可以是技能调用、条件判断、或者子流程。用一段简化代码说明核心逻辑:

from agent_skills import Skill, SkillContext, SkillResult from agent_skills.router import Router class OrderStatusSkill(Skill): skill_id = "order.status.query" async def run(self, ctx: SkillContext) -> SkillResult: order_id = ctx.params["order_id"] status = await order_service.get_status(order_id) # 模拟调用 return SkillResult.ok({"order_id": order_id, "status": status}) class CancelOrderSkill(Skill): skill_id = "order.cancel" async def run(self, ctx: SkillContext) -> SkillResult: order_id = ctx.params["order_id"] cancel_result = await order_service.cancel(order_id) return SkillResult.ok({"cancel_result": cancel_result})

在编排层,我们用类似“路由规则”的方式定义条件分支。比如取消订单这个业务流:

workflow: id: "order_cancel_if_unshipped" start: "order.status.query" nodes: - id: "order.status.query" next: "check_status" - id: "check_status" type: "condition" condition: "result.status == 'unshipped'" if_true: "order.cancel" if_false: "report_status" - id: "order.cancel" next: "report_cancel_result" - id: "report_status" endpoint: true - id: "report_cancel_result" endpoint: true

这条链的逻辑很直白:先查订单状态,再判断是否未发货,走对应的技能分支,最后返回结果。看起来简单,但它把大模型从“每一步都要重新推理该调哪个函数”里解放了出来。

要知道,多技能编排最大的坑在于上下文膨胀。如果让模型自己决定“先调A再调B”,它每一步都需要把上一步的结果重新读一遍,逻辑一旦超过三到四步,轻则超时重试,重则直接疯掉。用DAG把稳定链路固化下来,编排层只需要处理正常的参数传递,既快又准。

3.3 参数映射与反序列化

技能编排过程中,参数传递往往是问题高发区。尤其在链式场景中,A技能的输出要作为B技能的输入,中间还要做数据类型转换。

我们踩过一个大坑:B技能需要一个标准日期格式“2025-03-12”,但A技能返回的是“2025年3月12日”。初版直接让模型转换,结果模型偶尔会转成“03/12/2025”,B技能一执行就崩。后来我们加了固定的转换管道,所有时间类型从A出来就统一格式化为ISO 8601标准,B技能收到前再反序列化成自己的内部结构。

通用规则很朴素:如果一个字段是多个技能都要用的,那它只能有一种全局标准格式,不要指望每个技能自己适配。定义格式这件事听着很基础,但真的能省下90%的联调扯皮。

3.4 技能冲突消解与优先级

技能库大了,冲突在所难免。有两种典型冲突:功能重叠冲突和路由抢占冲突。

功能重叠冲突,比如两个团队分别做了“订单查询”和“订单详情查询”,输入输出几乎一样。这种冲突必须靠管理和代码双重手段解决:在注册中心层面,只允许一个技能绑定到某一组核心标签;在查询检索层面,对同组标签技能做去重,只保留优先级最高的一个。我们当时的处理方式是每周跑一次标签冲突检测,自动生成报告,让维护者合并或禁用重复技能。

路由抢占冲突更隐晦。一个用户问“我那个快递什么时候到”,这既可以被物流查询技能命中,也可以被订单查询技能命中,而后者因为描述里写了“物流信息”就被选中,但该技能返回的物流信息不完善,最终结果质量很差。解决办法是给技能加入意图覆盖度打分:当两个技能都命中同一意图时,优先保留描述里与当前问题重叠的语义单元更多的那个;如果仍分不出胜负,就走低优先级技能。这样至少保证有一个结果先返回,而不是让模型在多个技能间反复横跳。

4. 技能质量评估与回归保障

4.1 调用成功率之外的三个指标

很多团队上线Agent后,只看“技能调用成功率”。这个指标够直观,但用来指导优化远远不够。我们最终沉淀下三个核心指标:

  • 技能准确选中率(Selection Accuracy):在用户意图已标注好的测试集上,系统所选技能与标准技能一致的比例。这个指标衡量的是“有没有选错技能”,是所有指标里最敏感的。
  • 参数正确率(Argument Correctness):在技能选对的前提下,生成参数能否通过Schema校验,且语义上与用户要求一致。它衡量的是“参数提取得准不准”。
  • 执行完成率(Completion Rate):技能被调用后,返回结构合法且业务逻辑成功的比例。衡量的是后端接口的真实可用性。

这三个指标同时看,才能定位一个问题到底出在检索环节、映射环节还是执行环节。如果选中率低,问题在描述和检索;如果参数正确率低,问题在Schema设计;如果执行完成率低,问题基本在技能内部实现或数据权限。

4.2 技能回归测试集

技能系统不像普通代码,你很难用一个“单元测试”覆盖所有情况,因为技能的输入是自然语言,输出又是结构化JSON加自然语言组合。我们建立了一套“意图用例回归集”,每一类技能都准备了至少30个典型问法和30个边界问法。

典型问法比如“我要查昨天买的那本书的物流”“帮我看看618买的东西发货没”;边界问法比如“我也不知道单号是多少,你帮我找找”“之前那个订单现在怎么样了”。测试时,所有用例会跑一遍完整的链路:检索+精排+参数映射+执行验证。每轮改动后跑回归,对比选中率和参数正确率,低于阈值的直接阻止发布。

这套回归集看起来不起眼,但它是整个技能系统的守护神。某次我们把某个技能描述里“不可用于”清单删了,结果第二天回归测试立刻抓出3个用例的选中正确率掉到60%以下。如果没有这个机制,线上用户可能要为这个问题骂一整天。

4.3 线上监控与失效检测

技能系统的监控,和普通API监控不是一个难度级别。除了常规的P95延迟、错误率、QPS,我们还专门加了两类监控:

  • 路由稳定性监控:同一用户问题在相近时间窗口内,是否每次选中的技能都一致。如果同一个意图今天选A、明天选B,说明描述向量存在漂移,需要人工介入。
  • 执行结果校验器:每个技能返回后,用预先定义的结果校验器检查返回结构。比如“订单查询”技能必须返回order_list数组,数组为空时必须有reason字段。不符合结构的结果直接记为失败,避免模型拿一个空框架硬编答案。

此外,每晚自动跑一遍全量技能连通性探测,模拟调用每个技能,确认后端依赖服务是否还有响应。这一步帮我们发现过三次外部接口静默变更导致的技能集体失效,比用户投诉早了至少半天。

5. 实战中踩过的坑

5.1 描述写太长反而被忽略

我们曾经追求面面俱到,给每个技能写了400字描述,结果上线后选中率不升反降。分析了大量日志后才发现,模型对超长描述存在注意力衰减,真正起作用的只有前100字左右。后来我们强制限制描述在120字内,并遵循“谁 + 什么时候 + 干什么 + 不干什么”的句式,效果立竿见影。这里也给一个经验阈值:技能描述超过180字,每多50字,后半部分内容被模型利用的概率大约下降25%,这个数字来自我们的抽样统计。

5.2 相似技能互相打架

项目中期有“查询订单”和“查询订单明细”两个技能,功能高度重叠。线上经常出现用户问“订单里有什么东西”,模型随机选中其中一个,返回的粒度完全不同,用户一头雾水。最终我们放弃同时维护,直接合并成一个技能:内部区分简要模式和详细模式,根据用户问题里的关键词自动决定返回粒度。合并后,该场景的准确率从72%直接跳到93%。要认识到一个事实:相似技能并存,是技能系统最大的敌人。遇到重叠,想都不想就合并,别犹豫。

5.3 参数校验只信LLM的下场

早期我们为了省一次请求,不让参数校验器参与,直接相信LLM生成的JSON,结果大量参数错误在业务层才暴露,报错信息晦涩难懂,用户一脸懵。后来所有技能强制加Schema校验,不合法直接返给模型重新生成,并附加一句“你的参数有误:缺少order_id字段,请让用户补充订单号”。这样一来,用户体验从“报错”变成了“模型主动澄清”,满意度明显改观。

5.4 技能内部依赖环境问题

一个技能的执行体可能依赖第三方服务或内部微服务。如果依赖的服务版本升级,但技能执行体没适配,线上就会时不时报错。我们把每个技能注册时附带一份“依赖清单”,里面记录了依赖服务的版本区间、超时策略和重试次数。每次依赖变更,CI流水线都会自动触发一次该技能全量连通性测试。这套“依赖适配即发布”的机制,让我们避开过多次意外故障。

6. 从技能到技能市场

6.1 跨Agent复用

技能体系搭建完成后,最大的惊喜是跨Agent复用价值。我们的一个Agent只需要查询能力,另一个Agent需要下单能力,第三个Agent需要售后能力。把它们各自维护的技能统一放进同一个注册中心后,新Agent落地时不再从零写代码,而是在技能市场里按标签组合选择。

比如新上一个“物流客服Agent”,只需要把订单查询技能、物流订阅技能、投诉建议技能拖进技能组,再配置一套自己的语气模板就完成80%,整个过程从数周缩短到一两天。这就是把技能做成“能力积木”的回报。

6.2 技能自动生成

技能系统运转稳定后,我们开始尝试用LLM辅助生成新技能草稿。比如团队想接入一种新业务接口,工程师只需要上传接口文档和几个真实案例,模型自动生成初始的description和params_schema,再人工微调。这个过程中人工的重点工作不是写代码,而是校准边界和约束,比如加上“不可用于退款”“不支持共享订单查询”这类关键限制。虽然离全自动还远,但整体效率提升非常可观。

6.3 后续扩展方向

如果继续往下做,我看到的扩展方向至少还有三个:一是技能组合的高层意图模板,把“查单 -> 判断状态 -> 取消或催发货”这样的稳定链路做成可拖拽的模板,让非技术人员也能编排;二是技能自动降级与容灾,当主技能不可用时,自动寻找功能相近的备用技能执行,而不是直接报错;三是技能能力的量化评估,不只看调用成功率,还要衡量技能真正帮用户解决了多少比例的问题,通过用户最终反馈反哺技能描述和实现。

这些方向没有哪个是容易的,但基础打好了,每一个都值得投入。

这次做技能系统的经历,让我最大的感受是:Agent能不能干人事,核心不在于模型多大、Prompt多花哨,而在于你给它的“能力地图”画得清不清楚。技能描述、参数Schema、校验规则这些看似不起眼的工程细节,往往决定了一个Agent是能稳定复用,还是上线一周就重新陷入混乱。如果你正在做Agent,哪怕现在工具数量还不多,也建议尽早用技能注册、技能检索、技能描述规范这套思路来组织能力。等到工具多了再重构,那才是真的累。

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

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

立即咨询