
如果你最近在搞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(keylambda x: x[0], reverseTrue) 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阶段完全体会不到的。