如果你最近在搞AI Agent,应该没少被“技能化”这个概念刷屏。所谓agent-skills,就是把智能体的一次完整能力——比如查数据库、发消息、生成报表——拆成一个个可以独立注册、独立调用、独立复用的技能单元。以前大家调Agent都是写死Prompt加工具列表,Agent一复杂就全乱套。agent-skills的思路很直接:把每个能力做成一个“带说明书的功能模块”,Agent按需选择,而不是每次从零写死。这篇内容主要面向正在做Agent工程化、或者想把现有业务能力沉淀成可复用模块的开发者,我会从设计思路、落地实操、踩坑记录三个层面完整过一遍。
1. 从“一个Agent干所有事”到技能化复用
1.1 单体Agent为什么撑不住复杂场景
最早做Agent的时候,我的做法很粗糙:一个大Prompt,把所有工具的描述、调用规则、注意事项全部塞进去,然后让模型自己看着办。简单场景确实能跑,比如“帮我查一下天气”这种,工具就一两个,模型怎么都不会选错。但一旦进入真实业务,事情就完全不一样了。
举个我实际遇到过的例子:一个客服场景的Agent,需要查订单、查物流、查优惠券、算退款金额、发短信通知、写工单,前前后后十几个接口。我一开始把所有这些工具的描述全堆在一个System Prompt里,结果模型经常选错工具,比如用户问退款进度,模型却去调了“创建退款申请”的接口。更离谱的是,随着工具数量增加到20个以上,模型开始出现“幻觉式调用”——明明没有这个工具,它也会编一个出来调用。那个时候我才意识到,问题不在模型能力,而在于我给模型塞了太多“平铺”的信息,它根本没法快速理解每个工具到底是什么、什么时候该用。
这就是单体Agent的核心矛盾:能力越多,选择越难。你在一个上下文里塞的内容越多,模型对每个单项的注意力就越分散。而真实业务从来不会只有十个工具,它可能是几十个、上百个。靠一个巨大的工具清单去支撑Agent,迟早会撞上上下文窗口的天花板,也会撞上模型判断力下降的墙。
1.2 技能化的三个核心收益
后来我开始转向agent-skills的做法,把“工具”升级成“技能”。工具和技能之间看似只是叫法不同,实际差别非常大:工具通常是一个孤立的接口调用,而技能是一段可以被复用、被编排、被组合的能力单元。技能可以是“查订单”这种单步操作,也可以是“完成一次退款”这种包含校验、计算、调用接口、发送通知的多步流程。
第一个核心收益是可复用。以前写客服Agent,流程A里写一遍退款逻辑,流程B里又复制粘贴一遍,改一个接口参数要改两处,永远改不干净。技能化之后,退款就是一个独立技能,任何Agent场景想用直接挂载就行,逻辑只维护一份。第二个收益是可独立验证。技能是有明确输入输出的单元,我可以单独测试“计算退款金额”这个技能,不需要把整个Agent跑起来才知道它对不对,调试成本降了不止一个量级。
第三个收益,也是我觉得最关键的,是可动态编排。Agent拿到一张技能清单,不是一次性把所有技能塞进上下文,而是先根据用户意图找到跟当前任务相关的几个技能,再展开详细描述给模型。这就像你进一个工具房,不是把所有扳手螺丝刀全搬出来,而是根据要修什么东西,先把可能用到的两三件挑出来。上下文更干净,模型决策自然更准。
2. 先想清楚再动手:agent-skills的整体设计
2.1 技能仓库与注册表
技能化落地,第一件事不是写代码,而是设计技能仓库。这个仓库至少要能回答三个问题:系统里有哪些技能?每个技能是干什么的?技能当前的版本是不是可用?所以我建议用一个技能注册表来管理所有技能,而不是散落在一堆Python文件或者数据库记录里。
注册表里每一条技能记录,至少包含这样几个字段:技能唯一标识、技能名称、一句话描述、输入参数Schema、输出格式说明、调用方式、运行环境、版本号、依赖项、启用状态。看起来有点繁琐,但这些都是后面让Agent“看懂”技能的基础。技能名称和描述,是给大模型看的,决定了它会不会在正确的时机选中这个技能;输入输出Schema,是给解析层看的,决定了模型生成的参数能不能被正确执行;版本号和依赖项,是给你自己和其他开发者看的,方便追溯和回滚。
我见过不少团队,技能化第一步就省了注册表,直接在代码里把函数加上装饰器就完事。小项目没问题,一旦技能数量超过三五十个,没有注册表你就根本不知道哪些技能是废弃的,哪些技能描述早就过时了。再一个,技能注册表还可以作为动态加载的依据——启动时统一扫描注册,运行时按需加载,不用改一行主程序代码,新技能就能上线。
2.2 技能的输入输出协议
技能与技能之间能不能组合,取决于输入输出协议是否一致。我最开始犯过一个错:每个技能的函数签名完全是自定义的,有的返回JSON字符串,有的返回Python字典,有的返回文件路径。等到写编排层的时候,我发现根本没办法统一处理,每个技能都得写一套特殊适配代码。
做agent-skills这件事,提前把统一协议定死,比什么都重要。我现在的做法是:所有技能的输入必须是一个JSON对象,输出也必须是一个JSON对象,任何额外的东西(比如生成的文件、图片、附件)都放在输出JSON里用URL或者路径字段指向。为什么这么选?因为大模型的function calling/tool use机制天然理解JSON结构,输入输出对齐JSON之后,模型生成的参数可以直接解析,不需要写一堆乱七八糟的转换层。
输出格式我还会再加一层约定:每个技能返回的结果都包含status、message、data三个字段。status表示执行成功还是失败,message给到模型一个人类可读的说明,data才是真正的业务数据。这样Agent看到一个技能调用失败时,能根据message判断下一步怎么处理,是重试还是换个技能,而不是拿到一堆堆栈信息手足无措。
2.3 编排层:让Agent自主选技能而不是全塞进去
有了技能注册表,也已经定义好了输入输出协议,接下来就是最关键的一层:编排。这一层的核心任务是先选技能,再用技能。选技能的时候,不是把几十个技能全扔给模型,而是先做一个粗粒度的匹配,缩小范围。粗粒度匹配可以很简单,比如基于用户问题的关键词、意图分类结果、当前Agent所在场景的预设标签,把候选技能缩小到五六个以内。
这个粗筛我建议不要依赖模型,纯关键词语义匹配就够用。等候选技能确定后,再把这几条技能记录展开成完整的参数说明,交给大模型去选择并生成参数。这套“先粗筛、后精调”的方式,其实是在模仿人的决策过程:你先根据直觉判断大概要用哪些工具,再仔细看说明书确定具体怎么用。模型在五个技能里挑一个,比在五十个技能里挑一个要准得多,而且上下文占用量也小得多,响应速度肉眼可见地变快。
另外,编排层还要负责技能之间的串联。比如“处理退款”这个技能,内部会依次调用“查询订单”“计算退款金额”“调用支付网关退款”“发送通知”等多个子技能。这些子技能之间的逻辑应该是编排层写死的流程,而不是让模型临时决定下一步调什么。模型适合做的是理解意图、选择入口技能;一旦进入一个技能内部,执行顺序应该是确定的、可测试的,否则整个系统就变成了一个黑箱。
3. 实操:从零搭一套可复用的Agent技能库
3.1 用JSON Schema定义技能接口
动手的第一步,是用JSON Schema把技能接口定义出来。我习惯把每个技能都保存成一个独立的JSON文件,文件名就是技能标识,这样做的好处是:技能天然具备版本管理能力,以后想加减字段,只需要改一个文件;任何语言写的主程序都能解析这份定义,不绑定特定编程语言。
以“查订单”技能为例,一个最简定义是这样:
{ "name": "get_user_orders", "description": "根据用户ID查询最近一段时间内的订单列表,返回订单号、商品名称、金额、状态。适合在用户询问‘我买了什么’‘我的订单怎么还没发货’时使用。", "parameters": { "type": "object", "properties": { "user_id": { "type": "string", "description": "用户唯一标识,一般从会话上下文里取" }, "days": { "type": "integer", "description": "查询最近多少天的订单,默认30,最大90", "default": 30 } }, "required": ["user_id"] }, "returns": { "type": "object", "properties": { "order_id": { "type": "string" }, "product_name": { "type": "string" }, "amount": { "type": "number" }, "status": { "type": "string" } } }, "version": "1.2.0", "enabled": true }这里最需要注意的就是description。我发现很多人定义技能时,description写得特别短,比如“查询订单”四个字就完了。但模型判断该不该用这个技能,基本全靠这段文字。所以我写description的习惯是:不只说这个技能干什么,还要说清楚在什么场景下用,以及什么情况下不要用。比如上面那段末尾加一句“适合在用户询问……时使用”,模型就能更准确地跟用户问题做匹配。
3.2 技能注册与元信息管理
技能文件准备好之后,需要一个加载器把它们读进来,注册到内存里的技能表中。注册这个动作听起来简单,但有些细节值得注意。我用Python写过一个最简单的注册器,大概长这样:
import json from pathlib import Path class SkillRegistry: def __init__(self, skill_dir: str = "./skills"): self.skill_dir = Path(skill_dir) self._skills = {} def load_all(self): for f in self.skill_dir.glob("*.json"): skill_def = json.loads(f.read_text()) if not skill_def.get("enabled", True): continue # 简单校验:必填字段不能为空 assert skill_def.get("name"), f"{f.name} 缺少 name" assert skill_def.get("description"), f"{f.name} 缺少 description" assert "parameters" in skill_def, f"{f.name} 缺少 parameters" self._skills[skill_def["name"]] = skill_def return self._skills def get(self, name: str): return self._skills.get(name) def match(self, keywords: list[str]): """非常粗粒度的关键词匹配,用于快速缩小候选技能范围""" candidates = [] for skill in self._skills.values(): desc = skill["description"].lower() scored = sum(1 for kw in keywords if kw.lower() in desc) if scored > 0: candidates.append((scored, skill)) candidates.sort(key=lambda x: x[0], reverse=True) return [skill for _, skill in candidates[:6]]注册的时候我加了两个动作:一是校验必填字段,二是跳过未启用的技能。这样新技能上线时可以先把enabled设为false,部署完再切true,不需要动代码。关键词匹配这块千万别写复杂,先跑起来,后续可以用向量检索替代,但前期用关键词足够验证整套流程是否顺畅。
3.3 让Agent学会“看说明书”用技能
技能注册好之后,剩下的事情就是让Agent学会调用。调用方式本质上还是走大模型的function calling能力,但agent-skills的要点在于:传给模型的是注册表里的精简版技能,而不是所有技能的全量定义。
在具体实现时,我会在对话的开始阶段先做一次候选技能匹配,把匹配到的技能完整Schema传给模型。其余技能只保留一个名称,不让模型看到参数细节。这样带来的直接好处有两个:第一,模型需要“阅读理解”的内容大幅减少,选择准确率会明显提高;第二,传输给模型的Token少了,单次请求的延迟和成本都在降。
一个比较典型的调用流程是这样的:用户说“帮我看看我上个月买了哪些东西”,Agent先做意图识别,抽出“订单”“上个月”两个关键词,通过注册表的match方法选出两三个候选技能,比如get_user_orders、get_user_refunds。然后把这两个技能的完整定义拼进请求里,模型生成一个JSON格式的调用意图,比如:
{ "name": "get_user_orders", "arguments": { "user_id": "user_12345", "days": 30 } }拿到这个JSON之后,执行器查一下注册表,找到对应的执行函数,传入参数跑起来,再把返回结果转换成前面约定的统一输出格式,交回给大模型生成最终回复。整套链路里,模型的核心任务只是“选技能、填参数”,真正的业务逻辑全都在技能内部处理,这样拆开之后,每个环都可以单独优化、单独测试。
4. 真实踩坑记录:技能化落地最容易翻车的地方
4.1 技能粒度:拆太细和拆太粗都是灾难
技能粒度这个问题,是我花了最多时间调优的,也是最难给出标准答案的。我一开始倾向拆很细,认为一个函数一个技能才足够灵活,比如“获取用户ID”“获取用户地址”“获取用户手机号”都是独立技能。结果模型经常不知道怎么组合它们,或者为了完成一个简单请求连续调用四五个技能,中间只要一步参数没对齐就全盘出错。
后来我又走向另一个极端,把一大段业务流程直接包成一个“万能技能”,比如“处理售后订单”这种,里面又是查订单又是算金额又是发消息。粒度是粗了,模型确实不会选错了,但这个技能完全没法复用,换个电商场景、换个售后规则,就得复制一个新的。
我目前的经验是,技能的粒度应该对齐“业务动作”而不是“函数接口”。什么叫业务动作?对用户来说,“查订单”是一个动作,“退款”是一个动作。对内部流程来说,“订单风险校验”是一个动作,“计算可退款金额”也是一个动作。粒度判断的标准其实很简单:这个动作在其他场景里有没有可能被复用?如果大概率会被复用,就独立成技能;如果只是某个流程里的中间步骤,而且永远不会单独被调用,那就留在流程内部,不需要暴露给模型。
4.2 技能描述写不好,Agent根本不会调用
这个坑我踩得最深。有一段时间,系统里某个技能明明存在,而且功能完全正常,但Agent就是不用它。后来我把技能描述调出来一看,发现只写了寥寥一句话:“检查订单状态是否允许退款”。模型看到这句话,根本不知道这个技能跟用户的哪句话能对上。
那之后我把技能描述当成面向模型的用户文档来写,一个合格的技能描述至少要包含三层信息:第一,这个技能是做什么的,说清楚功能边界;第二,在什么场景下会被触发,最好直接写出用户可能说的话;第三,什么情况下不应该使用,用一两句负向描述帮模型排除错误选项。
举一个我改完之后的真实例子,原来是“检查订单是否可退款”,改成了“检查订单是否满足退款条件,并返回可退款金额与原因说明。当用户发起退款申请、询问能否退款、或者客服在审核退款时使用。注意:如果用户只是想了解订单状态,不需要调用本技能”。改完之后,模型调用这个技能的准确率有了非常明显的提升。所以如果你发现Agent老是不用某个技能,不要先怀疑模型,先去读一遍你的描述,问自己能不能一眼看懂。
4.3 依赖关系与状态隔离怎么处理
技能一旦变多,就会出现依赖关系。比如“发起退款”这个技能,它依赖“查订单”的结果。最直观的做法是在技能定义里加一个depends_on字段,把依赖关系写进去。但实际跑起来之后你会发现,让模型去理解依赖关系是一个非常痛苦的事情,它可能根本不在意,照样在没拿到订单数据时就调退款接口。
我的处理思路是:把依赖尽量封装在技能内部,不让模型感知。比如“发起退款”内部直接调用订单查询的Service层方法,而不是依赖Agent先调用“查订单”技能拿到结果再传进来。也就是说,技能对模型暴露的是一个完整的入口,模型只需要传一个user_id,剩下的“先查订单、再算金额、再退款”全是技能内部自己完成的。这样模型永远不需要掌握一个多技能编排的图,它面对的依然是一个个独立动作。
状态隔离则是指:每个技能的执行都应该尽量无状态,或者状态存在有明确标识的会话上下文里。技能的输入必须包含它需要的所有关键信息,不能靠上一次调用的隐式状态。否则Agent上下文一换,技能执行结果就会莫名其妙串场。我自己就遇到过用户A的订单信息被用户B的查询带出来,排查到后面发现是技能里用了模块级全局变量缓存订单号。这样的问题极其隐蔽,测试还测不出来,所以后来我硬性要求:技能内部禁止使用全局可变状态,所有跨调用数据都走上下文对象显式传递。
5. Agent技能化常见问题速查
5.1 高频问题与排查思路
技能化落地过程中,有些问题反复出现,我整理成了一张速查表,适合在调试的时候逐条对照。
| 现象 | 常见原因 | 排查方向 |
|---|---|---|
| Agent完全不调用某个技能 | 技能描述太短,模型没理解适用场景 | 重写描述,补充正面触发场景和负面排除场景 |
| Agent调用技能但参数经常填错 | Paramters Schema描述不清晰,缺默认值 | 给每个字段写详细说明,能设默认值就设默认值 |
| Agent频繁调用错误的技能 | 候选技能范围太广,相似技能太多 | 检查粗筛逻辑,增加意图标签,或在描述里写明区别 |
| 同一技能在多轮对话里返回不一致 | 技能内部依赖了全局状态 | 排查全局变量、模块级缓存,改成显式上下文传递 |
| 技能执行成功但Agent回复文不对题 | 输出协议没有统一,模型没拿到关键字段 | 统一status/data格式,把最核心的信息放在data最前面 |
| 新技能上线后老技能经常失灵 | 技能描述冲突,模型分不清 | 检查新技能的描述,增加“不要使用”的排除描述 |
| 技能数量一多,响应变慢 | 传入模型的技能描述太多 | 强化前置粗筛,确保单次请求只传3-6个技能定义 |
| Agent在流程中间突然跳去调别的技能 | 技能粒度太细,模型被迫做额外决策 | 把多步封装成高一层技能,缩小模型决策范围 |
排查的时候我还建议养成一个习惯:把每次模型调用时接收到的技能列表、选中的技能名、生成的参数、执行结果全部记到日志里。技能化系统的调试,本质上是看模型“看到了什么、选了什么、做得怎么样”。没有完整的链路日志,出了问题只能靠猜。
5.2 判断一个技能该不该拆出来的三个尺度
最后分享三个我判断技能是否合格的尺度。第一个是独立可用:这个技能单独给到一个新开发的Agent,只靠描述就能被正确调用,而且输入输出都自洽。如果你还需要额外解释一堆背景,说明它还不够独立。
第二个是可复用性:这个技能至少能在两个以上场景里使用,或者你有明确的规划认为未来一定会在别的场景用到。如果一个技能永远只能服务于唯一一个流程,那就把它并进流程里,不要让它占Agent的决策位。
第三个是可测试性:技能测试不应该依赖整个Agent环境。只要给定合法输入,它就必须返回可断言的结果。如果一个技能内部依赖外部服务、数据库、缓存等各种环境,那你必须提供测试桩。不可测试的技能,后面一定会变成定时炸弹。
每接一个新项目,我都会拿这三个尺度重新过一遍现有技能列表,该合并的合并,该拆分的拆分,该下线的下线。这个动作看起来很普通,但对整个系统的稳定性和后续扩展能力的影响是决定性的。
我个人在实际操作中的体会是,agent-skills最大的价值不在于“把工具改成技能”这个形式,而在于它逼着你去想清楚系统的边界到底在哪里。以前写代码我只需要想清楚函数接收什么参数、返回什么结果,现在要想清楚模型在什么情况下会用到这个能力、它需要什么样的说明书才能理解这个能力、它返回的结果又该如何被下一个环节消费。这个过程一开始有点别扭,但一旦熬过前几次重构,后面新增场景时你会发现,大部分能力都是现成的,Agent开发真正进入了“搭积木”的状态。你手里攥着几十个已经验证过的技能,接到新业务时,只需要挑几个出来重新编排,把新逻辑补成新技能,整套系统就转起来了。这种轻松感,是单体Agent阶段完全体会不到的。