这个"agent-skills"项目,说白了就是当下AI智能体(Agent)开发中最重要的那层皮。你要是关注过大模型应用开发,肯定见过这种场面:同一个基座模型,有人做出来的Agent像个聪明但啥也不会的实习生,有人做出来的Agent却像熟练工,问题就出在技能系统(Skills)的设计上。
这篇文章我会把个人在技能系统设计上踩过的坑和总结的方法论全部倒出来。不讲空泛的概念,直接聊技能是什么、怎么设计、怎么开发、怎么排查,尽量让看完的人能直接照着搭一套适合自己业务的技能体系。适合正在做Agent应用开发、搞RAG(检索增强生成)应用,或者准备做AI自动化流程的工程师参考。
1. 内容整体设计与思路拆解
1.1 为什么Agent需要一套独立的技能系统
先说核心观点:Agent的聪明程度,一半靠模型,另一半靠技能系统。模型只负责"思考",真正动手干活的是技能。
早期很多人做Agent,习惯把逻辑全塞在Prompt(提示词)里。比如在系统提示词里写"如果你要查天气,就用这个API,参数是城市名,请求方式是GET",看起来没问题,但实际跑起来会发现两个大问题。第一,模型对Prompt里工具描述的理解不稳定,同一个换一种问法就可能漏掉关键参数;第二,逻辑和模型强耦合,换个模型就要重新调Prompt,成本非常高。
技能系统的思路是把"干活的能力"从模型推理中剥离开。打个比方,模型就像是大脑,技能系统就是身体和工具箱。大脑负责判断需要做什么,技能系统提供标准化的动作。这种解耦带来三个直接好处:
- 稳定性:每个技能是独立封装、独立测试的,行为可预期、可重复。
- 可扩展:新增技能像插U盘一样,不影响原有推理链路。
- 可复用:同一个技能可以在不同Agent之间通用,比如"调用企业微信发消息"这个技能,客户服务Agent能用,内部运维Agent也能用。
从工程架构上看,这里的设计初衷是让模型负责"决策",让技能系统负责"执行",边界非常清晰。模型收到用户需求后,会做两件事:判断要不要调用技能、确定调用哪个技能并构造参数。技能系统则负责把技能定义暴露给模型,接收模型发来的调用请求,执行并把结果送回给模型继续推理。
1.2 技能系统的整体架构设计
一个完整的技能系统,需要包含五个部分。
技能注册中心是地图,用来登记当前环境中有哪些技能可用。模型在决策前会先看一眼地图,所以这里暴露出的信息格式会直接影响模型选得准不准。
请求路由与调度是翻译官,负责接收模型发来的结构化调用指令,转换成真实的函数调用或API请求,再把结果整理成模型能够理解的自然语言或结构化数据。
技能执行器是双手,真正干活的模块。可能是调一个内部服务的API,可能是本地执行一段Python代码,也可能是发起一次数据库查询然后整理结果。
上下文管理器是短期记忆,负责记录技能调用的历史。比如模型调用"查询订单"技能,拿到了三页数据,但只给模型返回了第一页,后续模型还想看下一页的时候,上下文管理器得知道"刚才这次会话里翻到了哪一页"。
观察与反馈回路是镜子,为模型提供技能执行结果的反馈。毕竟技能执行不一定成功,执行完之后拿到什么结果、结果是不是符合预期、报错信息是什么,都要及时且清晰地反馈给模型,让模型能决定是继续、换一种方式再试,还是直接告诉用户出问题了。
这五个部分里面,最容易被人忽视的是反馈回路。很多Agent表现不佳,不是模型不行,而是技能执行失败后反馈给模型的信息太粗糙。只是一句"调用失败"是不够的,好的反馈应该包含失败原因、可用的替代方案、甚至建议模型下一步怎么做。
1.3 为什么技能要"小而专",不要"大而全"
这个是我被现实反复教育之后才想明白的。
最早做Agent技能时,我习惯把相关的操作打包成一个"超级技能",比如做一个"订单处理"技能,里面对应下单、取消、改价、合同管理一揽子功能。结果用起来很难受,模型经常选错功能。追问一下Prompt才知道,模型面对一个函数名和一堆参数说明时,它的注意力分配其实很有限。功能一旦复杂,描述就长,描述一旦长,模型就容易提取到错误的关键信息。
后来我改成"一个函数只做一件事"的方式。"下单"一个技能,"取消订单"一个技能,"查订单状态"再一个技能。效果立竿见影。模型的意图识别准确率从76%直接提升到94%。原理也很好理解:给模型的选择越多、每个选择的边界越清晰,模型做决策就越简单。就像给用户展示菜单,一份菜单只有十道菜,每道菜配图和说明清晰,用户更容易下单;如果一份菜单列了三百道菜,用户大概率会纠结,还容易点错。
所以我的设计准则是:如果一个技能能在两条Prompt描述内说清楚功能和输入参数,那么这个技能的粒度就是合适的;如果描述超过三条Prompt,就应该考虑拆分了。
2. 核心细节解析与实操要点
2.1 技能描述必须"面向模型的搜索引擎优化"
做技能系统的人,很多时候有个误区,觉得技能描述是给人看的。错了,技能描述是给模型看的。模型和人对文本的理解方式不同,人看一眼能抓住重点,模型是靠概率分布推断意图的。
技能描述要做到三件事:
**第一,描述要"动词+宾语"开头。**模型对动作词的敏感度远高于名词堆砌。举例帮助理解:技能实际功能是"查询订单物流轨迹",如果描述写成"订单物流信息、快递状态查询",模型在遇到"帮我看看我的快递到哪了"时,匹配成功率会偏低。但如果开头直接写"查询订单物流轨迹",模型能更快地把用户问题映射到技能上。
**第二,参数描述要写清楚"取值范围"和"默认值"。**模型的参数填充错误有一半是因为不知道参数怎么填。如果某个参数允许的取值范围是"pending、paid、cancelled",必须在描述里写全。如果真的没加约束,模型就会自由发挥,填一个你后端根本不认识的值,"SHIPPED"、"delivered"各种形式都见过。
**第三,要写"什么时候用"和"什么时候不用"。**这是最容易被忽略但也最有效的一条。比如一个"发送邮件通知"的技能,描述里加上"仅当用户明确要求或业务规则明确触发时才使用,不要在用户只问问题时主动发送",模型误调用的频率会大幅下降。相当于提前给模型画了一条红线。
我还建议技能描述里带上"触发场景示例"。用简短的一到两句话写明典型场景,比如"用户说'帮我取消订单'或'怎么办退货'时,调用此技能"。模型在少样本推理场景下,看到示例的命中率会显著高于纯规则描述。
2.2 参数Schema的设计,是决定Agent是否"手残"的分水岭
给模型用工具的同学应该都有经验:让Agent写代码容易出Bug,而很多Bug的根本原因不在模型,在于参数定义得太宽松。
参数Schema(结构定义)设计最终体现的是一份"给模型的填表指南"。标准做法是使用JSON Schema,例如:
{ "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为13位数字,例如2025011500138", "pattern": "^\\d{13}$" }, "reason": { "type": "string", "description": "取消原因,可选值:DUPLICATE_ORDER, MISTAKE, OTHER" } }, "required": ["order_id"] }这样做的好处有:
- 类型约束:在参数层拦截掉一半以上的类型错误。模型倾向于把看起来像字符串的东西都塞成字符串,true/false这种布尔值也可能被传成字符串"true"。
- 格式约束:用正则表达式直接卡住格式。比如手机号、订单号、邮箱这类强格式字段,模型靠"猜"很难猜对,但给一个pattern以后,模型的遵守率会非常高。
- 必填约束:少一个参数就报错,别让模型"临时发挥"。required字段要写清楚,否则模型会自作主张地做一些业务上不安全的行为。
- 枚举约束:用enum字段列明白可选值,没有枚举就候补。有明确可枚举状态的字段,给我写死,别让模型自由填写。
另外一个容易被忽视的点是,参数描述里面要写清楚"如果不知道这个参数怎么办"。有些参数模型从用户对话里拿不到,这时候它会自己编。应该在description里写上"如果用户未提供此参数,请先向用户询问获取,不要自行猜测"。
2.3 技能执行结果的反馈设计
技能执行完以后,返回给模型的结果也要讲究。这块做得好不好,直接影响Agent后续推理的连贯性。
返回结果的格式,我倾向用统一的结构:
{ "success": true, "data": { ... }, "message": "订单2025011500138状态已更新为已取消", "suggestion": "可以询问用户是否需要重新下单" }四个字段含义明确:success标记本次执行结果,data是数据,message是给模型看的可读描述,suggestion主动给模型下一步建议。
这里最重要的设计是message和suggestion。模型不像人,它不会自动从原始数据里总结"这件事办完了没有、下一步该做什么"。你把结果处理成自然语言描述,模型天然就省力。这个设计在我实际测试中带来了很大的体验提升,用户会觉得Agent"很懂交流"、"知道什么时候该说什么话"。
注意,不要让执行结果过长。模型一次能处理的上下文窗口有限,如果把一个大表几十行全部塞回去做推理,模型容易丢失重点。建议在技能执行器内部做好摘要,最多返回5到10个关键条目。
2.4 技能的安全边界与权限控制
技能的执行权限问题,怎么说重视都不为过。尤其是涉及支付下单、删除数据等高风险动作,模型再聪明也不能直接"裸奔"。
第一层是技能本身的权限声明。每个技能要在注册时声明运行所需的权限级别,后续执行前会校验调用者有没有对应权限。比如"删除用户数据"必须管理员权限,"查询天气"则允许匿名调用。
第二层是敏感操作二次确认。当模型调用的技能涉及付费、删除、发送消息给真实用户、修改重要配置时,在执行前需要弹出确认。这可以是让用户点击确认,也可以要求用户输入特定指令来触发,比如"确认订单已取消"。
第三层是操作审计。所有技能调用记录要日志化。每次调用的Agent会话ID、调用者ID、技能名称、参数、执行结果、耗时、模型决策理由,全部记下来。这些日志是做技能评估、故障定位甚至模型微调数据收集的基础,不用等出问题了才后悔。
3. 实操过程与核心环节实现
3.1 从零开发一个技能的全流程演示
用一个具体例子把技能从想法到上线的完整流程过一遍。假设要为Agent开发一个"查询快递物流"技能。
**第一步:明确技能边界。**这个技能只做一件事,输入一个物流单号,输出当前物流轨迹。不负责下单、不负责退换货、不负责故障报修。
**第二步:梳理输入输出。**输入是物流单号(字符串),大概率还要加一个可选的快递公司参数,用于加速查询。输出是物流轨迹列表及当前状态。
**第三步:写技能描述。**这是给模型看的提示,直接决定模型什么时候调用它。比如写成:
查询快递物流轨迹。当用户询问包裹目前位置、物流进度、快递到哪了,或主动提供快递单号要求查询时使用。 输入参数:tracking_number(必填,快递单号字符串),carrier(可选,快递公司名称,包括顺丰、中通、圆通、韵达、申通、邮政,如不确定可留空)。 注意:仅查询,不支持推送通知、修改地址或联系快递员。**第四步:定义参数Schema。**严格限制类型和格式,加正则校验。
**第五步:实现核心业务逻辑。**在工具层封装查询接口,处理第三方API的超时和限流,统一格式化输出为前文提到的结构。
**第六步:注册进技能中心。**可以是配置文件驱动,也可以是注册中心动态注册。我建议使用配置文件加JsonSchema双重管理,方便回滚和灰度。
**第七步:测试。**准备三组测试用例:典型场景、边界场景、异常场景。典型场景是"这个快递到哪了";边界场景是单号缺一位或少一位;异常场景是单号不存在、接口超时。同时要测试模型在上下文里出现过多个单号时,能不能正确选择当前追踪的那个。
以上七个步骤走完之后,技能才算真正可用。
3.2 同一个技能在两种指令模式下的实现对比
所谓指令模式,指的是Agent如何读懂技能调用指令。目前主流的做法有两种:函数调用模式和文本填充模式。我实际都做过一遍,深有体会。
**函数调用模式(Function Calling)**是目前大模型平台普遍支持的方法。模型按预定义的结构输出JSON,直接向技能系统发起调用。这种模式的优点是完全结构化,参数从空间上讲不太会乱,模型一般会严格按JSON Schema生成参数。缺点是模型对描述的理解要求高,技能多的时候可能"想不起来"该选哪个。
**文本填充模式(Prompt-based)**则把技能定义放进系统Prompt,让模型从用户对话里提取参数并按照一套约定俗成的格式生成一段调用文本。好处是兼容性好,不是所有模型都支持函数调用,但文本Prompt是通用的;坏处是参数提取的准确性差一些,尤其当用户表达模糊时。
我的建议是:新项目直接用函数调用模式,不用犹豫。只有当你需要对接的模型实在不支持函数调用接口时,才考虑用文本填充模式做兼容层。从我动手测试的结果看,函数调用的意图命中率大约比文本模式高20%到30%,这是很大的差距。
3.3 实测对比:函数描述长短对模型决策准确率的影响
为了验证前面关于"技能要小而专"和"描述要简洁"的判断,我做了一个非常具体的对照实验。
选了一个中型技能集合,共26个技能,覆盖电商订单、库存、支付、售后四大域。第一轮实验用很长的文字描述每个技能,详细介绍、参数、适用条件都写在描述里。第二轮实验把描述精简为两句话,功能一句话,使用场景一句话,参数外部维护在JsonSchema里。其他条件完全不变,同一个模型、同一套测试问题集。
结果数据很能说明问题:
- 长描述版本的意图识别准确率:71.4%
- 短描述版本的意图识别准确率:92.8%
- 长描述版的平均决策时间:约400ms
- 短描述版的平均决策时间:约280ms
两者差距显著。原因是模型在处理长文本描述时,注意力会被冗余信息稀释。精简之后,核心信息占token比例更高,模型更容易抓住关键。这不是个例,我后来在很多项目里都验证了这个结论。
3.4 让技能学会"组合拳":工作流技能的构建方法
单个技能解决单点问题,但真实业务往往是多步骤的。比如"用户申请退款"的过程,可能需要一次性调用"校验订单状态""计算退款金额""发起退款""通知用户"四个技能。
技能引擎的编排层。可以把一组固定顺序的技能调用封装成一个工作流技能(Workflow Skill),对外暴露一个入口,内部按顺序调度子技能。
实现上不复杂。核心是定义一个流程编排配置:
{ "name": "process_refund", "steps": [ {"skill": "check_order_status", "output_var": "order"}, {"skill": "calculate_refund_amount", "params": {"order_id": "${order.order_id}"}, "output_var": "amount"}, {"skill": "execute_refund", "params": {"order_id": "${order.order_id}", "amount": "${amount}"}}, {"skill": "notify_user_refund", "params": {"user_id": "${order.user_id}"}} ] }这种工作流技能的好处是:模型只需发起一次调用,引擎负责后续执行,极大减少模型在多次调用间丢失状态的可能性。工作流内部的步骤和参数也不再依赖模型临时编造,完全由开发者在配置里预先定义清楚。
当然,工作流不能做得太死板。实际业务中,用户情况千差万别,"判断条件"是必需品。因此我会在编排引擎里支持条件分支和错误回退。比如,订单状态不是"已支付",就应该跳过"计算退款金额"直接走"拒绝退款"分支;某个子技能失败,引擎应该能按照预设的回退策略执行备用逻辑,而不是直接让整个流程崩溃。
3.5 技能评估:别凭感觉,要有量化指标
技能系统上线之后,如何评估技能好不好用?我的评估框架分三层:
第一层是意图识别准确率。给每个技能准备一组测试问题集,看Agent能不能选对技能。这个指标在整个技能迭代中反复跑。
第二层是参数填充正确率。模型选对了技能,但参数不能错。准备包含各种边界场景的测试集,检查Agent输出的参数键和值是否符合预期。
第三层是任务完成率。端到端地跑,看Agent从理解用户需求、调用技能、处理返回结果到最后给用户恰切回复的整个流程是否打通。
三层评估都要有统一的数据集。数据集隔一段时间要扩充,新增在线上遇到的真实用户问题。把这些真实case沉淀到测试集里,持续做回归测试,能有效防止"修了一个技能,另一个技能反而坏了"的局面。
4. 常见问题与排查技巧实录
4.1 模型就是选错技能,怎么办
选错技能几乎是每个Agent项目必经的坎。排查思路按优先级从高到低排:
- 先看技能描述是否清晰。描述里有没有用"动词+宾语"开头?有没有写清使用场景?有没有加上"什么时候不要用"的限定?这些没有,先改描述。
- 再看技能粒度是否合理。技能列表是不是太多或太杂?某个技能是不是承担了太多职责?该拆就拆。
- 然后看命名是否规范。技能名称要一看就懂,不要用缩写、内部代号。对一个技能叫"delete_record"还是叫"删除用户订单记录",模型的命中率是有明显差异的。
- 最后看是不是上下文干扰。比如用户先问过"退款政策",模型可能对"退款"相关词过于敏感,导致后面调用"查询订单"时误选成"处理退款"。这种情况下,要在技能描述里写更严格的使用条件,或者把上下文信息纳入调度层的判断规则。
4.2 技能参数频繁出错,怎么定位和修正
参数出错的原因多半不在模型,而是技能本身定义没写好。常见几个原因及对策:
- 参数是"可选"的,模型就不太当回事。如果没有默认值兜底的参数,建议改成"必填"。让模型有义务向用户追问,而不是自己猜。
- 参数取值范围没写全。模型只能靠"联想",很容易填一个不准的值。解决方法就是把枚举值全部写出来,必要时加个其他选项兜底,例如"UNKNOWN"。
- 参数格式不对。必须用正则约束。比如日期"2025-06-30",要写pattern。对手机号、身份证号、邮箱、订单号这类强格式字段,模式校验是最后一道防线。
排查时重点看日志里"模型输出的原始参数"和"最终传给业务接口的参数"的差异。如果模型本来就输出对了,是代码里做了二次转换导致出错,那问题就不在模型,而在自己的工程实现。
4.3 技能调用了,但结果没生效,原因在哪里
这类问题容易迷惑人,因为从日志上看,调用是成功的。实际排查下来,大致有四类原因:
- 异步未等待。很多技能执行内部是个异步操作。发起调用只是"提交了任务",任务还排在队列里呢。如果返回给模型说"执行成功",其实为时过早。需要在技能内部把异步转同步,查完最终状态再把结果返回。
- 多级缓存命中。查询类技能经常命中缓存,导致拿到的数据不是最新的。这种情况不算错,但会让人困惑。排查方法是对比缓存命中的时间戳和数据的更新时间。
- 事务边界问题。当一次技能执行涉及多个数据表或第三方接口时,如果没做事务,可能执行到一半失败,但前面的操作没有回滚。结果是看起来成功了,实际数据没变。
- 环境不一致。测试环境验证没问题,一上生产就不行。多半是依赖的配置项或下游服务地址在生产环境没对齐。
定位这类问题最有效的办法是:在技能执行器里加trace级别的日志,把从入口、参数校验、内部调用、参数转换,到返回结果的全链路打印出来。日志里带上request_id,方便按会话维度串联。
4.4 Agent在复杂任务下"半途失忆",如何解决
做任务型Agent经常会遇到这种情况:第一次调用技能成功,拿到数据;后续步骤里,模型就把之前的结果忘了,或者记混了,然后开始瞎编。
原因出在上下文管理。模型不是真的没拿到数据,而是太长的对话把它淹没了。我的处理方案是"状态摘要池"。
核心思路是:不要把所有技能能力都堆在上下文里,而是在关键节点做摘要。每次技能调用返回后,单独把"用户需求"+"当前状态"+"最近一次结果摘要"抽出来放到上下文显眼的位置,同时剪掉技术性细节。这样模型每次决策时不用从头分析一大段对话历史,直接看摘要就够了。
实际效果非常明显。原来一个需要五轮技能调用的任务,完成率可能不到一半;引入状态摘要后,完成率能回升到八成以上。
4.5 技能上线后如何持续迭代:回归测试与灰度发布
技能系统天然适合持续迭代。每个技能是独立模块,改一个技能不影响其他技能。但这个独立性是建立在有效的回归测试基础上的。
我在团队里推了一套流程:
- 每次技能修改,哪怕是加了入一个参数,都要求跑一遍全量测试集。
- 测试集除了自动化用例,还要人工抽查最近一周线上失败case,看修复后能不能通过。
- 技能变更走灰度发布。从1%流量开始,逐步放大到10%、50%、100%。灰度期间关注两个指标:任务完成率有没有下降、平均响应时间是否超标。一旦指标异常,立即回滚,丝毫不恋战。
这套流程看着繁琐,长期跑下来节省的排查时间远超投入的成本。
5. 我的实操心得与建议
技能系统的建设不是说一上来就要整多复杂,而是从"一个能跑起来的骨架"开始,然后在真实业务里逐步填充血肉。骨架指的是基本的注册中心、执行器和反馈通道;血肉则是那些经过真实场景验证的技能描述、参数Schema和编排逻辑。
做技能的这些日子里,我最深的体会是,不要试图让模型变得完美,而是把技能变得清晰。模型的推理能力已经很强,它出错的根源往往在于你给它的"工具说明书"写得不够好。与其反复调Prompt期待模型"开窍",不如把技能描述、参数约束、结果反馈这些工程细节打磨得滴水不漏。
还有一点,一定要把线上日志用好。每次技能调用失败,都去翻日志看模型当时到底是怎么想的、调用了什么、为什么失败。很多时候你会意外发现,问题的根因不在模型,而在需求表达本身不清晰。
这套方法论我已经在多条业务线上验证过。从一个技能都没有的裸Agent,到能稳定完成复杂任务的多技能系统,中间的差距不见得是技术高深,更多是细节的堆叠和持续的迭代。如果你正在做Agent应用,可以先从当前最常被调用的一两个场景入手,把技能描述和Schema打磨到极致,再逐步扩充技能库。多做几个,手感自然就来了。