如果你的AI Agent还停留在"能聊天但不会干活"的阶段,那问题大概率不在模型,而在没有给模型配置一套真正可用的"技能系统"。我最早接触agent-skills这个概念的时候,以为它就是给Agent塞一堆API工具定义,跑通之后就完事了。实际做下来才发现,技能系统远比想象中复杂,它决定了一个Agent是"看起来聪明"还是"真的有用"。
这篇文章我打算把从零搭建一套Agent技能系统的过程拆开讲。从技能应该怎么定义、参数怎么设计,到技能库怎么组织、怎么挂载到Agent上,再到实测阶段会遇到哪些坑和优化方向,全部基于我自己的实操经验,不是教科书式的概念复述。适合正在做AI Agent应用的开发者阅读,也适合想理解"Agent能力边界到底由什么决定"的产品和算法同学参考。
1. 为什么Agent的能力首先体现在"技能"上
1.1 我为什么开始拆解"技能"这件事
有一次我让Agent帮我做一份竞品分析报告,结果它输出了一个目录结构的markdown文件,前三章写得工工整整,后面全是"待补充"三个字。我一度以为是模型推理能力到了瓶颈,后来把链路拆开才发现,根因是我根本没有告诉它如何调用外部搜索、如何筛选可信信息源、如何把结构化数据转换成报告章节。换句话说,模型有分析能力,但它没有"调用外部能力"的技能。
agent-skills这类项目的核心思路,就是把"模型本身的能力"和"模型可以调用的外部能力"做分层。模型负责语义理解、规划、生成,技能库负责提供可执行的操作单元。这个分层看起来简单,但落地时涉及大量细节:技能怎么命名才能被模型理解、参数怎么设计才能减少模型的自由发挥空间、技能执行结果怎么反馈给主流程,每一层都可能出问题。
我后来的做法是,把Agent看作一个"拥有工具箱的工人",模型是工人,工具箱里每一件工具就是一项技能。工人不可能记住所有工具的说明书,所以工具箱里的每一项技能都需要自带清晰的使用说明。
1.2 技能框架是什么样的一组边界
我实践的agent-skills本质是一个技能框架层,它不直接提供某个具体业务功能,而是定义了技能应该长什么样、如何注册到系统、如何被模型发现并调用。整体从上到下可以拆成三层:
- 技能目录层:负责技能的注册、存储和索引,每个技能在这里有唯一的ID和元信息。
- 技能执行层:负责把模型发来的调用请求翻译成具体的函数调用、API请求或者脚本执行。
- 技能描述层:负责生成给模型看的技能说明文本,也就是系统提示词里的工具调用说明。
这个分层思路最重要的好处,是把"技能的展示"和"技能的执行"解耦。比如同一个技能,在浅层对话场景只需要简短描述,在深度任务场景需要带详细参数约束的完整说明,技能执行层不用改动,只需要切换描述模板即可。
我之前犯的一个错误是反着来的:直接把工具函数塞给模型,每个函数带上三四种调用时序,结果模型根本搞不清楚应该先调哪个。后来借鉴agent-skills模式统一了技能定义规范,每个技能只做一件事,描述里明确写出前置条件和成功判定标准,调用准确率才真正拉起来。
1.3 "技能"和"工具"有什么本质区别
很多人一上来会问,技能不就是工具函数吗?区别确实存在,而且很关键。传统意义上的工具是一个函数、一个API,描述清楚参数就行。但技能包含几个工具之外的维度:
- 使用条件:什么场景下该用这个技能,什么场景下不该用,这个模型必须一眼能判断。
- 执行步骤:有些技能内部包含多个步骤,比如"获取资讯"要先调用搜索、再做去重、再按时间排序。
- 反馈格式:技能执行完应该以什么数据结构返回,成功和失败分别长什么样。
- 后续动作:执行完之后,是直接返回结果给用户,还是把结果交给另一个技能继续处理。
就拿"获取资讯"来说,如果按照工具函数定义,可能就是search(keyword)一个接口。但是按照技能定义,它要包含:是否指定时间窗口、是否需要过滤某些来源、返回结果是否需要按相关性排序、要不要附带原文摘要。这些约束如果全交给模型临场发挥,结果极不稳定。技能框架要做的事,就是把这些不确定性在定义层就尽量收敛,让模型只需要做选择题,而不是做自由论述题。
2. 技能系统的核心:从意图到描述,再到验证
2.1 一组好的技能定义长什么样
我现在的技能定义统一走一套schema,核心字段大概是这样的结构:
skill_name: 技能名称,全部小写,用下划线分词 description: 一句话说清楚这个技能做什么,以及什么时候使用 trigger_conditions: 触发该技能的前置条件 parameters: - name: 参数名 type: 参数类型 description: 参数含义 required: 是否必填 enum: 可枚举值时列出来 default: 默认值 success_criteria: 执行成功的判定标准 failure_handling: 执行失败时的处理建议这里我想特别说一下两个容易被忽略的字段。第一个是trigger_conditions,它解决的是"模型什么时候应该调用这个技能"的问题。很多工具定义只写了"这个函数做什么",但没说"应该在什么情况下使用",导致模型经常在不合适的场景调用。比如一个获取天气的技能,trigger条件里应该写清楚"当用户询问当前或未来某地的天气状况时调用,不适用于询问历史气候"。
第二个是failure_handling。实际跑Agent的时候,技能执行失败是非常常见的事,模型如果不知道失败后该怎么办,要么卡死,要么自作主张瞎编结果。在技能定义里明确写出失败处理建议,比如"搜索无结果时,尝试降低关键词粒度再搜一次""API报错时,等待5秒后重试一次",模型的行为会可控得多。
2.2 参数设计的关键经验
参数设计是我踩过最多坑的地方,总结下来有三条比较重要的经验。
第一条,参数宁少勿多,能省则省。每个参数对模型来说都是一个记忆负担,参数太多,模型在处理时会忘记填、填错类型、甚至凭空捏造。我曾经设计过一个生成报告的技能,有12个参数,结果测试的时候模型有三分之一的情况会出现参数缺失。后来我把参数压到4个,把其余参数全部下沉到配置里,准确率立刻就上来了。
第二条,能用enum枚举的就不用自由文本。给模型一个开放式的参数,相当于把选择权交给它,自由发挥的后果就是格式五花八门。我在"查询订单"技能里把状态字段定义为["待支付", "已支付", "已发货", "已完成", "已取消"],模型基本不会选错;而最早用自由文本时,出现过"已发"、"发货中"、"运输中"等各种变体,下游解析直接崩溃。
第三条,参数的description要用"模型听得懂"的话,而不是"程序员看得懂"的话。同一个概念,写"createTime:创建时间的Unix时间戳"模型理解得一般,但写成"createTime:用户下单的时间,格式为10位秒级Unix时间戳,例如1699999999"就清晰很多。说白了,这个文本是给大模型读的,要按它的理解方式去写。
2.3 技能描述里的"系统提示词"
技能描述本质上就是微型的系统提示词,它在模型决定"要不要调用、怎么调用"时扮演了核心角色。我在实践中发现,描述文本的措辞变动会对调用行为产生显著影响。
一个很典型的例子是"温度查询"技能的description,最初写的是"查询天气信息,返回温度、湿度、风向"。这个描述太宽泛,模型在用户问"明天适合穿什么"时也会去调这个技能,因为描述里没有约束场景。改成"当用户询问当前或未来的天气、温度、体感时调用,提供结构化温度信息,不用于穿衣建议、出行规划等综合分析"之后误用率明显下降。
还有一点,技能描述的句式会影响模型对"这是不是一个必须调用"的判断。加一句"如果用户只是想闲聊,不要调用此技能"这样的否定语句,比单纯写"该技能用于XXX"更有区分效果。我现在每个技能定义里都塞一条否定条件,实测对降低误召回非常有效。
描述之外,技能的验证环节同样重要。我每写一个技能,会先准备一组典型的用户问题作为测试集,跑一遍看实际调用率、参数正确率、结果格式准确率。这三个指标不过关的技能,宁可先不上线,因为模型一旦习惯了错误调用方式,后续要改成本很高。
3. 技能库的目录组织与版本管理
3.1 目录结构与命名规范
技能数量少的时候怎么放都行,一旦超过几十个,目录结构就成了维护性的关键问题。我目前使用的目录结构是按照"领域+层次"双维度划分的:
skills/ productivity/ # 生产力域 schedule/ # 日程相关 create_event.yaml query_events.yaml mail/ # 邮件处理 compose_mail.yaml parse_mail.yaml knowledge/ # 知识域 search/ # 检索相关 web_search.yaml knowledge_base_query.yaml extract/ # 抽取相关 article_extract.yaml data_analysis/ # 数据分析域 sql_execute.yaml chart_generate.yaml我建议每个技能的yaml文件里加一个owner字段,写上这个技能的维护人。技能挂在业务线上跑的时候,一旦出现问题,可以快速定位到人,这个字段在跨团队协作时特别有用。
命名上强制小写加下划线,禁用缩写。skill名称就是模型的"记忆锚点",一个叫get_proj_sts的技能和一个叫get_project_status的技能,对模型的辨识度完全不同。模型对语义化名称的理解远好于缩写。
3.2 技能的三种分类:原子、组合、认知
技能不全是同一类,我按照可复用粒度把技能分成三种,这个分类直接影响了技能是怎么被模型调用的:
- 原子技能:不可再拆的最小操作单元。调用一次执行一个动作,返回一个结构清晰的结果。比如"通过订单ID查询订单详情"。
- 组合技能:内部串联多个原子技能,对外只暴露一个入口。比如"生成竞品分析报告"内部会调用信息检索、页面抽取、文本聚类、报告模板渲染等原子技能。
- 认知技能:不操作外部系统,而是对模型已有的信息做加工。比如"摘要生成""观点提取""翻译改写",这类技能依赖模型自身能力,但通过技能化的方式定义了标准输出格式。
这个区分在实践中的价值在于路由策略。组合技能和认知技能通常由模型在高层级规划时调用,原子技能则更多在下层执行阶段调用。如果所有技能在同一个平面里,模型在第一步规划时就会陷入细节,导致规划质量下降。
3.3 技能版本演进方式
Agent系统上线之后,技能不可能一成不变。调参、增加约束、修复bug都会带来技能定义的变化,如果技能没有版本管理,线上出了问题根本不知道用的是哪份定义。
我现在给每个技能定义文件都维护一个version字段,同时在技能库里放一份CHANGELOG,记录每次变更的原因、变更点、影响范围。上线新版本之后,把旧版本文件保留三个月,方便快速回退。
更关键的是,技能版本升级的时候需要重新跑回归测试。技能A的定义变化,可能会影响技能B的触发判断,因为它们处于同一个提示词空间里。我遇到过的情况是,给"日程创建"加了一个参数之后,"日程查询"的调用准确率突然掉了一截,原因就是模型在技能选择的注意力分配上发生了偏移。回归的时候不要只验技能A,要把经常和它搭配使用的周边技能一起验。
4. 把技能装进Agent:挂载、路由与上下文管理
4.1 技能挂载的两种形态
技能定义好之后,要挂到Agent上才能被模型感知到。挂载方式我试过两种:静态挂载和动态挂载,各有适用场景。
静态挂载就是在系统提示词里把所有技能描述一次性给到模型,适合技能数量少、又希望模型随时可精确调用的场景。它的优点是实现简单、模型响应稳定,缺点是技能太多的时候会挤占上下文窗口,而且模型在长技能列表中的注意力会被稀释。
动态挂载则是先给模型一个技能大纲,里面每个技能只有一句话摘要,模型决定要执行某个技能时再把完整定义加载进来。这个方案解决了技能数量大的问题,但引入了新的难点:大纲摘要写不好,模型在第一层级就排除了正确技能,后面再没有挽回机会。
我实测下来的经验是以80个技能为分界线,低于80个静态挂载效果更好,高于80个建议走动态挂载。超过160个技能还继续全量塞给模型,上下文会被技能描述吃掉三分之一以上,整体响应质量和速度都不理想。
4.2 路由选择策略
有了技能列表,接下来是模型怎么选技能的问题。业界讨论较多的有两种策略:让模型自己选,还是用规则/向量检索帮他选。我在实践中尝试过两种方案的组合。
让模型自己选的优势是灵活,模型能结合上下文语义做判断。但问题是技能数量大的时候选择准确率下降,而且模型偶尔会选一个"看起来相关但实际不匹配"的技能。
用向量检索做预筛选,把候选技能缩小到5个以内再交给模型选,准确率确实能提升。做法是把每个技能的name、description、trigger_conditions拼接成一段文本做embedding,用户请求来的时候用同一embedding模型做相似度检索,取top-k作为候选集,再让模型在这几个候选里做最终决策。
我目前的方案就是"向量粗筛+模型精排"。粗筛保证候选集覆盖正确技能,精排保证在相似技能之间做出准确区分。实测下来技能命中率从裸模型自选时的82%提升到96%左右,代价是增加了一步向量检索的延迟,大约多出几十毫秒,完全可接受。
4.3 上下文管理的边界约束
技能在运行过程中会产生大量中间数据,比如技能执行后的结果、循环调用的中间状态、错误信息等,这些上下文如果不做管理,上下文窗口会迅速膨胀。
我现在用一套上下文预算机制来管理。给每类内容分配一个max_token预算,比如系统提示词不能超过3000 token、技能执行结果缓存不能超过5000 token、历史对话保留最近20轮。超过预算的部分按优先级丢弃:历史对话最优先丢弃、技能执行中间结果次之、系统提示词和当前任务描述永不丢弃。
上下文管理不只是节省窗口,更重要的是防止模型"看到"太多历史残留信息产生错误判断。有一次Agent在查询订单时突然返回了另外一个订单的结果,排查下来发现是上个任务的查询结果没有被清理,模型在历史上下文里"看到"了旧的订单号。从那之后,我在技能执行链路的开头和结尾都加了上下文清理步骤,这个问题就消失了。
5. 实测一线:从"能跑通"到"能打"的打磨过程
5.1 技能命中率不高的问题
第一版技能系统上线之后,我跑了一批真实业务请求,发现技能命中率只能在八成左右徘徊。也就是说十个请求里,有两个模型选错了技能或者压根没选技能。这个水平肯定没法交给业务方用。
我逐条分析了错误case,发现命中失败可以分成几类。最大的一类是技能描述没有覆盖用户的真实表达方式。比如技能描述里写的是"查询订单物流信息",而用户说"帮我看看我的快递到哪了",模型就犹豫了,没有匹配到技能。后来我把描述扩成一组同义表达:"当用户询问订单物流、快递位置、包裹到哪了、配送进度时调用",写成枚举式的触发条件集合,命中率立竿见影地上升。可见技能描述不是给程序看的接口文档,是给语言模型看的使用手册,必须站在语言模型理解的角度写。
第二类是多个技能之间确实有重叠,模型对选用哪一个产生困惑。比如"发送邮件草稿"和"保存邮件草稿"两个技能,描述里都可能包含"邮件""草稿""收件人"这些词。我最后的处理方式是给技能加互斥提示,在每个技能的description里加一行"本技能仅负责X,发送动作请使用send_mail技能,保存动作请使用save_draft技能"。模型在做技能选择时,看到这行互斥提示就不纠结了。
第三类是有些复杂任务确实需要多个技能串成一个工作流,而模型卡在"一个任务只能选一个技能"的思维里。这个问题的解法是把组合技能直接定义成一个带step列表的技能,内部展开成多个原子技能的调用序列,对外只给模型一个统一入口。模型不需要理解内部流程,只需要决定"要不要做这个任务"。
5.2 技能间的相互干扰
技能数量增多之后,出现了一个意料之外的问题:新技能上线,旧技能的调用突然不灵了。起初我以为是模型不稳定,后来统计了多次运行的结果,发现规律是固定的——新技能的描述如果和旧技能有相似词汇,旧技能的调用率就会下降。
这个现象本质上是一个注意力分配问题。模型在读取技能列表的时候,对每一个技能分配注意力资源,新增技能分走了一部分原本属于旧技能的注意力。明白了原理之后,我在每次新增技能时都会做一次全量技能列表的回归测试,重点检查与新增技能语义相近的旧技能是否受到影响。如果影响确实存在,我会在旧技能的description里补充一句"当用户意图是X时使用,不要与skill_new混淆",用显式消歧来对抗注意力稀释。
另一个干扰从另一个方向来,也就是技能的冗余。新功能加着加着,某个旧技能的功能事实上已经被新技能覆盖了,旧技能留在列表里没有任何价值,只会增加模型的选择负担。我养成了一个检查习惯,每个月梳理一次技能调用日志,找出长时间零调用的技能,评估是删除还是合并,再顺手把"功能被覆盖但我还没删干净"的技能标记成废弃状态。
5.3 测出过一组让我后怕的技能
在技能系统基本稳定之后,我做过一轮更"刁钻"的测试,专门构造一些语义绕着走的问题来试探技能调用的鲁棒性。有个case让我印象特别深:我让Agent处理"帮我挪一下明天的会"这个请求,结果它调用了"创建日程"技能而不是"修改日程"技能。表面上看都是日程操作,但本质上一个新建一个变更,动作完全不同。用户说"挪一下"隐含的是日程已存在需要改时间,模型没有识别出这个前提条件,直接走错了方向。
这个case让我回头调整了日程类技能的定义,在"创建日程"的trigger_conditions里加上了"仅当用户在创建一场新的日程时调用,如果用户提到修改、取消、调整已有日程,属于日程变更,请使用update_event技能"。加了这种边界条件之后,再跑这类口径刁钻的测试,行为就稳定了。
还有一个让我比较意外的case是用户问"帮我整理一下收藏夹里的技术文章"。按我的设计意图,正确路径应该是先读收藏夹、再对文章做分类、最后生成一份整理报告。但模型直接调用了"技术文章搜索"技能,它把"整理收藏夹"理解成了"搜索更多技术文章"。这类意图理解偏差靠修技能描述解决不了,需要在任务规划层增加一步意图预判:先判断请求是否涉及已有资源,再判断是否需要外部信息。现在我的所有技能统一标注是否涉及存量数据操作,模型在处理请求时先走这个判断,再进入技能调用环节,这个case就没再出现过。
6. 后续可以往哪个方向扩展
6.1 低频技能如何低成本维护
技能系统跑了一段时间后,会出现一个尴尬的局面:有些技能非常高频,每天被调用成百上千次,被模型使用得很娴熟,很少出错。但另一些低频技能,一个月只被调用几十次,却占了技能列表里将近一半的描述空间,维护成本还很高。低频并不意味着不重要,比如"导出年度报表"这个技能,一个月就用七八次,但关键客户提出来的时候,一次做错就是事故。
我现在的处理思路是用"按需加载"把低频技能和高频技能分到两个池子。高频池里的技能全量挂载,保证模型随取随用。低频池里的技能只保留一句话摘要,摘要写清楚"这个技能管什么、典型的请求长什么样",模型判断需要时再展开完整定义。这样既不丢功能,又省下大批量占用的上下文空间。
6.2 技能评测自动化
人工回归测试在技能数量少的时候还行,技能一多,每次变更都靠人工验证不现实。我现在给技能库加了一套半自动化的评测流程。每个技能定义里挂一组evaluation_cases,每个case包含一个用户输入和一个期望调用的技能ID。跑评测的时候,把这批case灌给Agent,统计技能选择命、参数填充正确率、执行成功率、结果格式合规率等指标。技能定义变更后,先跑一遍全量评测,指标没回落的才允许合并上线。
评测case的构造也不难,直接从历史真实请求里挖,挑那些代表性的、容易引发歧义的输入存下来,慢慢积累一套技能评测集。这个评测集就是技能系统的质量护城河,比任何线上监控都更能防止"改一个技能,坏一片功能"的回归问题。
6.3 技能的可演进性
技能系统还有一个被低估的维度:它在用户使用过程中产生的数据本身,其实是一个新技能的原料仓库。我目前积累了大量"用户用什么说法触发什么技能"的真实日志,这些数据后续可以用来做技能描述自动优化,甚至让模型自己"提议"新的技能拆分方案。
我自己目前还没跑完整条链路,但经验已经明确了一个方向:技能系统不应该是一个静态配置,它应该和业务一起生长。用户提出新需求,先看现有技能能不能组合出方案,如果组合不出,才考虑新增技能,而且新增技能的定义要尽可能复用已有的原子能力。保持技能库精简而强健,比让它大而全可靠得多。