1. 先说清楚:技能系统在Agent架构里到底解决什么问题
我去年接手过一个客服Agent的重构项目,最初的实现方式很直接:把所有业务能力写成大段提示词塞进系统讯息里,再配上几个零散的函数调用。上线三个月后问题集中爆发——新增一个查快递的能力要改提示词,改完经常把别的能力带崩;模型偶尔把查天气和查订单搞混;测试用例越写越多,但每次修改都像在雷区里走路。那段经历让我彻底想明白一件事:Agent的能力组织方式,决定了它的上限。能力如果不做结构化拆解,就没有办法独立测试、独立迭代、独立复用,最后一定变成一座理不清的屎山。
这正是agent-skills这个方向要解决的问题:把Agent能做的每一件事,抽象成一个独立的、可描述、可注册、可编排、可评测的“技能”。你可以把它理解成给大模型装了一套“可插拔的手脚”——大脑(推理)不用管每个动作的细节,只需要根据任务目标选择调用哪只手、按什么顺序调用。开发者也不需要在提示词里维护几百行“你会做什么”的说明书,而是维护一套结构化技能库。
这篇文章面向的读者,是那些正在做Agent应用、但发现“写提示词 + 裸函数调用”越来越撑不住复杂场景的开发者。我会从技能的分类体系、描述注册、编排执行、评测迭代四个维度,配合我自己踩过的坑,把一条从零搭建技能系统的完整路径讲清楚。全文不涉及具体框架绑定,核心思路在任何Agent框架里都能落地。
2. 技能的分层与分类:没有分类学的技能库,迟早变成一锅粥
2.1 原子技能与应用技能:层级必须先分清楚
我见过很多团队一上来就平铺直叙地定义技能,每个技能都是一个函数封装,比如“查询天气”“查询订单”“发送短信”“计算运费”。这种做法在技能数量少于十个时勉强能跑,一旦超过二十个,模型的选择准确率会肉眼可见地下降。问题出在哪?层级太扁平了。
我自己的做法是把技能分成两层:原子技能和应用技能。原子技能对应一个不可再拆的系统调用,比如“通过订单号查询订单状态”“向指定号码发送短信模板”这类,它只做一件事,输入输出边界非常清晰。应用技能则是一段可以编排的逻辑,它内部可能会调用多个原子技能,甚至调用其他应用技能,面向的是一个相对完整的用户意图,比如“处理退款申请”——这个技能内部要查订单、校验退款资格、调支付接口、发通知短信,但它对外只暴露一个入口和一套参数。
为什么必须分层?因为大模型在做技能选择时,如果一个技能列表里既有“查询订单状态”又有“查询订单状态并计算预计送达时间”还有“查询订单状态并判断是否支持退款”,这些高度重叠的描述会严重干扰模型的判别力,响应延迟和误调用率同时上升。分层之后,模型面对的选择面上层只有五六个应用技能,决策空间小得多;而每个应用技能内部怎么编排原子技能,是确定性代码决定的,不需要模型参与推理。
2.2 按用途分类:数据类、操作类、计算类各自的特性
除了层级,我还习惯给每个技能打上“用途类型”标签,这直接决定技能注册时的参数设计和错误处理策略。
数据读取类技能,典型如“查询库存”“获取用户资料”,特点是只读、幂等、无副作用。这类技能的安全约束最少,可以放心地让模型频繁调用,但需要在描述里明确“返回数据的时效性”,否则模型可能拿缓存数据当实时数据来回答用户。
操作写入类技能,比如“创建订单”“修改密码”“发送验证码”,特点是会产生副作用,不可随意重试(重试可能重复下单或重复扣款)。这类技能必须在注册表里强制声明“是否幂等”“是否需要用户二次确认”,执行引擎看到这类标记,会在调用前自动加一道确认门槛,在失败重试时改用专门的幂等策略。
计算推理类技能,比如“计算运费”“生成报表摘要”,特点是无外部依赖、纯函数式,但也最容易被模型误用——模型经常靠自己的推理硬算,而不去调用标准计算技能。对付这个问题的办法,是在描述里强调“这是标准计算逻辑的唯一入口,不要自行推算”,同时把输入参数设计得足够完整,让模型没有理由自己动手。
2.3 技能元信息字段:除了函数名,你还得给模型一套完整“说明书”
一个技能要能被模型正确理解和调用,光有函数名和参数列表是远远不够的。我在技能注册表里要求每个技能必须有如下字段:
| 字段 | 示例 | 为什么必要 |
|---|---|---|
| 技能ID | order.refund | 全局唯一,用于编排引用 |
| 用途类型 | 操作写入 | 决定副作用处理策略 |
| 所属层级 | 应用技能 | 决定是否参与顶层选择 |
| 一句话描述 | 处理用户退款申请,含资格校验与通知 | 顶层模型做技能选择时主要看这个 |
| 详细描述 | 说明适用场景、不适用场景、前置条件 | 帮助模型排除误用 |
| 参数Schema | JSON Schema | 参数校验与示例生成 |
| 返回结构 | 标准返回格式 | 供后续技能消费 |
| 权限标记 | 需二确认/需管理员 | 执行引擎的安全闸门 |
| 幂等性 | 幂等/非幂等 | 决定重试策略 |
| 超时阈值 | 3000ms | 防止技能挂死拖垮整个Agent |
这套元信息体系,本质上就是把“模型该知道的信息”和“引擎该知道的信息”全部结构化。我见过有人把权限、超时这类工程属性也写进给模型看的描述里,结果模型被“权限不足”这类词干扰,反而不敢正常调用。正确的做法是:描述文本只面向模型,工程属性只面向执行引擎,两者物理隔离。
2.4 技能数量失控前的预警信号
技能库膨胀是必然的,但失控之前通常有几个信号:模型开始频繁选错技能(同类技能超过三个且高度相似);技能描述越写越长,但准确率不升反降;新人往里加技能时不看已有技能,导致重复造轮子。我处理这些问题的核心手段是:每个季度做一次技能收敛,把调用量低、功能重复的技能合并或下线,同时检查同类技能描述之间的区分度,如果两个技能描述让普通人都分不清,模型必然更分不清。
3. 技能描述与注册:让大模型“看得懂”才有机会“调得对”
3.1 描述文本怎么写:一句话描述决定选择准确率
技能描述是模型做技能选择的直接依据,重要性被严重低估。刚开始做agent-skills时,我写描述特别随意,比如“查询订单状态”,结果模型在用户说“我的快递到哪了”时,经常在“查询订单状态”和“查询物流信息”之间犹豫。
后来我总结了一套写法:一句话描述要包含“动作 + 对象 + 触发条件”,并在句尾加上典型的用户表达方式作为锚点。对比一下修改前后的效果:
修改前:“查询订单状态” 修改后:“根据订单号查询订单当前状态(待支付/已发货/已完成),适用于用户询问‘我的订单怎么样了、发货了没’等场景”
后者把场景锚点直接写进描述,模型在意图匹配时的置信度明显提升。实测下来,同类技能的选择准确率从原来的78%左右提高到94%以上,提升幅度非常显著。但也要注意,描述不是越长越好,我在后面专门讲这个坑。
详细描述部分,我要求必须包含三块内容:适用场景的边界(哪些情况可以用)、不适用场景的排除(哪些情况不该调用本技能,应该调用哪个技能)、前置条件(比如需要用户已登录、需要订单号存在)。这三块信息能有效抑制模型的错误调用倾向。有一次我把“计算运费”的不适用场景写成“仅适用于标准快递,同城急送请调用urgent.delivery.calculate”,错误调用率立刻降了一半。
3.2 参数Schema:类型、必填、约束、示例一个都不能少
参数Schema是技能最容易被模型调炸的地方。我经历过模型把电话号码传成纯数字不带区号、把日期传成“明天”这类自然语言、把枚举值传成模糊说法等一堆情况。
JSON Schema本身是标准,但在Agent场景里,我会额外要求四个字段:格式示例、值域约束、自然语言转换规则、参数间依赖关系。举个例子,一个下单技能的参数“收货地址”,Schema里的type是object,这远远不够。我会在description里写“收货地址包含province、city、district、detail四个字段,若用户只提供部分信息,则询问补充,不要猜测补齐”。
再比如时间类参数,模型经常把“下周一下午”直接转换成具体日期,但如果用户没说是哪一周,模型就很容易猜错。我的做法是在Schema里指定“若用户表达为相对时间(如明天、下周),禁止自行换算,必须额外追问确认具体日期”。这个约束写进Schema后,时间类参数的错误率直线下降。
必须补充的是参数示例字段。模型对示例的敏感度远超对类型定义的敏感度,一个“参数示例”往往能让模型照着葫芦画瓢。我在每个参数里都放一到两个完整示例,实测下来,参数格式错误率能再降两到三成。
3.3 注册流程:静态注册还是动态拉取?
技能注册的架构方式,直接决定技能系统的灵活性和稳定性。我经历过两个阶段的演进:
第一个阶段用静态注册:所有技能写在一个巨大配置文件里,启动时一次性加载。好处是简单、可控、方便做全量校验;坏处是技能更新要重新发布整个Agent服务,低频技能也一直占用上下文(描述全部塞给模型),token开销非常大。
第二个阶段改成动态注册:技能按域拆分成多个描述文件,加载时按需拉取某几个域的技能集合。比如用户一上来提到“退款”,Agent只加载订单域 + 售后域 + 通知域的技能描述,而搜索、营销这些域完全不进入上下文。这种方式对token消耗的改善非常可观,也保证了模型面对的技能数量始终控制在十几个以内。
动态注册要解决的额外问题是技能发现和冲突检测。我维护了一个技能索引服务,每个技能注册时自动检测ID冲突、同名函数、参数Schema合法性,并且为每个技能打上“所属域”标签,供按需加载使用。这样技能团队可以独立维护自己负责的技能,不用每次改动都触碰全局配置。
3.4 技能版本管理:改了技能描述,等于改了模型的“眼”
技能注册本质上是一个知识库的持续更新问题。我的习惯是每个技能都有版本号,技能的代码实现变了属于代码版本,技能的描述文本变了属于“认知版本”。这两个版本必须分开管理。为什么?因为描述变了,模型对技能的认知就变了,可能影响选型行为,这必须在一个可审计的变更记录里留痕。
有一次我只是把“查询优惠券”的参数提示改得更详细了一点,结果线上相关任务的技能选择分布发生了明显变化。如果当时没有版本记录,我根本定位不到这个波动是因为描述变更引起的,还会去排查别的无关因素。所以现在技能描述的任何改动,我都会记一条变更日志,并且跑一遍回归测试再上生产。
4. 编排与执行引擎:多个技能如何协同而不打架
4.1 四种编排模式:顺序、条件、并行、重试
技能系统里只定义“有什么”是不够的,还得有“怎么组织执行”的机制。我整理下来,应用技能内部最常用的编排模式就四种:
顺序编排是最常见的,比如退款流程必须先查订单、再校验资格、再执行退款、最后发通知,这个顺序是硬约束,不允许模型自由发挥。我在执行引擎里用DAG(有向无环图)来表示这类依赖关系,保证每个技能执行时,它的前置技能一定已经完成。
条件编排主要处理“根据某个技能的输出决定下一步走哪个分支”。典型例子是风控判断:先调用风险评估技能,如果分数低于阈值,走正常流程,继续调用下单技能;如果高于阈值,走人工审核分支。这个分支逻辑如果让模型临时决定,在高风险场景里非常危险,所以我在编排层把它写成规则,模型只负责提供输入参数,不负责判断流程走向。
并行编排用于没有依赖关系的技能同时执行,显著降低整体延迟。比如用户问“我的订单什么时候到,运费怎么收”,查询订单状态和计算运费两个技能互不依赖,并行执行比串行快接近一倍。但并行执行要求技能必须是线程安全的,不能有共享可变状态。我踩过坑:两个并行技能同时写同一个Redis key,导致数据互相覆盖,排查很久才发现是共享状态问题。
重试编排是可靠性最关键的环节。我的原则是:只对幂等技能做自动重试,操作写入类技能绝不自动重试,而是转人工确认或返回错误让上层决策。重试次数和退避策略也要有上限,我一般设最多三次、初始间隔300ms、指数退避,超过三次直接熔断。
4.2 冲突消解:技能选择歧义与参数冲突
多技能系统必然面临两个层面的冲突。第一层是选型冲突:用户说的话同时命中两个技能描述,模型拿不准调用哪个。第二层是参数冲突:同一个参数在编排链路的不同技能里定义不一致,前面技能产出的字段后面消费不上。
选型冲突的消解,我在技能注册表里加了一个“互斥优先级”字段。比如“查询订单状态”和“查询物流轨迹”在很多场景下都模糊,我会在两者描述里都明确写“如果你不确定用户指订单状态还是物流轨迹,请调用order.query,然后根据返回值判断是否还需调用logistics.query”。这就是把消解规则直接写进描述,让模型有一个确定性兜底路径,而不是靠概率猜。
参数冲突的消解更偏工程化:我在编排引擎里定义一个“上下文对象”,所有技能的输入输出都挂到这个全局上下文上,字段名统一用一套命名规范。比如订单号,在订单域叫orderId,就不能在售后域叫afterSaleOrderId。如果不统一,编排脚本里就要写一堆字段映射,极易出错。统一命名规范虽然初期有点繁琐,但长期收益非常大。
4.3 上下文传递与中间结果缓存
技能编排的中枢是一个上下文管理器,它负责三件事:保存每个技能的中间结果、向后续技能提供输入参数、清理不需要再用的临时数据。
我刚开始设计时,简单用一个JSON对象一路传递,结果很快发现两个问题:一是上下文越积越大,几轮编排下来塞进模型的token大幅膨胀;二是一个技能改了上下文里的某个字段,后面技能读到的是被修改过的脏数据,产生隐蔽的bug。后来改成“只读 + 显式写入”模式:上一个技能的输出放进上下文时生成一个不可变快照,下一个技能只能读取,如果需要修改,必须显式声明“写入新字段”。这个约束彻底解决了脏读问题。
中间结果缓存则是性能优化的利器。同一个会话里,如果两个技能需要查询同一份数据(比如用户身份信息),第二次调用直接读缓存而不是重复查询。我在缓存key里带上用户ID和会话ID,防止跨会话串数据。缓存刷新策略上,数据读取类技能缓存时间可以稍长,操作写入后立刻失效相关缓存,保证后续读取能拿到最新数据。
4.4 执行引擎的守护机制:超时、熔断与降级
每个技能都要设置超时阈值,这个阈值不该统一,而要根据技能的实际情况来定。网络调用型技能我一般给3000ms到5000ms,纯计算型技能给800ms就够。超时之后的处理也要分级:可降级的技能返回默认兜底值(比如推荐列表返回空列表),不可降级的技能抛异常交给上层策略。
熔断机制按技能维度做隔离。我遇到过某个第三方接口故障,导致所有依赖它的技能全部超时,拖垮了整个Agent响应。后来我加了熔断器:连续5次失败即熔断该技能,熔断后快速返回兜底,不再等待网络,同时定时探测恢复。这套机制上线后,单个技能故障不再影响其他技能。
降级是兜底策略里最容易忽略的一环。实操经验是:每个技能在注册表里要声明“降级行为”。查询类技能可以降级为返回空值并提示稍后重试;操作类技能不能盲目降级,必须转人工;计算类技能则可以在主技能不可用时切换到备用简化算法。这些降级策略必须在技能注册时就规划好,等到故障发生再临场设计根本来不及。
5. 技能评测与迭代:好技能不是写出来的,是度量出来的
5.1 单技能评测:从参数到输出逐项验收
技能系统上线后,我记得最常见的问题是“不知道这次改动是好是坏”。靠感觉判断太危险,我把评测拆成三层,每一层解决不同的问题。
单技能评测只验证一个技能独立运行时的表现。它的核心关注点有两类:
一类是参数正确性。我准备了一批真实用户表达(带自然语言噪声的那种),逐个喂给模型,看它能不能从表述中正确抽取参数并映射到Schema。比如用户说“帮我看看上周的订单到哪了”,评测目标包括:正确识别意图是查询订单状态、正确把“上周”转换为具体日期范围(若规则要求则输出追问)、正确不调用无关技能。
第二类是输出正确性。技能执行后的返回结构要严格符合声明的返回Schema,字段不能缺、类型不能错、错误码要规范。这一步我会用自动化断言校验,再配人工抽检。
单技能评测必须在每次技能描述或实现变更后自动执行,这是技能迭代的安全底线。
5.2 场景级评测:整条编排链路跑不跑得通
单技能通过不意味着整体没问题。场景级评测关心的是:一个真实用户任务,从意图识别到技能编排到多步执行,最终能不能完成。
我维护了一个场景评测集,每个场景包含:一段用户真实表述、一个期望的执行路径(需要调用哪些技能、按什么顺序)、一个期望的结果状态。评测执行时,Agent按真实链路跑完整流程,然后对比实际路径与期望路径的匹配度。
这类评测很擅长暴露编排层的问题:技能顺序错了、某个分支没触发、上下文传递断了。我印象很深的一次——用户退换货流程,单技能全通过,但整条链路里“换货”场景的编排脚本错误地调用了“退款”分支,场景评测一下就给抓出来了。如果没有场景评测,这类问题必然是用户先碰到。
场景评测集需要持续扩充。每在线上发现一个新的问题case,就把它沉淀进评测集,作为回归用例。这个过程坚持半年后,线上问题率会明显下降,因为同类错误的复发会被评测集拦截。
5.3 回归测试:技能变更引发“蝴蝶效应”时的防护网
技能系统的脆弱之处在于“牵一发动全身”。改了一个技能的描述,可能影响模型对另一个相似技能的选择;调整了编排顺序,可能影响整条链路的输出。回归测试就是守护这道防线的机制。
具体做法是:每次有任何技能变更(注册、描述、编排、实现),都跑一遍单技能评测加场景级评测的完整套件,并且对比评测分数与上一次运行的差异。分数下降的点就是回归异常点,必须定位原因才能上线。
我还建立了一个“敏感度记录表”:记录每个技能描述变更后,哪些其他技能的选择率发生了波动。表格不是为了出报告,而是让我在后续改动时能预判风险范围。比如我知道“query.order”和“query.logistics”之间存在高敏感关系,那我改其中一个描述时,就会特别注意另一个的回归测试结果。
5.4 评测数据的收集:线上真实流量比手工用例更值钱
离线评测集终归是人工构造的,难免有“题库思维”。更宝贵的是线上真实流量的回流。我在Agent服务里做了完整的调用日志埋点,每条日志记录:用户输入、模型选择的技能、选取时各技能的置信度、实际执行结果、用户是否满意(比如是否有后续投诉或重复提问)。
每周我会从日志里捞一批典型案例做复盘:一类是“模型选错技能的case”,分析是描述歧义问题还是参数引导问题;另一类是“模型不敢选技能的case”,通常说明技能描述里缺少场景锚点。这些案例成为评测集扩展的主要来源。
有意思的是,线上日志还常暴露一个现象:部分技能的调用频率极低,且从未被评测集覆盖。这些低频技能常常是流程设计时“觉得需要”,但实际用户很少触发。对于这类技能,我会评估是合并还是补场景,避免它变成无人维护的死代码。
6. 落地过程中的典型坑,以及我现在坚持的做法
6.1 坑一:技能描述越长,选择准确率不升反降
我起初犯过一个严重的错误——为了让模型“更好地理解”,把技能描述写得非常详细,场景、历史原因、注意事项全塞进去。结果技能选择准确率不升反降,响应时间也变长了。后来我意识到,模型的注意力有限,描述越长,关键锚点信息越容易被淹没。
现在的做法是:一句话描述严格控制在50字以内,只保留“动作 + 对象 + 触发场景锚点”;详细描述控制在150字以内,只写必要边界、排除情况、前置条件。能用精确表达解决的,绝不用冗余话术。每条技能描述都要经过“删减测试”——删掉一句话如果模型表现不变,那就删掉。
6.2 坑二:技能数量失控,模型连“翻牌子”都难
技能库从十几个涨到五六十个的过程中,我明显感到模型选择准确率在滑坡。原因很清楚:越来越多的技能描述在语义空间上高度靠近,判别难度指数级上升。
我的对策是严格限制顶层技能数量,超过二十个就必须做应用技能层面的收敛或聚合。具体方法有两个:一是合并功能高度重叠的技能,把差异变成内部参数而不是独立技能;二是更激进地使用“按域动态加载”,让模型在任何一个时刻面对的技能候选不超过十二个。实施动态加载之后,技能选择准确率恢复到了九成以上,token消耗也降了一大截。
6.3 坑三:只测“能用”不测“边界”,线上问题防不胜防
早期评测用例几乎全是“正例”:用户输入规范、场景明确、意图清晰。结果到了线上,用户表述千奇百怪——说“订单发货了没”“我的包裹咋还没到”这种跨域模糊表达;说“帮我把那个退了”这种指代不明的指令。技能系统在这些边界输入上经常掉链子。
现在我在评测集里专门留了超过三成的比例给边界case:模糊表达、缺少关键参数、多意图混杂、直接要求调用不存在的能力。这些case的价值在于逼迫技能描述和编排逻辑把边界防守住——要么正确地拒绝,要么通过追问收敛意图,而不是强行瞎调技能。
6.4 现在坚持的几条原则
经过这几个版本的迭代,我把agent-skills的设计原则沉淀成了五条:
第一,技能是面向模型的产品,描述文本和参数Schema是给模型看的最核心“文档”,每一行都要为模型的理解负责。第二,分层分类是技能库的骨架,没有骨架,技能一多就崩。第三,编排逻辑尽可能用确定性规则,少让模型在流程分支上做高自由度决策。第四,评测集和线上回流是技能迭代的两个轮子,缺一个都会开歪。第五,一切变更要有版本痕迹,没有审计能力的技能系统在未来维护中一定付出代价。
关于agent-skills这套体系,我个人最大的体会是:它本质上不是在写函数,而是在构建一个“让Agent稳定行动的系统工程”。技能定义、注册、编排、评测、迭代,环环相扣,任何一环偷懒,最后都会以线上事故或迭代困难的方式还回来。如果你正好在搭Agent技能体系,建议从小规模开始,先把元信息字段和评测机制立起来,再逐步扩充技能库,这会比一上来堆一堆技能要扎实得多。