“agent-skills”这个标题我盯了很久。乍一看就是一个兜售“给Agent添加技能”的教程类项目,但实际拆开后,它踩中的是整个AI应用落地里最容易被低估、却又最决定产品上限的一层:技能系统设计。
如果你是刚接触Agent开发的新手,大概率会遇到这种场景:大模型本身什么都会一点,但让它干正经事就拉胯——让它整理会议纪要,它把无关内容也塞进去;让它调用外部API,参数传得驴唇不对马嘴;让它跟别的模块协作,又频频超时和幻觉。问题不是模型笨,而是你根本没给它设计“手”和“脚”。技能系统就是给模型配好工具、说明书写清楚、边界划明白的那一层。这篇文章我会围绕“agent-skills”这套思路,把技能系统从设计原则、编码实现、编排策略到踩坑排查,完整过一遍。适合正在做AI应用、Agent工作流、智能体平台的同学参考,也适合想系统理解Agent工程化原理的开发者。
1. 技能系统设计:Agent能不能干活的真正分水岭
1.1 技能并不等于工具,更不等于函数
很多团队做Agent时,上来就把一堆内部函数暴露给大模型,美其名曰“工具调用”。但干着干着就会发现,模型根本Hold不住:函数太多它不知道选哪个;参数太复杂它频频捏造;报错信息它读不懂,直接在逻辑里绕圈。
技能和工具函数最本质的区别在于:工具是“给程序调用的接口”,技能是“给模型理解的能力单元”。一个技能应该是一个自包含的、带清晰边界、有输入输出协议、有错误处理策略的完整封装。就好比给下属布置任务,你不会说“你去把CPU利用率读一下、把内存读一下、把日志翻一下、把五个文件打开”,而是会说“你去查一下这个服务的健康状态”。健康状态这个事,就是技能。
所以设计技能的第一步,是从业务目标出发,把Agent需要完成的原子能力拆分成技能清单。这一步做得好的标志是:每个技能都能用一句话说明白“什么时候用、输入什么、返回什么”。说不清楚,就说明这个技能拆得不到位。
1.2 技能系统的三件套:函数本体、Schema、描述文本
一个能稳定工作的技能,至少包含三个组成部分。
函数本体负责真正执行逻辑。它可以是本地函数,也可以是HTTP调用、数据库查询、第三方SDK封装,核心要求是逻辑单一、可重入、不藏副作用。
Schema是技能的“参数说明书”,它告诉模型这个技能接受哪些参数、每个参数是什么类型、哪些必填哪些可选。这块最常见的坑是:把Schema写得太细,把内部数据结构整个铺开;或者写得太粗,参数描述含糊,导致模型随意发挥。
描述文本是写给模型读的“使用手册”。这往往是新手最容易忽略、但实际对成功率影响最大的一部分。同样的函数,描述写得好不好,准确率可能相差30%以上。描述文本需要包含:这个技能是干什么的、典型使用场景是什么、参数有什么隐含要求、什么情况下不应该用这个技能。
这三件套缺一不可。很多团队在原型阶段图省事,只写了函数和参数列表,效果往往惨不忍睹。后来老老实实把每个技能配了详细描述,一次性成功率翻倍都不夸张。
1.3 技能分类:通用技能与领域技能分层
技能系统做大了以后,需要分层管理。我惯用的分法是两层。
通用技能是跟具体业务无关的基础能力,比如“网络请求”“文件读写”“时间日期处理”“计算器”“代码执行器”。这些技能可以跨项目复用,沉淀成团队公共技能库。它们的特征是稳定、通用、几乎不需要针对业务定制。
领域技能则是跟具体业务场景强绑定的能力,比如“查询订单状态”“计算运费”“检查合规关键词”“生成报表”。这些技能通常需要懂业务的人参与梳理,而且会随业务迭代频繁变更。领域技能一般放在业务代码库里,走独立的发布流程。
这种分层最大的好处是:模型在决策时能更快锁定候选技能;技能变更时影响面可控;通用技能可以被多个项目间共享,减少重复开发。我见过不少团队把所有技能不分青红皂白塞进一个仓库,最后技能超过几十个之后,模型的选择准确率肉眼可见地下降。
2. 实操基础:5个能直接提升成功率的细节
2.1 技能命名:用“动词+名词”,别搞艺术创作
模型对技能名的理解基本靠语义匹配,所以命名一定要直白。比如“query_order_info”“send_email”“calculate_shipping_fee”,一眼就能看懂。不要用“order_helper”“data_processor”这种含糊的名字,更不要用缩写和内部代号。
还有一点容易被忽略:技能名不要相互包含。比如一个技能叫“get_order”,另一个叫“get_order_detail”,模型很容易混淆。更好的做法是让技能边界互斥——要么用“get_order_basic”和“get_order_detail”,要么干脆把两个能力合并到一个技能里,通过参数区分。
2.2 Schema设计的三个关键原则:扁、少、严
Schema设计直接影响模型能不能正确传参。基于我大量团队的实测反馈,有三个原则非常有用。
第一,参数结构尽量扁平。嵌套过深的JSON对象非常考验模型的出参能力,一层两层还能应付,三层以上错误率飙升。能摊平就摊平,必要时可以把内部结构简化。
第二,参数数量宁少勿多。如果你发现一个技能需要十几个参数,先别急着写Schema,回头想想是不是这个技能的拆分粒度不对。一次调用传两个三个参数,模型表现最稳定;超过七个参数,幻觉率显著上升。
第三,枚举值和格式要严格限定。能提供枚举值的字段,全部列出来;有固定格式的,比如日期格式、电话号码格式、货币单位,都要在描述里写清楚,并在函数本地做一层校验,模型传错了也要能容错接收。
2.3 描述文本的写作模板
我给每个技能写描述时,基本套用一个固定结构,效果很稳定。
第一段:一句话说清技能用途。 第二段:说明典型触发场景,用“当用户需要……或……时,你可以使用这个技能”。 第三段:逐参数解释含义,特别是那些不是一眼能看明白的参数。 第四段:明确写出不该用这个技能的场景,防止模型乱调用。
别小看第四段,很多时候模型选错技能不是因为不知道该用什么,而是因为没有收到“什么不该用”的约束。你把边界画清楚,模型的行为会大幅收敛。
2.4 错误处理:模型读得懂的错误,才是好错误
传统函数开发里,错误处理面向的是程序员。技能系统的错误处理,面向的是模型。
当技能执行失败时,返回给模型的错误信息必须包含三个信息:错在哪一步、可能的原因、下一步建议。例如“文件不存在,请先调用list_files确认文件路径”“用户余额不足,请引导用户充值后再试”。这种错误信息能让模型自主纠错,而不是直接摆烂或无限重试。
我见过很多团队返回一堆堆栈Trace给模型,模型根本看不懂,只能反复调用同一个失败的技能,白白浪费Token和时间。技能系统的错误信息本质上是在给模型铺一条“能走通的岔路”。
2.5 幂等性与副作用:必须提前想清楚
技能系统的调用是模型自主发起的,同一个技能可能因为模型重试、超时重发、链路重放等原因被执行多次。如果你的技能内部有副作用——比如发邮件、扣费、写库——就必须让技能具备幂等性。
最简单方案是给每次任务生成一个请求ID,技能内部用这个ID做去重;复杂一点可以做状态机加业务校验。我在实际项目中遇到过一次事故:模型因为网络超时重试了三次“创建订单”技能,结果系统生成了三笔订单,用户差点被重复扣款。从那以后,所有带写操作的技能,上线前都会被强制要求过一遍幂等性评审。
3. 从零到一带一个技能上线:完整实操过程
3.1 选择技能描述格式:JSON Schema是当前最稳的选择
当前主流Agent框架对技能描述的支持,基本都兼容JSON Schema。我推荐用这个格式,不因为它是某家巨头的私有标准,而是它本身就是一个跨语言、可校验、表达能力足够强的开放规范。
技能需要对外暴露的字段主要有:技能名称(name)、功能描述(description)、参数定义(parameters)。下面是一个我实际用过的会议纪要结构化整理技能的定义:
{ "name": "structure_meeting_minutes", "description": "将会议记录文本整理为结构化会议纪要。当用户提供会议录音转文字内容、会议笔记等原始材料,并要求整理成纪要时,使用此技能。输出包含会议主题、时间、参会人、讨论事项、决议和待办任务。", "parameters": { "type": "object", "properties": { "raw_text": { "type": "string", "description": "原始会议记录文本,可以包含口语化表达、非完整句子,长度上限为2万字符" }, "meeting_date": { "type": "string", "description": "会议日期,格式为YYYY-MM-DD。如果原始材料中未提及,可以为空" }, "meeting_type": { "type": "string", "enum": ["日常例会", "项目评审", "客户沟通", "其他"], "description": "会议类型,无法判断时使用其他" } }, "required": ["raw_text"] } }注意几个细节:可选字段meeting_date明确写了“没有就为空”这个隐性许可,模型就不会硬编造一个日期;meeting_type的枚举收敛了模型的选择范围,避免它自由发挥出奇怪的类型。
3.2 函数本体的落地写法
协议定义好之后,函数本体反而比较简单,但它有几个隐藏工程点。最关键的包括幂等性,也就是说,只要输入相同,不管调用多少次,输出都完全一致。对于纯分析类、整理类技能,这天然就满足;但对带外部副作用的需要额外处理。
函数内部还要做参数消毒和校验。模型填的参数经常会有格式问题,比如把数字写成字符串、日期写成“今天”,函数本体要能处理这些情况。太较真不行,太宽松也不行,我的原则是:能自动修复的就修复,不能修复的返回结构化错误信息。
3.3 把技能注册进Agent系统
技能定义好了,怎么让模型在对话中感知到它?主流做法是把技能列表放进System Prompt里,模型在做工具选择时,会读到候选技能的全部信息。一次性把所有技能全部暴露给模型,既费Token又容易干扰判断。更高效的做法是做技能路由预筛。
比如用户的请求是“给张三发一封邮件说项目延期”,即使系统里有五十个技能,只需要把“发送邮件”“查询联系人”这两个候选技能提供给模型就够。预筛规则可以做成基于关键词的规则引擎,也可以做一次轻量级的语义匹配。
我也试过完全靠LLM自己从全部技能里做选择,在小规模下还凑合。技能超过二三十个后,选择准确率掉得很厉害。加了一步预筛之后,效果立刻改善。这个思路不是谁发明的,而是很多Agent框架里的标配设计。
3.4 Agent主循环:一次完整的技能执行链路
把上面的内容串起来,一次完整的技能执行链路是这样的:
用户发出请求,预处理模块判断是否需要调用技能;如果需要,候选技能集合经路由预筛后连同System Prompt一起交给模型;模型在推理中决定调用哪个技能、填入什么参数;解析出的函数名和参数被送到执行器,校验、执行、返回结果;结果写回对话上下文,模型继续生成面向用户的内容。
这整条链路里,最需要盯住性能的是两步:一是候选技能列表的生成速度,二是返回结果在上下文中的组织方式。前者影响响应时延,后者影响模型后续判断的准确性。
3.5 用一次调试验证整个链路
我用一个简单的示例串联一下,假设技能是“查询订单状态”,别笑,确实简单,但对新手验证链路足够。
模型会生成类似这样的调用意图:
{ "name": "query_order_status", "arguments": "{\"order_id\": \"20240521001\"}" }执行器解析出来后,调用对应的查询函数,返回结果给模型:
{ "order_id": "20240521001", "status": "已发货", "tracking_number": "SF1234567890", "estimated_delivery": "2024-05-25" }模型拿到这个结果后,组织成用户友好的自然语言回复。
这一步看起来简单,但它验证的是完整链路:模型能选对技能、能填对参数、执行器能正确调用、返回结果能被模型消化。链路走通了,后面的一切都踏实了。
4. 多技能编排与协作:从单点能力到复杂工作流
4.1 顺序编排:一个技能喂给另一个技能
最基础的多技能协作方式,是把上一个技能的输出,作为下一个技能的输入。比如用户说“帮我找一下和某供应商的合同,把里面的付款条款提取出来”。这个任务可以被拆成:搜索文档 → 读取合同 → 提取条款,三个技能依次调用。
实现上,关键点是上下文传递和中间结果保存。两个技能之间的数据是结构化的,不要通过自然语言转述,容易出现信息损耗。通常的做法是维护一个执行上下文空间,前一个技能的输出直接写入Context的指定Key,后一个技能从Context读取。
4.2 规划器模式:让模型决定技能的执行顺序
很多复杂任务,没法预先确定技能调用顺序。这时候可以引入规划器,让模型先做任务拆解,生成一个执行计划,再按计划逐项执行。
这种模式的使用要点是给规划器足够的上下文。模型做计划时不了解真实的数据状态,常常会计划出不可行的步骤。所以比较稳妥的做法是“规划-执行-再规划”的小步快跑模式,每执行一个技能,就把最新的状态反馈给规划器,让它根据实际情况调整后续步骤。
我在项目里用过好几版规划器,最后的结论是:与其训练出一个完美的规划器,不如设计好技能本身的“可组合性”,让每个技能都只做一件足够小的事,并且清楚地在返回结果中附带“下一步可以做什么”的提示,效果来得更真实。
4.3 技能组合与任务的封装
技能做多了之后,会发现一些固定使用模式。例如“查询天气”和“根据天气推荐穿衣”经常被组合在一起使用。这时候,与其每次让模型从头推理,不如把这种固定组合封装成一个新的技能。
封装后的技能可以让代码逻辑更清晰、调用效率更高、也方便统一维护。但要注意,不要过度封装。封装多了一层,就意味着模型对内部细节的感知更弱。我建议封装的条件是:该组合被使用的频率足够高、组合逻辑已经稳定、且内部步骤不会频繁变化,哪怕一个条件不满足,保持原样按部就班地调用就好。
4.4 循环调用的风险控制与退出机制
模型在做技能编排的时候,可能出现无限循环:它调用技能A,看到结果后调用技能B,看到B的结果又觉得需要再调用A,如此周而复始。这会让系统消耗大量资源,用户等到的只是转圈。
必须给技能编排加上终止条件。最简单的方案是设置最大调用次数(比如单轮任务最多调用15次技能),超过强制终止并做最后回复。更高级的方法是做异常行为检测,发现相同技能连续被调用超过N次,自动插入人工干预提示。
循环问题很难完全避免,但可控性一定要有。再啰嗦一遍我踩过的坑:上线前一定要做压力测试,用一个多步骤任务连续跑几十次,观察有没有循环和超时现象。
5. 常见踩坑与排查技巧:做完20多个技能后的教训清单
5.1 模型选错技能:描述冲突与召回不足
模型选错技能,最常见的原因是技能描述之间有语义重叠。比如一个技能叫“查询库存”,另一个叫“查询商品详情”,当用户问“这个商品还有吗”的时候,模型大概率会在两个技能之间徘徊。
排查方法是把技能描述全部打出来,人肉检查有没有相似的触发词。改法也很直接:要么调整描述,让两个技能的应用边界更清楚;要么在描述里加一个“如果用户只是想了解……请使用另一个技能”的引导。
另一个原因是候选技能列表没有召回正确的技能,这是预筛规则的问题。需要看一下预筛规则是否太严格,或者关键词没覆盖到用户的表达。
5.2 参数幻觉:模型填了不存在的枚举值
参数幻觉的表现通常是:技能定义里只有“周一、周二、周三”,模型却填了“星期四”。或者日期格式要求YYYY-MM-DD,模型填了“明天”。
这类问题的根源,多半是描述文本里的约束不够显眼。把格式要求放在参数描述的最前面,并且给出正例,模型会表现好很多。比如“date:会议日期,格式为YYYY-MM-DD,例如2024-05-21”。如果模型还是犯错,就在函数本体里做容错,能解析的就解析,不能解析的返回默认值并附带提示。
5.3 错误信息被模型忽略导致的反复重试
这是我见过的最耗钱的问题:一个技能因为权限不足失败了,返回了明确错误,但模型没有理解,换个参数又重新调用了几次——白白消耗了大量Token,还拖慢了响应速度。
排查方案是看执行日志里有没有出现“同一技能短时间内连续调用N次且全部失败”的模式。优化方法是错误信息的措辞,用更简单直接的语言,避免堆砌技术术语。实在不行,可以给特定技能加了调用熔断:连续失败超过三次,该技能在本次会话中暂时禁用,防止模型继续烧钱。
5.4 超时与响应延迟:技能调用不能拖慢整个对话
技能本身的执行时间是不可控的,可能有外部服务响应慢,也可能有数据量太大。如果技能调用阻塞了整个对话流程,用户体验会很差。
调整策略是区分同步技能和异步技能。耗时低于2秒的技能,同步调用没问题;更耗时的操作一律异步化,先返回任务已受理,等结果出来再做消息推送,不让用户干等。
还有一个细节:技能返回的结果不是越大越好。大段文本丢给模型既费Token,又容易让模型在后续推理中分心。技能返回前尽量做精简,只要模型完成回复所需的最小信息集。
5.5 评测:怎么快速验证技能质量
技能系统的优化不能靠感觉,得有评测指标。我的作法是对每个技能维护一组评测用例,至少包含:典型正例(正确输入)、边界输入(罕见但合法)、反例(不应该触发该技能的输入)、异常输入(格式错误等)。定期跑一遍,统计技能触发准确率、参数抽取成功率、任务完成率。
评测周期不用太频繁,每周一次足够。但每次技能描述或Schema调整后,一定要回归测试,防止改动影响了原有能力。我吃过这个亏:优化技能A的描述,结果导致模型在相似场景下频繁误用技能A,原本能正确选择技能B的用例崩了一片。
这个坑和排查清单其实可以一直列下去。技能系统遇到的大部分问题,本质都可以归结到描述不清、边界不明、约束不严这三类原因里。每次遇到问题,先检查这三样,多数情况下能定位到根因,而不是头疼医头。
6. 进阶体会:技能系统是持续迭代的活物
技能系统不是写完就完事的一次性工程。业务在变,模型能力在变,技能本身的描述也需要持续迭代。我在实际维护中会定期做一次技能全量复盘:看调用频率数据,低频技能考虑是不是边界太窄;看失败日志,高频报错的技能优先优化;看用户反馈中是不是有反复出现的、现有技能覆盖不了的需求,这可能意味着新技能的切入点。
还有一点关于技能粒度的体会想多说几句。整体来看,技能拆得太粗,模型会频繁误用;拆得太细,上下文会被技能列表塞满,选择成本也变高。我的经验是:初期先稍微拆细一点,观察模型的行为,再把不合理的合并或删除。面试过不少候选人,一上来就谈“我们要做几百个微技能”,我一般都会问一句:“那你的模型是怎么从几百个技能里选对那一个的?”答案如果说不清楚,那大概率还没实操过。
操作过程中我还保持了一个小习惯:所有技能的描述文本,都用同一种语言风格书写。模型对风格一致性其实相当敏感,风格一致的技能列表,比一篇像散文、一篇像说明书、一篇像产品文档的混搭列表,选择成功率要高不少。这个点可能略有玄学成分,但我实测过两次,结果都是正向的。
综合来看,技能系统听起来是Agent开发里最“接地气”的部分,不涉及模型训练,不涉及复杂算法,最多就是写写描述、设计Schema。但恰恰是这部分,决定了你的Agent在真实业务里是靠谱的帮手还是瞎搅和的玩具。把技能的每一项细节抠到位,模型的能力才能真正落到底。这个体感,等你把第一个技能系列跑通之后,会感受得尤其明显。