提到“agent-skills”这个词,很多接触过大模型应用开发的人第一反应是:哦,给Agent加一堆tools(工具),让模型能调用外部能力。但如果一个项目被命名为“agent-skills”并且被认真对待,它往往做的事情不是简单塞几个函数进去,而是在思考一个更深的问题——Agent的技能到底应该怎么构造、怎么管理、怎么被精准触发。
我在实际做AI应用落地时最大的感触是:大模型本身只是一个“会推理的大脑”,但它离真正能干活还有十万八千里。你要让它帮你查数据、发消息、操作业务系统,必须有一套机制把“能力”交到它手里。而“简单地把功能写成函数列表”这件事,在Demo阶段很爽,一旦你的技能数量超过十个、场景开始多样化,马上就会变成一场灾难——参数错乱、触发时机不准、技能之间相互干扰、维护成本爆炸。
所以当我看到“agent-skills”这个命名角度时,我意识到它真正在讨论的,是一个可以被复用的、结构清晰的Agent技能体系设计。这套东西如果你只把它当作“工具调用”来理解,会错过它最有价值的部分。今天这篇,我打算把整个思路拆开揉碎了讲一遍,把我做过的东西、踩过的坑、反复调整后沉淀下来的方案,全部分享出来。不追求什么宏大框架,只求看完之后你能自己搭一套技能系统,并且知道每一步为什么这么设计。
1. 重新理解Agent与技能的关系
1.1 为什么单纯堆工具会出问题
先还原一个典型场景。你在做一个客服类的智能体,早期只需要查订单、查物流,于是给Agent声明了两个function。一切看起来很顺利,模型在对话中能自己判断何时调用哪个函数。后来业务方提需求,要加退换货申请、优惠券发放、库存查询、客服转人工……两个月后,你的tools列表里躺了五十多个函数声明。这时候你会发现几个诡异的现象:
模型开始“选择性忽视”一些工具,尤其当用户问的问题跟多个工具相关时,它常常选错。排查下来发现,工具描述写得太模糊,或者某些工具的触发条件重叠,模型根本分不清边界。参数传递也经常出错,明明该传整数的地方传了字符串,该填枚举值的地方填了自由文本。
本质上,tools是“平铺”的,它们之间没有任何结构关系。而技能是“结构化”的,它不仅有“能做什么”的函数签名,还包含什么时候该用、什么时候不该用、参数如何校验、执行结果如何反馈等一系列行为约定。把五十个工个平铺给模型,相当于让一个新手同时记住五十条办事规矩,不懵才怪。
1.2 技能的本质是有边界的可复用能力
我喜欢的定义是:技能是Agent能力的基本单元,它有明确的触发条件、输入输出契约、执行逻辑和自我描述。它不同于普通函数的地方在于,技能必须为模型提供足够的“上下文友好”的信息,让模型能高效且准确决定是否调用、如何调用。
举例来说,一个“查询订单状态”的技能,不仅仅是暴露一个query_order(order_id)的函数接口。它还需要说清楚:用户什么时候会需要这个技能(当用户询问包裹位置、配送进度时);order_id从哪里来(从对话中抽取,还是需要追问用户);如果查不到怎么办(返回未找到并引导提供正确单号)。这一大段信息如果不结构化地组织好,模型就会反复试错,而用户看到的就是Agent“抽风”。
技能还应该具备可组合性。比如“取消订单”这个技能,不应该从零做一套流程,而是复用“查询订单状态”和“发起退款接口”,它的核心其实是业务规则编排,而不是所有逻辑都自己扛。这种分层设计,能大幅降低每个技能自身的复杂度。
1.3 技能系统解决的三个核心问题
被动接需求式地加工具,长期看一定会让系统越来越难维护。技能系统真正要解决的,至少是这几个问题。
能力管理问题。技能不再只是活在代码里的函数,而是以“注册表”的方式被管理起来。每新增一个能力,需要声明它的名字、描述、版本、参数结构、权限要求、所属领域、依赖关系。这样系统里有哪些能力一目了然,该扩展谁、该拦截谁、该下线谁,都有迹可循。
动态决策问题。模型在每一轮对话中都应该知道“当前这个问题在我的技能库里有没有能接住的”。这需要技能系统提供一种高效的筛选机制,把几十上百个技能聚合成较小的候选集,减轻模型的判断负担。如果没有这层机制,模型面对海量工具时的决策质量下降是必然的。
质量保障问题。技能的执行结果需要被持续观测。调用了多少次、成功率多少、平均耗时多少、哪些技能老被误触发。没有这些数据,所谓的智能体业务就是盲人摸象。
这三个问题,展开就是一套完整的设计蓝图。
2. 技能系统的内部结构与设计思路
2.1 核心模块拆分与协作
在我的实践中,一个可用的agent技能系统至少包含四个核心模块:技能注册中心、技能加载器、技能运行时、意图决策桥。
技能注册中心是一个元数据仓库,最直白的实现就是一张大表或者一批配置文件。它记录每个技能的所有信息——技能ID、版本、触发关键词和语义描述、输入参数Schema、权限等级、所属领域、执行端点或函数引用、限流配置。它解决了“有哪些技能”的问题。
技能加载器负责根据注册中心的配置,把技能的执行体加载到内存中。执行体可以是一个Python函数、一个外部HTTP接口、任意可调用的远程能力。加载器需要做依赖检查、版本核验、资源池化,避免每个技能重复申请连接资源。
技能运行时是真正跑技能的地方。它拿到模型填好的参数后,先做参数校验和规范化,然后调用技能执行体,最后把结果按照约定的结构返回给模型。运行时还要处理超时、重试、熔断、结果截断等容错逻辑。
意图决策桥是连接大模型和技能库之间的纽带。它负责把用户的输入、当前对话上下文和技能注册表中的候选技能做匹配,把最相关的几个技能声明动态塞给大模型,供其选择。这一步是整个系统的灵魂,因为它决定了模型能不能在正确的时机看到正确的技能。
2.2 为什么需要“动态技能选择”而不是全量塞入
早期我图省事,把几十个技能全部以JSON Schema的形式写在系统提示词里。结果首先是Token消耗暴涨,每个请求都要把那么长的工具列表重复发送一遍,成本高得离谱。更麻烦的是模型注意力被稀释,明明应该触发技能A,它却选成B。
后来改成动态选择后,情况发生了质变。每轮请求前,我先做一次语义召回,把候选技能压缩到三到五个,再把这些技能的定义交付给模型。这样模型看到的工具列表是短而精的,它的判断准确率有了明显提升,响应速度也快了,因为模型生成的工具调用Token序列更短了。
实现动态选择的方式有很多种,从基于关键词的倒排索引,到用向量模型做embedding相似度召回,再到干脆用一个小模型做意图分类。我实际对比下来,对绝大多数业务场景,用embedding召回加规则修正已经足够稳定了。主题越垂直的小型业务,召回准确率越好;领域横跨很大的场景,规则修正权重就要提高。
2.3 设计中的关键取舍
整个系统设计中最需要想清楚的一点是:技能定义到底面向人还是面向模型。
面向人,意味着要用清晰的文档目录来组织技能包,方便开发人员阅读和维护。面向模型,意味着每个技能的描述必须极度语义化,要让模型一看就懂、一懂就对。这两者经常矛盾。比如“发起退款”这个技能名字很清晰,对人来说一目了然。但对模型来说,“退款”和“售后申请”在语义上有重叠,如果描述不精确,模型极容易混淆。
我的解决思路是给每个技能加三层描述文本。第一层是“技能使用场景描述”,用一句话说明用户什么情况下会触发它。第二层是“技能行为边界描述”,明确哪些情况不应该使用它。第三层是“参数填写指南”,逐字段说明含义、取值来源、缺失时怎么办。三层文本一旦写清楚了,模型的准确率能上一个台阶,有时候比模型本身升级还管用。
另一个取舍是关于技能执行体放本地还是放远程。本地函数的优势是延迟低、能直接操作内存中的共享数据,缺点是强耦合,技能一多代码就变乱。远程服务的方式更利于隔离和规模扩展,但会引入网络开销和额外运维复杂度。我个人建议初期阶段用本地函数包,等到技能数量超过三五个、涉及跨团队协作,再演进成独立服务。
3. 从零搭建一个可复用的技能库
3.1 技能定义的标准结构
先给出一个技能定义的标准结构。这是我在多个项目中沉淀下来的格式,它兼顾了人机共读,既能被开发人员阅读,也能被模型快速理解。每个技能条目包含五个核心字段:元信息、触发条件、执行配置、参数契约、返回契约。如果从代码角度来看,技能定义一般采用JSON格式存储,便于加载器解析和动态下发。
一个典型技能定义的骨架大概是这样的:
{ "id": "orders.query_status", "version": "1.2.0", "name": "查询订单状态", "domain": "orders", "enabled": true, "when_to_use": "用户询问订单的配送进度、物流位置、签收状态时使用", "when_not_to_use": "用户询问退货退款进度时不要使用,应使用售后类技能", "parameter_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "完整订单号,通常由平台生成。若对话中没有明确给出,应询问用户" }, "customer_phone_tail": { "type": "string", "description": "客户手机尾号四位数,用于身份校验,可选" } }, "required": ["order_id"] }, "execution": { "type": "local_function", "func_name": "biz.orders.query_status" }, "access_control": { "roles": ["user", "agent"], "timeout_ms": 3000 } }3.2 技能加载与注册机制
技能加载器读取一批这样的定义文件,进行校验,然后注册到内存中的技能注册表。注册过程不光是简单地把JSON放进字典,还需要做很多约束检查。比如检查是否有重复的ID、参数Schmea是否符合JSON Schema规范、执行函数是否存在、版本之间有没有冲突、依赖的其他技能是否都已在注册表中。
我一般把技能分为三类来管理:基础原子技能、业务领域技能、组合编排技能。原子技能是纯粹的单一动作,比如发送短信、查询库存;业务领域技能是在原子技能基础上加了业务规则,比如“下单前的库存校验与锁定”;组合编排技能则是预定义流程,比如“处理退款”需要按顺序调用订单校验、库存回补、支付原路退回等多个原子技能。注册中心对每一类技能采用不同级别的校验规则,组合技能重点校验依赖路径是否完整。
这里特别提醒一个容易出问题的地方:技能的id必须稳定,不能随便改动。因为技能的调用记录会累积数据,id一变,历史统计就会断层,而且若已有用户或业务进程持有旧id引用,改动会导致调用失败。我建议把技能id理解为公共接口,一旦发布,需要向后兼容。
3.3 参数契约与运行时校验的实现
技能参数校验这事,看起来简单,实际坑很多。模型从对话里抽取参数时,经常出现类型错误、信息缺失、字段混淆。比如用户说的是“把收货地址改成北京市朝阳区XX路1号”,模型可能将“北京市朝阳区XX路1号”解析成order_id,这就闹出了笑话。所以参数契约不能只是一纸空文,运行时必须做实际校验和修正。
我的方案是在运行时安排“参数处理器”这个环节。处理步骤有两步:规范化、纠偏。规范化是将模型传入的值转换为执行体实际需要的类型和格式,比如字符串数字转int、去除多余空格、格式化日期。纠偏则根据参数语义做一些规则兜底,例如发现order_id的值以汉字开头,推断模型抽取错误,选择清空该字段并触发向用户的追问。
运行时校验完毕之后才真正进入执行环节。执行环节最重要的设计是结果的统一封装。无论底层调用成功、失败、超时,返回给模型的结构都应该一致,不能有时候返回纯文本,有时候返回JSON,否则模型在下一步推理时会迷茫。我常用的返回结构是{status, code, data, message},后处理简单且模型理解成本低。
3.4 技能调用触发链路演示
把完整链路串起来的模样是这样:收到一条用户消息后,决策桥先基于文本召回技能候选集,再结合对话历史和业务上下文进行二次过滤。然后决策桥把候选技能的定义动态注入到大模型可使用的工具列表中。大模型在生成回复时判断是否需要调用技能,如果需要,就输出一条结构化调用指令。运行时拿到指令后执行技能,将执行结果作为新的上下文的一部分回复给大模型,让模型根据结果生成最终的用户回复。
链路看似简单,但每个环节都有很多细节。比如候选集生成时,不能只看用户当前的一句话,还要结合最近几轮的对话内容。用户可能上一句在讲订单,这一句突然只说了句“那这个呢”,如果决策桥只看当前句,召回就会失败,因为“这个”没有指代信息。所以我在实现路由时会把最近三轮对话摘要一起参与召回。
4. 技能调用的稳定性保障
4.1 超时与重试策略
技能执行过程中,网络抖动、下游服务变慢是家常便饭。如果不设超时,一个技能卡住可能导致整个Agent响应节奏被拖垮。超时值需要实测得出,不能拍脑袋。我给本地计算类技能设的默认值是3000ms,外部API类技能默认6000ms,特别重的批量查询会到15000ms。
重试策略也讲究轻重分离。对于幂等技能,也就是重复执行不会产生副作用的技能,比如查询类,可以采用“最多重试两次,间隔300ms”的策略。对于非幂等技能,比如创建订单、发送消息,绝对不能盲目重试,否则可能导致重复下单、重复发送等问题。这类技能的处理方式是返回失败,然后由Agent向用户展示补救选项,而不是系统悄悄重试。
单一技能超时重试解决后,还有更复杂的场景:多个技能串行编排时,整体链条的耗时预算需要分配好。比如组合技能A调用B和C,B最大耗时应被限定在整个预算的40%,C最多占60%,避免前面环节耗费太多导致后面没有缓冲余量。
4.2 并发与资源隔离
当Agent服务于多个用户时,技能执行就可能并发发生。如果某个技能内部使用了共享的内存缓存变量,或者公用的数据库连接池,不加控制就会出现数据错乱。
我在系统里采用了“技能隔离舱”的思路:每个技能执行体跑在独立的执行上下文中,拥有自己的资源引用和日志通道。同一技能的多个并发请求共享连接池,但请求级别的业务状态不跨请求共享。日志通道里会包含技能ID、请求ID、用户会话ID,这样任何一个请求在哪个技能上出了问题,都可以快速回溯。
这里有另一个容易踩的坑:技能内部不要用全局变量做用户级数据传递。就算Python单线程模式看起来不会并发,多线程部署一上来,全局变量就会变成竞态条件温床。正确办法是数据都挂在请求上下文对象上,跨技能传递时显式传递上下文引用。
4.3 可观测性设计
技能系统的可观测性主要盯三个面:调用量、成功率、丝路耗时。调用量能反映业务对技能的依赖度,同时能暴露哪些技能常年闲置,可以考虑下架。成功率指标要细化到失败原因,是参数失败、超时、还是远端业务返回失败,排障效率完全不同。链路耗时则按技能自身、技能下游、大模型决策三段拆分,哪一段慢,优化重点就放在哪一段。
实现上,不需要第一天就上特别重的调用链追踪系统。简单做法是在技能运行时埋点,把每次调用的元信息异步写入日志,再定时聚合成监控数据。只要每次调用都写入了技能ID、调用结果、耗时、参数指纹哈希、上下文摘要词,事后绝大多数问题都能通过日志查出线索。
5. 常见问题与排查技巧实录
5.1 模型总是不触发技能,怎么办
这是碰到过最多的求助,模型在那里一通发挥,就是不调用工具。排查看不到明显配置问题的话,大概率是以下两种原因。
第一种原因是技能描述与用户表述之间语义距离太大。比如技能描述写的是“订单状态查询”,但用户口语会说“我的快递到哪了”,如果决策桥在动态注入技能时没有结合口语化的改写,模型根本想不到该用这个技能。解决办法是把用户高频口语说法收集起来,写进技能的触发条件里,而不是指望模型自动泛化。
第二种原因是技能候选集漏掉了该用的技能。如果某种问题恰好没被召回,模型再聪明也看不到这项能力。这时需要排查决策桥所有环节的语义匹配分阈值是不是太高了,或者向量词典里技能文本是不是太干瘪,连示例句式都没写。给技能描述加几种典型问题变体,往往立竿见影。
5.2 技能参数被填得乱七八糟
模型填错参数是高频问题,最常见的错法是把对话里任意一个数字当作订单号。排查这类问题,首先要检查参数Schema中字段的description是否写清楚了来源。如果描述里只写“订单号”,没有强调“必须是系统中存在的完整订单号”,模型的发挥空间就太大了。
处理手段有两个方向。一是在参数处理器里增加规则校验,比如订单号必须有固定的前缀且长度为20位,不合规直接丢弃并要求澄清。二是在模型侧给示例,就是预设一个“用户说X,应当抽取Y”的样例,直接放进技能声明中。某些通用模型对少数示例的服从性提升非常明显,一个看似简单的例子,往往把准确率提高不少。
遇到反复纠正还经常错的场景,我建议直接在决策桥层动手,不要等模型抽完参数再亡羊补牢。可以针对特定技能做一个更小的抽参子模型,替换通用模型负责该技能的参数生成。代价是多调一个模型,但换来的是实打实的稳定性提升。
5.3 技能被误触发,行为边界失守
技能误触发是比不触发更麻烦的问题,因为看起来一切都在正常运作,但实际上做了不该做的事。比如用户只是随便问了一句“退款麻烦吗”,结果Agent自动发起了退款申请,场面就很尴尬。
这是典型的边界描述缺失导致的问题。技能声明里的when_not_to_use字段一定要认真写。不能只写“用户问退款时触发”,还必须写清楚“仅当用户表达明确退款意愿并提供了订单号时触发;用户只是询问流程、抱怨政策时,不得调用”。
另外,动作型技能的调用确认机制必不可少。凡是会产生业务副作用的高危技能,在执行前应该生成一个“操作摘要”回传给模型,让模型向用户做二次确认:“您确认要退款订单123456吗?”得到肯定回复后再执行。这个步骤虽然多了来回,但能通过防误触发的最后防线。
5.4 技能版本升级导致调用异常
技能迭代升级是常态,但升级过程中经常出现新技能行为与旧调用方式不兼容的问题。我自己吃过这个亏:某次给“查询库存”技能加了必填参数warehouse_id,结果下游所有调用方因为没传这个字段集体报错。
技能升级需要遵循兼容性规则。新版本必须保持老版本可调用,可以将新参数设为可选,内部用默认值兜底。确实要破坏兼容时,应使用新id发布,例如stocks.query_v2,并在注册表中保留旧技能一段时间,给业务侧足够的迁移时间。运行时的版本管理能力越强,技能系统的迭代成本就越低。
5.5 技能卡死导致整体响应变慢
某段时间系统延迟飙升,监控发现是有个技能一直阻塞在远程接口调用上,因为下游服务性能劣化且超时设置没兜住。查日志时发现所有请求都在等这个慢技能返回,其他领域的技能即使可用也被拖到“连带堵车”。
解决思路是给每个技能设定独立的超时值并启用熔断。熔断逻辑是,当某技能在滑动窗口内连续失败达到阈值,就直接进入“快速失败”模式,在短暂窗口期内不再真正执行,直接返回降级结果。恢复期后放行少量试探请求,成功率达到要求就自动回归正常。这样单个技能的问题不会拖垮整个Agent。
6. 我踩过坑之后沉淀的几点体会
技能系统这个东西,做起来不如调模型炫酷,但它的扎实程度直接决定Agent能不能从Demo走向生产环境。我个人的体会有几条。
第一条,技能描述永远值得花时间打磨,远比调参影响大。我在内部分享时经常说,技能描述就像招聘JD,写得越清楚,来面试的模型就越精准。JD含糊的后果,是垃圾简历连天,JD改清楚后,匹配率直接翻倍。每一条技能描述开始的几天看起来都“已经够清楚了”,但真正用一段时间后总能发现新的模糊点。
第二条,动态技能选择不是可选项,而是必选项。只要技能数量超过十个,就必须做召回而不是全量塞给模型。召回机制的建立不复杂,但见过太多团队因为起步时工具少,没把动态选择框架搭出来,后期技能一膨胀,头疼无比。框架趁早搭,后面持续加技能就不会yq,永远能保持添加技能的低成本和高稳定性。
第三条,技能执行结果一定要做结构化包装。这个看起来是小事,但影响模型下一步决策很大。模型生成回复时,看到混在一起的文本和JSON,经常搞不清楚状态。统一的结构能让模型把注意力放到“结果反馈引发的下一步动作”上,而不是解析结果本身。
现在这个技能框架已经成了我在多个业务场景中搭智能体的底座。新需求来了,多数情况下只是在技能库中新增一条定义,写一个执行函数,然后在描述里补充足够多的触发示例和边界条件。Agent的整体可靠性能够保持在稳定水位,这个思路是绝对绕不开的。
如果你正好也在弄Agent方向的东西,不用照着全部照搬,先从“技能定义标准化”和“动态技能选择”这两个环节切入就好。把这两个点做扎实,你的Agent体感提升会非常明显。剩下的细节可以等做的时候再补。