从零搭建一套Agent技能库:我踩过的坑和沉淀下来的工程方案
一直想谈谈技能库这个话题。最近半年我一直在折腾Agent类项目,从最早的单个Prompt驱动,到后面接函数调用,再到现在直接上技能库这套玩法。回头去看,真正让我把项目稳定落地、开发效率翻倍的转折点,就是引入了类似agent-skills这样的技能管理机制。这东西解决的核心问题非常具体:当你的Agent要处理的事情变多、要调的外部系统变杂,你不能把所有逻辑都塞进一个巨大的系统提示词里,而是要把它拆成一个个可以独立声明、独立加载、独立执行的技能单元。
所谓技能,你可以理解为Agent的“可插拔能力包”。每个技能定义一个名字、一段描述、一份参数规范、一套执行逻辑,Agent拿到任务后根据描述自己去匹配、去调用。听起来不复杂,但真正把技能设计好、管理好、让模型稳定选中正确的技能,里面全是在生产环境里才会暴露出来的细节。这篇文章不打算聊那些夸夸其谈的概念,我想把我自己实际搭建、反复调优、最后跑通的一整套方案完整拆给你看,包括目录怎么规划、描述怎么写、参数怎么约束、加载失败怎么办,以及那些不同文档里很少明确告诉你的坑。
这套方案适合谁?适合已经用过LangChain或自建过简单Agent、现在想让系统支撑更多任务场景的开发者,也适合正在为团队设计一套可复用的Agent能力注册中心的后端工程师。我不保证每一条经验都放诸四海皆准,但它们是真实跑过生产流量、被逼到墙角才总结出来的东西。
1. 整体设计与拆解:技能库到底在解决什么问题
1.1 技能和工具、插件的本质区别
很多人第一天接触技能库时会问:这不就是函数列表吗?跟之前给模型传tools有什么区别?
表面上看确实像,但有一个非常关键的差异。函数调用时代,你给模型传的是一份JSON数组,里面定义了函数名、描述、参数,模型根据用户输入决定调用哪个函数。这种做法在函数数量少的时候完全没问题,可一旦函数超过20个、30个,你会发现模型开始频繁选错、漏选,甚至把参数都填错位。原因很简单:上下文里塞了太多函数定义,模型对每个函数的注意力被稀释了。
技能库做了一件事——把“模型看得见的描述”和“可执行的核心逻辑”分开管理。技能库内存的不是简单的函数列表,而是一套带元信息、带依赖、带加载机制的完整技能单元。Agent在启动时不会把全部技能一股脑塞进上下文,而是先看用户需求,经过一层意图路由,只把可能相关的十几个技能的定义和说明注入上下文。
还有一点容易被忽略:工具是“给模型用的函数”,技能是“给Agent用的能力包”。能力包意味着它内部可以包含多个步骤、多轮外部调用、失败重试逻辑,而不仅仅是一个纯函数。我在做技能库时最大的感受就是,这套设计逼着你把能力边界先想清楚,然后再写代码。
1.2 没有技能库时,Agent开发会碰到的三个典型痛点
我最早做一个跨平台内容收集Agent时,系统提示词写了好几千字,从网页解析到API调用全堆在里面。表面上看起来“很有智能”,实际用起来痛苦不堪。
第一个痛点是上下文膨胀。凡是跟任务相关的东西都堆在系统提示词里,用户每次对话都要重复消费这些Token,成本是一个问题,更重要的是模型注意力被稀释——它记住了细节,却失去了对大方向判断的能力。
第二个痛点是逻辑耦合。我想新增一个“查询天气并推荐出行方案”的技能,得去改系统提示词,然后重新定义函数,再处理跟已有逻辑的边界关系。改了一处,往往牵动七八处,测试一次要跑十来个回归场景。
第三个痛点是无法复用。同一个爬虫技能、同一个数据库查询技能,在那个项目里写得死死的,换一个项目就得复制粘贴再改一遍。模块化意识稍微差一点,代码就成了意大利面。
我没法一一说明我当时经历的狼狈,但技能库这套方案确实从根上把这三个问题解决掉了:上下文只放进当前任务相关的东西,新增技能不影响已有技能,技能之间按标准接口互相独立,换项目直接搬过去就能用。
1.3 技能库适合什么样的业务场景
从我的实践来看,技能库不是所有Agent项目的必选项。如果你的Agent只做一件事,比如只做SQL生成,或者只做固定的客服问答,那直接用单Prompt方案就够了。但一旦你的Agent表面上是一个入口、背后要接七八个系统、覆盖四五个业务领域的任务,技能库就是刚需。
举个例子,我后来接手一个数据处理Demo,需求是用户可以用自然语言提问,底层要支持数据查询、报表生成、异常告警、数据对比、定时任务,甚至还要能调用一些外部服务。这五个领域的能力逻辑完全不同,如果全部塞进一个Prompt,模型八成会疯掉。拆成技能库之后,每个领域就是一组技能,数据查询技能内部走SQL生成链路,报表技能走模板渲染链路,两个技能没有直接依赖关系,Agent只要负责决策哪个技能能解决当前用户诉求。
判断标准我一般用一条:是否同时满足“任务类型多样”和“调用方式异构”。两条都满足,就值得上技能库。只满足一条,可以再等等。
2. 技能分类体系与技能元信息设计
2.1 技能怎么分:先按领域切,再按动作切
技能分类这件事,做得好不好直接决定了Agent后续路由的准确率。我一开始犯的错误是只按功能划分,什么“网页搜索技能”“数据库技能”,结果发现Agent经常在同一个功能类下选错具体技能,比如用户说“帮我看看上个月的数据”,它分不清该走“数据彩信”还是走“数据表格导出”。
后来我改成“领域优先、动作次之”的两层切分法。第一层按业务领域分,比如数据域、内容域、通知域、系统运维域。第二层在领域内按用户动作意图来分,数据域下面会有“获取概览数据”“获取明细数据”“生成图表”“导出文件”这几个技能。这样分类带来的好处是,领域分类决定了Agent的意图路由走向,动作分类决定了具体技能的绑定关系。两层加起来,Agent选错的概率明显下降。
同时建议加上技能能力等级。就是给每个技能标一个复杂度级别,从一级到三级,一级技能是只有一个函数调用的“原子型技能”,二级技能是内部包含两步逻辑的“组合型技能”,三级技能是包含分支、循环、可能需要调用多个外部系统的“流程型技能”。有了这个等级,Agent在做复杂任务时就会优先组合低级技能,而不是一股脑去选那个最重的大技能。
2.2 技能元信息的核心字段:不能只写名字和描述
我在技能库里给每个技能设计了一份元信息清单,你可以理解为技能的“身份证”。这份清单长这样:
- 技能ID:全局唯一,命名用领域前缀加动作名,比如
data_get_detail。 - 技能名称:中文短名,方便调试时人看。
- 功能描述:这是最关键的一段,决定模型什么时候选它。
- 参数Schema:定义调用参数和格式,按JSON Schema规范。
- 执行类型:是本进程内执行,还是调用外部API,还是需要先拉起子进程。
- 依赖技能:执行时可能需要先调用其他技能,写明依赖链。
- 超时设置:每个技能单独指定最大执行时间。
- 权限标识:这个技能是否涉及敏感操作。
- 启用状态:开关,方便临时下线某个技能。
这里面最容易被忽视的是“依赖技能”和“权限标识”。依赖技能不写清楚的后果是,Agent调度技能时没法做链式规划,明明要先查库存再下单,它却直接调了下单技能,然后报错。权限标识不写清楚的后果更严重,一个只读类的查询技能可以接线到已销毁的资源,或者执行了不该执行的操作,出事之后排查半天找不到根因。
2.3 技能目录的工程化组织方式
技能库的目录结构长什么样,直接决定了团队协作和后续扩展的顺畅度。我目前用得最顺手的结构是这样的:
skills/ data/ 获取概览数据/ SKILL.md schema.json handler.py 获取明细数据/ SKILL.md schema.json handler.py notify/ 发送站内信/ SKILL.md schema.json handler.py每个技能目录下固定三件套:SKILL.md(模型看的说明)、schema.json(参数约束)、handler.py(执行逻辑)。这种“说明书、约束、执行”三分离的设计,好处有三点。第一,SKILL.md面向大模型优化,writer可以专门静下心打磨提示词;第二,schema.json面向校验和前端表单生成,调参的人不需要读懂执行代码;第三,handler.py面向程序员,改逻辑不影响另外两个文件。
我试过把元信息直接写在代码文件里,比如用装饰器定义技能属性。短期看很爽,代码和定义在一起,但后期一旦技能数量超过三十个,搜索、索引、批量修改都会变得非常痛苦。文件分离带来的代价是每个技能多两个小文件,但收益是管理层面的有序性,这笔账怎么算都划算。
3. 从零搭建一套可用的技能库:核心实操过程
3.1 第一步:定义技能描述,这一步决定模型能不能找到你
我故意把技能描述放在第一步,因为太多人一上来就写handler实现,等写完了回过来补描述,结果描述写得一塌糊涂。技能描述是给模型看的,不是给文档系统看的。它不需要像技术文档那样严谨,反而要像“写给一个聪明但没常识的实习生”那样,把适用场景、边界条件、常用说明用大白话交代清楚。
举个例子,数据查询领域里有一个“获取概览数据”的技能。第一版描述我写的是:“查询数据汇总信息,返回关键指标。” 这个词太短了,模型根本不知道什么时候该用。后来我改成这样:
当用户想看整体情况或者汇总数据时,使用本技能。适用但不仅限于这些表达: - “看一下整体数据” - “本月大概什么情况” - “核心指标有没有变化” 返回结果为汇总数值和环比变化。注意:如果用户想看逐条明细,不要使用本技能,请使用“获取明细数据”技能。 只要用户没有明确要求原始明细,都优先使用本技能。这段描述里写了触发词、写了对立场景、写了优先级规则。实测下来,因为后面加了“如果没有明确要求明细就优先用这个”,模型选择这个技能的比例一下子提升了二十多个百分点。
关于描述还有一个非常重要的原则:写清“不要做什么”。“不要去做XX”“如果涉及XX请换另一个技能”,这类负向约束比正向描述更能帮模型划清边界。因为大模型在上下文里做匹配时,天然倾向于寻找最接近的表达,如果你不主动划清边界,它就会把模糊的请求往它看到的第一个相似技能上靠。
3.2 第二步:参数Schema一定要严格,宽松就是给自己挖坑
技能参数Schema不仅约束了外部的调用方,更关键的是约束了Agent这个“不靠谱的调用方”。模型虽然能理解自然语言,但它生成的参数经常出现字段名拼写错误、数值类型对不上、必填参数漏掉这些问题。
我实践下来的参数Schema最佳实践是:用JSON Schema规范定义类型,给每个字段写中文描述,把所有枚举值明确写全,能不用自由文本就不用自由文本。比如一个技能叫“发送通知”,它的参数就不能是{“content”: “任何文本”},而要拆成title、body、msg_type,其中msg_type必须用枚举限定为email、sms、inbox。
类型校验要放在Agent调用之前做一次,执行之后再做一次结果校验。前者防止模型传错参数,后者防止执行逻辑返回了不符合预期的数据。我在实际项目里见过参数校验缺失导致的一系列事故:模型把一个时间字符串传成了整数,后端直接崩;模型把用户ID和订单ID搞反了,结果查出来的数据完全是另一个人的。严格校验能挡住绝大多数这种问题,你的技能库越早建校验层,后面返工越少。
3.3 第三步:技能加载与调用的工程实现
技能描述写好、目录建好之后,接下来要做的是让Agent在运行时能“看见”这些技能。加载流程我分成两个阶段:注册期和运行期。
注册期是在服务启动时,扫描整个技能目录,解析每个技能的metadata,构建成一张技能索引表,存到内存或者Redis里都可以。这张表的作用是给意图路由模块用的,快速根据用户问题找到候选技能ID列表。
运行期是当用户请求到达时,先经过意图路由,找到候选技能,再把候选技能的SKILL.md内容注入到给模型的上下文里。模型根据注入的技能说明和用户问题,选择具体技能并填充参数,最后执行handler,返回结果。这个过程里有一个细节要注意:注入给模型的内容一定要做截断和筛选。我的一开始实现是直接把全库技能全注入,结果上下文爆炸,后面改成top-k召回机制,只注入相关度最高的五个技能,效果立竿见影。
召回机制我用的是关键词加语义双通道。关键词通道做精确匹配,比如用户提到“邮件”“发送”,直接锁定通知域的发送技能;语义通道用向量检索,适合那些描述比较模糊的请求。两个通道的结果做加权融合,再丢给模型决策。这套召回方案实测稳定,也没有复杂到需要单独部署一套向量数据库的程度。
3.4 第四步:技能的执行环境隔离与依赖管理
技能不是只读的说明书,它的handler是真正要跑代码的。代码执行带来的第一个问题是环境隔离。不同的技能可能需要不同的Python包、不同的系统依赖,甚至不同版本的同一个包,硬塞在同一个进程里迟早会出事。
我现在的方式是:基础技能直接跑在主进程里,比如简单的字符串处理、数据格式化、文本生成;重量级技能通过子进程调用方式运行,给每个技能指定一个独立的Python虚拟环境或者容器。子进程执行的好处是资源回收干净,就算某个技能跑崩了、内存泄漏了,也不会拖垮整个Agent服务。
依赖管理的粒度按目录记,每个技能目录下有一个requirements.txt。有环境冲突的技能就单独做隔离运行,没有冲突的技能可以共用环境。这个判断标准是:开发期图方便可以共用环境,生产环境至少保证高风险的技能独立运行。
关于并发,也要在设计期就想清楚。技能内部如果有外部API调用,一定要设置超时,建议默认十五秒。如果不设置超时,一个下游服务抖动,整个Agent都会卡住,用户会觉得这个机器人“傻了”。我做了一个全局的并发控制,限制同一个技能同时最多执行N个实例,超出就排队,避免下游API被冲垮。
4. 实操中的关键细节与参数调优记录
4.1 技能描述怎么写,模型才会稳定命中
这是整个技能库里最“玄学”但又最值钱的环节。我前后反复打磨了三个版本才找到一个相对稳定的写法。
版本一的问题是只有功能说明,没有触发场景。版本二加了触发场景,但举例太少,模型还是会漏。版本三我把描述拆成三块:第一块说清楚“这个技能是什么”,第二块列出“用户通常会怎么说来触发这个技能”,第三块写清楚“不要用这个技能的情况”。区块之间用分隔线隔开,模型在理解结构化文本时表现比连续散文好很多。
另一个我提炼出来的技巧是:描述中的动作词要跟用户口语对齐。比如用户很少说“查询”,更多说“看看”“帮我找一下”“有没有”。如果你的描述里只有“查询”,模型可能匹配不上。我建议把口语化同义词作为触发词直接写在描述里。
再一个心得是:每过一段时间要回头审视描述。模型版本升级后,它可能对同样的描述产生不同的理解倾向。我在实际操作中养成了一个习惯——每次切换模型版本,都会用一批历史真实用户问题做回归测试,定位那些描述失效的技能,然后针对性调整。这事不能省。
4.2 参数的枚举、默认值与错误提示设计
参数Schema里,我强烈建议给每个字段设计合理的默认值,只有当用户明确表达特定意图时才覆盖默认值。为什么要这样?因为Agent在用户没有提供完整参数时有两种选择:瞎猜一个,或者反问用户。瞎猜容易出错,反问用户则经常让用户觉得机器人“不够聪明”,因为很多信息其实是可以合理推导的。
比如“生成周报”这个技能,参数有start_date、end_date、template_id。用户如果说“生成上周周报”,那template_id直接给默认模板就行,没必要反问用户。如果用户说“用新的模板”,那才需要额外传模板参数。我在Schema里给每个字段标记了need_ask属性,值为假就默认用默认值,值为真才触发Agent反问。
关于错误提示,我也走了一些弯路。一开始错误提示是给程序员看的,一堆堆栈和错误码,结果用户反馈“机器人报错了,但不知道错在哪”。后来我把错误提示分成两层:对用户的口语化提示和对调试者的详细日志。用户层提示要包含“能做什么补救措施”,比如“抱歉,查询超时了,你可以稍后再试,或者缩小一下时间范围”,而不是直接说“报错”。
4.3 技能注册中心的动态开关与灰度发布
技能库一定要支持动态启停,不能每次改技能都要重启服务。我实现了一个技能注册中心,里面有一个简单的管理接口,可以调整技能的enabled状态和路由权重。当一个技能出现问题时,可以一键下线;当一个新技能上线时,可以先只对白名单用户开放,验证没问题了再全量。
路由权重也是一个实用功能。比如同一场景下有两个技能都能实现类似效果,其中一个更稳,那就在注册中心把它权重调到0.9,另一个应急方案保留权重0.1。Agent在遇到模糊匹配时,会优先选权重高的技能。这套机制让我在后期的运营中可以“用脚投票”,不用频繁改代码就能调整Agent行为。
灰度发布的流程一般是这样:先在线下按技能维度做回归测试,通过之后在线上给内部账号放量,观察日志里的命中率和成功率,确认稳定后逐步放开。如果发现某类对话频繁触发了一个不是最优的技能,就回来调整描述和权重,再验证一轮。
4.4 技能的失败重试与降级策略
技能执行失败是在所难免的。网络抖动、下游限流、数据源变更,任何一个外部因素都可能让技能执行中断。关键是失败之后怎么办。
我的方案是给每个技能定义三种失败处理策略。第一种是直接重试,适用于超时类错误,最多重试两次,每次退避三秒。第二种是降级到备用方案,比如“实时查询技能”失败后,降级到“缓存查询技能”,虽然数据可能不是最新的,但至少用户能拿到一个结果。第三种是明确报错并引导用户,适用于那些无降级路径的技能。
在选择策略时,要特别注意区分错误类型。参数错误、权限错误这类是确定性错误,重试一万次也没用,直接返回错误提示;超时、限流这类瞬时性错误,重试才有意义。我以前见过一个团队把所有错误都做重试,结果一个权限错误整整重试了五次,Agent响应时间直接爆炸,用户早就走了。
还有一个经验:技能内部如果涉及多步骤,要在每一步落日志。哪一步失败了一眼就看出来,不然一个组合技能挂了,调试的人要把所有日志拼起来才能猜出是哪个环节的问题,效率低到让人怀疑人生。
5. 常见问题与排查技巧实录
5.1 Agent频繁调用错误技能,怎么定位和修复
这是技能库上线的第一天就会遇到的高频问题。用户明明问的是A,Agent却选了B技能。遇到这种情况,我一般按三步排查。
第一步,把用户原话和Agent实际选择的技能日志捞出来,人工对比。看是不是技能描述本身没有包含对应意图的触发词。如果是,直接补触发词和同义词。
第二步,检查是不是候选召回环节出了问题。如果候选列表里根本没包含正确技能,那改进召回逻辑;如果候选列表里包含正确技能但Agent没选,那就是描述排序或权重的问题。
第三步,看是不是多个技能描述过于相似。如果两个技能都能处理同一类请求,模型没法区分,我给它们的描述分别加上“优先处理精确明细”“优先处理汇总数据”这样的边界词,并用具体的反面例子划清边界。
定位之后的重测很关键。我推荐维护一个回归问题集,每一轮技能库变更都把历史典型问题跑一遍。这能拦截大多数“改好一个、弄坏两个”的回归事故。
5.2 技能加载路径或依赖解析失败的处理
技能目录扫描或依赖安装出问题,通常表现为服务启动时技能缺失或运行时报找不到模块。常见原因有三个:路径写死、依赖冲突、命名不规范。
路径问题最蠢也最常见。我见过有人把技能库路径写死在代码里,部署到另一台机器后路径不对,整个技能库加载失败。后面统一改成相对项目根目录的解析方式,或者通过环境变量注入,才把这个隐患解决掉。
依赖冲突出现时,优先做隔离运行。比如技能A依赖某库2.x版本,技能B依赖1.x版本,就把其中一个技能切到独立的虚拟环境。另一个技巧是:技能目录下的requirements.txt要尽量锁版本,不要用“>=”这种宽松约束,否则一次环境重建就会引入未知版本,技能行为变得不可预期。
5.3 技能执行结果被模型误判或截断
有时候技能执行是成功的,返回的数据也完整,但Agent在总结给用户时把关键信息漏掉了,或者把结构化数据理解错了。这个问题不在技能库本身,而在“执行结果如何反馈给模型”这一段。
我的经验是:技能执行结果返回给模型之前,一定要做一次数据精简和结构化。如果数据量很大,直接把原始JSON塞给模型,模型不仅会消耗大量Token,还会因为信息过载而抓不住重点。我先用一个小的summarizer函数,把核心结论、关键数字、异常点单独提取出来,再拼装成一段简短的Markdown给模型作为“执行观察”。这样模型再忙也不至于把最重要的数字漏掉。
这个summarizer可以是一个简单的规则函数,也可以是一次调用轻量模型的快速总结。实测下来,前者的稳定性和速度更好,因为它是确定性的,不会因为模型状态不同而变化输出结构。
5.4 安全边界:不能用技能放开所有能力
最后这个问题我必须提醒。技能库如果设计得太宽松,Agent就变成一个“什么都能干”的超级系统,一旦用户输入被恶意构造,可能引发严重问题。
首先,涉及敏感操作的技能(删除数据、发送消息、修改配置),必须有独立的权限校验流程。不能只靠模型判断是否调用,还要在handler入口做二次校验。校验依据可以是对会话级别的权限标签做匹配。
其次,所有技能的描述都应当避免“直接执行用户提供的任何指令”这种话术。技能描述给Agent看的,Agent又容易被用户话术带偏。我在描述里明确写出了“如果用户的指令与技能定义不符,不能强制使用”“遇到冲突时优先触发二次确认”。
再次,技能的输出做内容过滤。尤其是通知类、对外发布类技能,在真正发送之前要做一遍敏感词扫描和人工确认开关。这里不展开具体规则,但方向必须坚持:能力越大,越要有闸门。
结尾:一点补充的小经验
技能库这套模式我一直在持续迭代,其实最深的感受是:它不是一个一次性建完就能一劳永逸的东西,而是一套需要持续运营的机制。我自己会在每次项目迭代后重新审视一遍已经沉淀的技能,看看哪些描述已经跟不上用户的实际表达习惯,哪些技能调用频率很低可以降级合并,哪些技能的参数约束太松需要收紧。
如果你的技能库还处于初建阶段,我建议先挑两三个最关键的能力做试点,把从描述编写到执行回流的整条链路跑通,再逐步扩量。不要一开始就追求几十个技能全上,那样排查问题会让你觉得系统到处都在漏风。另外,每次上线新技能都记得留一份基准测试集,哪怕只有三十条真实问题,也能在后续迭代中帮你快速发现“这次改动到底改好了没有”。这个习惯比任何框架选择都重要。
以上内容基于我个人的实际项目经验整理,具体方案请结合你的业务场景裁剪。希望这些踩坑记录能让你少走几步弯路。