SkillNet 这个方向,解决的是 AI Agent 在技能变多之后不知道怎么管的问题。很多刚接触 Agent 开发的人,会以为只要把功能函数写出来,再放进系统提示词里告诉模型“你可以调用这些”,就够了。但等技能数量超过十个,甚至几十个,模型会开始选错工具、参数频繁传错、多个技能之间配合不起来。SkillNet 的思路,就是把技能当成一套可注册、可检索、可编排、可调用的网络资产,让 Agent 在面对任务时不是靠运气选工具,而是有一条确定的链路:注册、检索、组合、调用。
这里先说明:SkillNet 这个名字来自 EvoAgentX 系列讨论里的技术方向,它不是某一家独有的封闭系统,下面很多内容是通用设计方法,可以直接套到自己项目里。我会按实际落地顺序,先讲它解决什么问题,再拆注册和检索,然后讲组合编排,最后给参数和排查经验。
1. 先理解 SkillNet:解决的是“技能怎么管”的问题
1.1 Agent 技能管理的三个问题:发现、选择、组合
如果手上只有三个技能,查天气、查日历、发邮件,那不需要什么网络。直接在代码里写分支判断就行。但当技能变成三十个、三百个,Agent 会面临三个绕不开的问题。
第一个是发现。Agent 根本不知道系统里有哪些技能,或者知道存在,但不知道这个技能适不适合当前任务。比如用户说“帮我整理一下这个季度的销售数据”,如果系统里有一个数据分析技能,但它的描述写的是“执行 SQL 查询并返回结果集”,模型很可能没有意识到“整理销售数据”应该调用它。技能写好了,Agent 发现不了,等于没写。
第二个是选择。即便技能被发现,模型还要在多个相似技能之间做选择。比如系统同时有“通用网页搜索”“垂直新闻搜索”“本地文档搜索”,用户问“最近有什么新闻”,模型该调哪个?如果候选不区分场景,结果经常会选错。
第三个是组合。真实任务很少是一个技能能完成的。用户说“帮我查几个开源项目,并看看它们的 star 趋势”,这就涉及搜索、读取仓库信息、数据处理、生成总结多个环节。如果这些动作要程序员在代码里手动串联,那系统就不是 Agent,而是一个固定业务流。
SkillNet 的核心,就是把这三个问题统一处理。注册是为了解决发现,检索和路由是为了解决选择,编排是为了解决组合。三件事一起设计,才能叫“技能网络”,否则只是多了一个工具管理表。
1.2 和传统 Function Calling 的差异
传统函数调用大家已经很熟了。把函数定义传给模型,模型根据对话内容决定调哪个函数,然后返回结构化参数。这个思路本身没问题,也是很多 Agent 框架的底座。
但它有两个短板。
第一,函数列表是平铺的。技能少的时候没问题,技能多了之后,把所有函数定义塞进上下文,不仅 token 消耗高,模型还容易在相似函数之间混淆。
第二,它只解决单次调用。函数调用回答的是“这次该调用哪个”,但回答不了“这三个技能之间应该按什么顺序执行”“一个技能失败之后要不要换另一个”。真实多步任务需要编排,而编排需要先把技能组织起来。
SkillNet 的思路不是抛弃函数调用,而是在它上面加两层东西:一层是技能检索,先把候选范围缩小;一层是技能编排,把多个技能按照任务需求串成图。函数调用仍然存在,只是不再直接暴露给模型,而是被上层网络调度。
这也是为什么它适合 EvoAgentX 这类多智能体场景。在多 Agent 环境里,不同 Agent 需要不同技能集合,如果所有技能都全局平铺,管理成本会非常高。按需要给每个 Agent 挂载部分技能,再加上统一检索,才更可控。
1.3 什么阶段该引入 SkillNet 思路
不是所有项目都需要技能网络。判断标准很简单:技能少于 10 个,用户请求也比较固定,直接维护函数列表,甚至写一个简单的工具枚举,效率更高。不要为了架构而架构。
出现下面这些特征时,再考虑引入:
- 技能数量持续增长,每周都有新的工具函数要接入。
- 同一个用户问题经常需要多个技能配合完成。
- 不同角色、不同 Agent 使用不同的技能集合。
- 产品或运营希望在不改代码的情况下调整 Agent 能力。
如果你的项目命中两到三个特征,说明问题已经不只是“模型选哪个函数”,而是“技能资产怎么组织”。这时候再开始做注册表、检索和编排,才有实际收益。
过早引入技能网络,会变成额外负担。技能很少时,维护一套注册表、检索索引和编排配置,本身就是成本。技术选型要看场景阶段,而不是看概念热不热。
2. 技能注册与检索:让 Agent 先知道“你有什么能力”
2.1 技能注册表是第一步
很多人一上来就想做向量检索、做语义匹配,但真正动手后才发现,最基础的问题是你的技能根本没有结构化登记。技能分散在代码里、文档里,甚至只存在于某个同事的脑子里。这种情况做检索没有意义,因为检索的是索引,索引的前提是数据。
技能注册表,简单说就是一张清单,一个技能一条记录。每条记录至少要包含这些字段:
- skill_id:技能唯一标识。
- name:技能名称。
- description:技能描述,说明这个技能能干什么、适合什么任务。
- tags:标签,方便静态过滤。
- input_schema:输入参数定义。
- output_schema:输出结构定义。
- permission:权限要求。
- timeout:超时时间。
- enabled:是否启用。
这张表放在哪里不重要。本地 JSON 文件、SQLite、PostgreSQL、配置中心,都可以。初期从 JSON 或数据库表开始就行。
注册表的核心价值不是存储,而是让后续的检索、路由、编排有唯一数据源。AI Agent 每一次决定“要不要调用技能”“调用哪个技能”,都应该基于注册表,而不是基于散落在代码里的函数注释。
2.2 检索链路:静态过滤、向量召回、精排
注册表建好后,接下来是检索。技能检索不能只是“把用户的话和技能描述做相似度匹配”,要分几层。
第一层是静态过滤。先根据当前 Agent 的职责、用户权限、技能标签,排除一批明显不该出现的技能。比如当前 Agent 是只读分析型角色,那些写操作技能直接过滤掉。这一步能大幅缩小后续检索范围,而且几乎不出错。
第二层是语义召回。把用户当前请求转成向量,和所有技能的描述向量做相似度计算,取 Top N。相似度阈值不能一开始就定死,要看你用的 embedding 模型和文本表达。一般可以先取 Top 10 到 Top 20,后面再精排。
第三层是精排。候选已经缩小到十几个,可以让 LLM 从里面挑出最合适的 1 到 5 个,也可以写规则做最终排序。如果用户需求明显是搜索类,就优先搜索类技能;如果是信息抽取类,就优先文本处理类。
整个检索链路最终输出的是“候选技能列表”,这个列表才是给 Agent 看到的东西。不要一上来就把全部技能列表暴露给模型。
有一个容易被忽视的细节:静态过滤要在向量召回之前。过滤是精确判断,召回是模糊判断,先把确定的排除掉,再对剩余部分做模糊匹配,准确率和性能都会更好。反过来做的话,向量召回阶段会浪费很多计算在无关技能上。
2.3 技能描述怎么写,直接影响检索命中率
技能描述是检索系统最敏感的因素。两个技能功能接近,描述写得好,检索命中率可以差很多。
我一般会按这个格式写 description:该技能用于什么场景,输入是什么,输出是什么,适合处理哪些任务,不适合处理什么。
举个例子:
{ "skill_id": "order_query", "name": "订单查询", "description": "根据订单编号或用户 ID 查询订单状态、金额、物流信息。适合在用户询问订单进度、售后状态时调用。不适合查询历史销售统计报表。", "tags": ["order", "query", "ecommerce"], "input_schema": { "type": "object", "properties": { "order_id": {"type": "string"}, "user_id": {"type": "string"} }, "required": ["order_id"] }, "output_schema": { "type": "object", "properties": { "order_id": {"type": "string"}, "status": {"type": "string"}, "amount": {"type": "number"}, "logistics": {"type": "string"} } }, "timeout_seconds": 10, "permission": "read" }description 里写了“适合什么时候调用”和“不适合做什么”。这样检索和 LLM 精排时,能更好判断当前用户请求是否匹配。
每个技能还要写清楚输入输出参数,尤其是参数类型。如果 schema 只写{"type": "object"},模型和程序都没法组装参数。至少要标识 required 字段和每个字段的语义。
注意:技能描述不是给程序员看的注释,是给检索模型看的“商品标题”。它最重要的目的是让正确的技能在召回和精排时排到前面。
3. 技能编排:从单技能到多技能协作
3.1 先把单个技能调稳
这是我踩过最多坑的地方。很多人做技能网络,一上来就设计复杂编排图,结果连单个技能都没跑稳。最后排错的时候,根本分不清是技能坏了还是编排坏了。
所以我的建议非常直接:先不做编排,先把每个技能当成独立单元跑通。
怎么判断单技能跑稳了?看三点。
第一,参数能正确传入。不同调用方传参方式可能不一样,有的传 JSON,有的传表单,你的技能要能用统一方式接住。
第二,输出可解析。如果技能返回的是自由文本,后面对接会很难。尽量返回 JSON 结构,成功失败要有明确标识。
第三,失败要可读。技能异常时不能只是抛一个 “Error”,最好返回错误码和简要说明。这样编排层才能决定是重试、换技能,还是直接中断。
把单技能这条基础打牢,再讨论组合编排。这就好比你要先确认每一块积木是完好的,搭积木才谈得上有意义。
3.2 顺序、分支、并行三种编排方式
技能编排的常用方式有三种,实际项目里经常混用。
顺序编排最直观。技能 A 执行完后,把结果作为技能 B 的输入,一条线走到底。比如:调用搜索技能 → 调用网页读取技能 → 调用总结技能。这种模式最容易实现,也最稳定。第一版编排建议只做顺序编排。
分支编排支持根据条件走不同技能。比如查询订单状态,如果订单已经发货,调用物流查询技能;如果订单未付款,调用支付提醒技能。实现分支时,要注意条件判断来源于上一个技能的结构化输出。如果上一个技能输出是非结构化文本,分支判断会很不可靠。
并行编排是多个技能互不依赖,同时执行,最后合并结果。比如同时调用多个搜索源,等所有结果返回后做聚合。并行调用能显著提升速度,但也带来了三个新问题:并发控制、部分失败处理、结果合并顺序。第一次做编排时,不要急着开并行,先把顺序和分支跑熟。
很多框架会把编排定义成图结构,节点是技能,边是数据流。SkillNet 这个方向也一样,“可编排”说的就是这个图能灵活配置,而不是写死在代码里。
3.3 输入输出衔接:组合任务最容易踩的坑
组合任务里,大部分故障不是技能本身出问题,而是输入输出结构衔接不上。
常见的坑有三个。
第一个,类型不匹配。上一个技能输出是数组,下一个技能输入要求是字符串。没有任何转换,程序直接报错,或者模型编了一个奇怪的转换逻辑。
第二个,数据量过大。搜索技能返回了 50 条结果,全部传给下一个总结技能,token 一下子就爆了。有些项目就在这里频繁触发上下文限制。
第三个,字段命名不一致。上游输出result,下游 schema 期望data,匹配不上,模型开始猜字段,猜错了就出幻觉。
怎么解决?在编排节点之间增加一层“数据转换”。不要直接把一个技能的 output 原样塞给下一个技能。先做提炼:只保留必要字段,做类型转换,控制长度。这样才能保证链路稳定。
如果实在要做复杂场景,我建议给每个技能配一个轻量的“输出处理器”。例如搜索技能的输出处理器负责截断、排序、只保留标题和链接;总结技能的输入处理器负责接收这些精简字段。有人会觉得这点工作不值得做,但在密集组合调用时,这个设计能省掉很多无意义的 debug。
4. 实际调用过程:自动路由和显式编排怎么平衡
4.1 什么时候让 Agent 自己选技能
技能编排做到后面,会面临一个选择:流程是写死好,还是让 Agent 动态决定好?
我自己的经验是分任务类型。
对于高重复、结果确定性强的任务,用显式编排。比如“音频转写 = 上传音频 → 转写 → 分段 → 整理”。你不需要模型思考要不要先调用天气技能,直接按预定流程执行,速度更快,结果稳定。
对于开放式任务,让 Agent 动态选择。比如“帮我做一份行业调研报告”,模型需要根据当前进度决定下一步是搜索、是读文章还是总结。这个时候如果写死固定流程,Agent 就没有智能可言了。
SkillNet 的思路里,两种模式不是对立的。可以是固定编排图和动态生成的编排图共存。系统里有预定义模板,也有运行时让 LLM 根据任务生成的流程。判断标准就一条:如果这个任务每次路径都一样,就固化下来;如果路径经常变化,就交给 Agent 动态编排。
4.2 参数组装与结果回传
调用技能时,最容易忽略的是参数组装。
技能的输入不只是用户说的话。例如用户说“帮我查一下我在平台的订单”,调用订单查询技能时,你需要自动带上 user_id。user_id 来自哪里?来自登录状态、对话上下文,或者用户画像组件。
如果忽略这一步,技能就只能拿到用户原话,无法完成查询。所以调用前要有一个参数组装环节:把用户显式参数、Agent 运行状态、前置技能输出结果合并成一个完整的调用参数。
结果回传同样需要处理。回传前先做摘要或过滤。假设订单查询技能返回了 1000 行表格,不能全塞回给 LLM。要么用程序提取关键字段,要么用另一个技能做摘要,再把压缩后的结果交给主对话模型。
技能网络里的节点并不只是“调用一个函数”,它还包括数据的进入和离开。把输入输出控制好,整条链路的稳定性会明显提升。
4.3 调用失败和成功率低的排查方向
技能调用成功率不理想时,不要第一个就怀疑大模型选错了技能。按出现概率排序,我一般会看这么几个方向。
优先看技能描述与用户意图是否匹配。用户问“我的东西送到哪了”,你的技能描述如果只写“根据订单编号查询订单金额”,模型匹配不上很正常。这种问题改代码没用,改 description 和 tags 才有效。
其次看参数组装。很多调用失败不是技能不行,是参数没传对。比如 user_id 字段存在但进程里没取到,或者类型是字符串但 schema 定义成数字。到日志里看实际 payload,基本能确认。
再看候选技能数量。有些项目在提示词里塞了 20 个工具,模型经常把相似工具搞混。减少候选到 3 到 5 个,准确率通常会上升。
最后才是模型能力问题。如果前面几项都正常,还是频繁选错,可以换更大的模型或者加规则兜底。但大多数情况下,问题出在前三项,尤其是描述质量。
5. 落地环境、参数阈值与排查链路
5.1 需要哪些模块和环境准备
SkillNet 不是一个软件包,是一个架构方向。真要在项目里落地,需要准备这些模块。
技能注册模块负责保存技能定义。最简单的实现就是一个数据库表和读写接口。数据量不大,用 SQLite 足够,技能数量很多再做迁移。
检索模块负责找候选技能。如果技能少,可以用关键词和标签过滤;技能多、表达多样,再引入向量检索。向量检索需要 embedding 模型和向量存储。embedding 可以调用在线 API,也可以用本地模型。
执行模块真正去调用技能对应代码或远程接口。这一步建议和主 Agent 进程解耦。比如技能执行是独立服务,Agent 通过 HTTP 或消息队列调用。好处是单个技能超时或异常,不会拖垮整个 Agent。
编排模块维护技能的调用顺序、分支条件和状态。可以自己写一个简单流程引擎,也可以复用现有工作流引擎。第一版不需要图编排,顺序执行加条件判断就够。
日志和缓存模块容易被忽略。运行日志要记录每个技能从检索、选到执行、返回的完整链路。状态缓存用来保存多技能协作过程中的中间状态。
环境方面,单机开发学习用 8G 内存的机器就够跑注册表、检索和编排逻辑。本地部署开源模型时,建议 16G 以上内存,显存看模型体积。生产环境建议把技能执行做成独立服务,避免单个技能拖垮主进程。具体资源需求以你选型的模型和框架为准。
5.2 常用参数建议
进入调参阶段后,我会重点关注这些参数。
| 参数 | 建议初始值 | 调参方向 |
|---|---|---|
| 候选技能数 top_k | 3 到 5 | 候选越多,LLM 选错概率越高 |
| 相似度阈值 | 先不设,观察召回结果 | 召回结果不相关再逐步提高 |
| 外部技能超时 | 10 到 20 秒 | 外呼接口偏长,内部服务偏短 |
| 内部技能超时 | 3 到 5 秒 | 快速失败优于傻等 |
| 重试次数 | 1 到 2 次 | 只对网络类和外部接口重试 |
| 最大编排步数 | 5 到 10 | 防止动态编排进入死循环 |
| 单次输出长度上限 | 2000 字符左右 | 按下游节点承受能力调整 |
这些参数没有绝对标准。实际值取决于你的模型、技能复杂度和数据规模。我的习惯是先用默认值跑通,再基于日志调整。
排名靠前的参数是 top_k 和超时。top_k 太大,模型容易看花眼;超时太短,慢接口经常被误杀。先把这两个调稳,再看其他参数。
5.3 一套可复用的排查顺序
最后给一套排查链路,技能网络出问题的时候,按这个顺序看,比乱试快很多。
第一步先看注册。技能是否已经写入注册表,enabled 是否为 true。很多时候排了半天,结果是技能被误下线了。
第二步看检索。把用户输入和当前的候选技能列表打印出来,确认目标技能是否出现在候选里。如果没出现,问题在描述或检索阈值。
第三步看路由。模型或规则最终选中了哪个技能。如果选中了错误技能,说明精排阶段有问题,可能需要减少候选数量或优化描述。
第四步看参数组装。查看实际发出去的 payload,逐个字段检查类型和值。
第五步看执行。技能服务本身有没有正常返回,返回码是多少,耗时多少。这一步要看技能服务的独立日志,而不是只看 Agent 主日志。
第六步看结果回传。回传的数据有没有被截断,结构是否正常,能否被下一个节点解析。
每一步都能在日志里定位到对应节点。这也是为什么我强调日志要覆盖整个链路:没有链路日志,排查基本靠猜。
回到 SkillNet 本身。EvoAgentX 系列里讨论的技能网络,真正落地的价值不是把工具调用包装得花哨,而是让 Agent 在多技能、多步骤、多角色场景下仍然可控。做过一次就会发现,能不能跑通,首先取决于技能注册表和描述写得好不好;稳定不稳定,取决于参数组装、输出转换和错误处理完不完善。
如果只是学习,可以先从 10 个技能的小项目开始,把注册、检索、顺序编排跑通,再逐步做分支、并行和动态生成编排。等你踩过一轮日志、检索、参数类型的坑,再回头看,很多“Agent 选错技能”的问题,本质上都是工程问题。