1. 从“技能堆积”到“按需调用”的认知转变
手里攒了上百个 Skill,这大概是每个深度折腾 Agent 的人都会经历的阶段。一开始特别兴奋,每看到一个有意思的能力就忍不住封装成 Skill 塞进去——查天气的、读文件的、调接口的、做格式转换的、跑数据清洗的,恨不得把能想到的场景全给覆盖了。结果呢?Agent 跑起来要么是“选择困难”,要么是“张冠李戴”,明明该调用 A 技能,它偏偏去碰 B 技能,甚至把几个不相干的 Skill 串在一起执行,输出一堆莫名其妙的结果。
这个问题的根源,其实不在 Skill 本身的质量,而在于加载机制。大多数人搭建 Agent 时,默认把所有 Skill 一股脑全塞进上下文,觉得“给得越多,能力越强”。但实际跑下来你会发现,Agent 的决策空间被严重稀释了——就像你给一个刚入职的同事递了一百本操作手册,告诉他“都在里面了,你自己翻”,他大概率会懵。真正有效的做法,是分层加载:让 Agent 先知道“有哪些类别的能力”,再在需要的时候逐层下钻到具体的 Skill 定义。
这篇文章就是围绕这个核心问题展开的。我会从整体设计思路、分层架构的拆解、具体实现步骤、以及实际踩过的坑四个维度,把多 Skill 场景下的分层加载机制讲透。适合已经上手过 Agent 搭建、手里有一定数量 Skill 积累、但苦于调用准确率上不去的朋友。如果你还在“一个 Skill 打天下”的阶段,也可以提前了解这套机制,后续扩展时少走弯路。
2. 多 Skill 场景下的核心矛盾与设计思路
2.1 为什么“全量加载”必然导致调用混乱
先把这个问题的本质说清楚。Agent 在决定调用哪个 Skill 时,依赖的是上下文中的描述信息。当你把上百个 Skill 的描述全部塞进系统提示或工具列表里,会出现三个连锁反应。
第一,注意力稀释。模型的注意力机制是有容量上限的,当候选选项从 5 个变成 100 个,每个选项分到的“关注权重”就会急剧下降。原本描述清晰的 Skill 可能因为被大量相似描述淹没,导致模型无法准确区分。
第二,语义重叠干扰。你攒的 Skill 里一定有功能相近的,比如“读取本地文件”和“解析文档内容”这两个 Skill,在语义上高度相关。全量加载时,模型很容易在两者之间反复横跳,甚至同时调用,造成重复执行。
第三,Token 成本失控。每个 Skill 的描述、参数定义、示例说明加起来少说几十个 Token,上百个 Skill 就是几千甚至上万 Token 的固定开销。这不仅推高了每次调用的成本,还挤占了本该留给实际任务处理的上下文空间。
我做过一个粗略的对比测试:同样是 80 个 Skill 的场景,全量加载时首轮调用准确率大概在 60% 出头,而采用分层加载后,首轮准确率能稳定在 85% 以上。这个差距在复杂任务链中会被进一步放大。
2.2 分层加载的核心逻辑:从“扁平列表”到“树状索引”
分层加载的思路其实不复杂,用一句话概括就是:把扁平的 Skill 列表改造成树状的索引结构,让 Agent 逐层检索,而不是一次性面对全部选项。
具体来说,我把它分成三层:
- 第一层:领域分类层。这一层只告诉 Agent “你有哪些大类的技能”,比如“文件操作类”“网络请求类”“数据处理类”“格式转换类”。每个类别只给一个简短的名称和一句话说明,不展开具体 Skill。
- 第二层:技能概览层。当 Agent 判断当前任务属于某个领域后,再加载该领域下的 Skill 列表,每个 Skill 只给名称和一句话功能描述,不包含详细参数。
- 第三层:完整定义层。确定要调用某个具体 Skill 后,才加载它的完整定义,包括参数说明、调用示例、返回值格式、异常处理等。
这个结构和人类查字典的逻辑是一样的:先翻目录找到大致范围,再定位到具体词条,最后才看详细释义。没有人会从第一页开始逐字读完整本字典。
2.3 方案选型:为什么不用“向量检索”替代分层
看到这里你可能会问:为什么不直接用向量检索?把用户输入做 Embedding,然后跟所有 Skill 的描述做相似度匹配,取 Top-K 不就行了?
我一开始也是这么想的,实际跑下来发现几个问题。向量检索在 Skill 数量少、描述差异大的时候效果不错,但当 Skill 数量上去之后,相似度分数的区分度会明显下降。比如“读取 CSV 文件”和“读取 Excel 文件”这两个 Skill,向量相似度可能都在 0.9 以上,模型很难仅凭分数判断该选哪个。
更关键的是,向量检索是单层扁平的,它无法表达“先判断领域,再选择具体技能”这种层级决策逻辑。而分层加载天然支持这种递进式推理,Agent 可以先确认“这是一个数据处理任务”,再在数据处理领域内做细粒度选择,决策路径更清晰,可解释性也更强。
当然,分层加载也不是没有代价。它需要多轮交互,延迟会比单次检索高一些。但在准确率优先的场景下,这个代价是值得的。而且我后面会讲到,通过合理的缓存策略,可以把额外延迟压到几乎无感。
3. 分层加载机制的详细拆解与实现要点
3.1 领域分类层的设计:粒度把控是关键
领域分类层是整个分层机制的入口,它的设计质量直接决定了后续检索的效率。这里最容易踩的坑是分类粒度过细或过粗。
粒度过细,比如把“文件操作”拆成“文件读取”“文件写入”“文件删除”“文件重命名”四个类别,那这一层就失去了“概览”的意义,Agent 还是要面对大量选项。粒度过粗,比如只分“输入类”和“输出类”,又会导致每个类别下挂载太多 Skill,第二层依然拥挤。
我的经验是:领域分类控制在 5 到 9 个之间,每个类别下的 Skill 数量尽量不超过 20 个。这个数字不是拍脑袋来的,而是基于两个考虑。一是人类工作记忆的容量大约就是 7±2 个组块,模型在处理分类决策时也呈现类似的规律;二是当每个类别下的 Skill 超过 20 个时,第二层的选择准确率会开始明显下降。
具体分类方式可以根据你的 Skill 构成来定。我目前用的是这套分类:
| 类别名称 | 包含的 Skill 类型 | 典型数量 |
|---|---|---|
| 文件与存储 | 本地文件读写、目录操作、格式解析 | 15-20 |
| 网络与接口 | HTTP 请求、API 调用、数据抓取 | 10-15 |
| 数据处理 | 清洗、转换、聚合、统计 | 15-20 |
| 文本与内容 | 摘要、翻译、改写、关键词提取 | 10-15 |
| 系统与工具 | 命令执行、环境检测、日志记录 | 8-12 |
| 多媒体 | 图片处理、音频转写、视频截帧 | 8-12 |
每个类别的描述我控制在 20 字以内,只说明“这个类别能做什么”,不列举具体 Skill。比如“文件与存储”的描述就是:“处理本地文件的读取、写入、格式解析和目录管理。”
注意:领域分类层不要放任何具体 Skill 的名称。一旦放了,Agent 就会跳过第二层直接去猜,分层就形同虚设了。
3.2 技能概览层的组织:一句话描述怎么写才有效
第二层的任务是让 Agent 在选定领域后,快速定位到具体要调用哪个 Skill。这一层每个 Skill 只给名称 + 一句话描述,描述的质量直接决定选择准确率。
我总结了一个“三要素”写法:动作 + 对象 + 关键约束。举个例子:
- 差的写法:“处理 CSV 文件”(太模糊,不知道是读还是写)
- 好的写法:“读取 CSV 文件并返回结构化数据,支持指定分隔符和编码格式”
再比如:
- 差的写法:“发送请求”(不知道发什么请求、用什么协议)
- 好的写法:“向指定 URL 发送 HTTP GET 请求,支持自定义请求头和超时设置”
关键约束这部分特别重要,因为它能帮 Agent 快速排除不匹配的选项。比如两个 Skill 都是“读取文件”,一个支持大文件流式读取,一个只支持小文件一次性加载,那在描述里标明“适用于 100MB 以上大文件”和“适用于 10MB 以内小文件”,Agent 就能根据实际场景做判断。
另外,第二层的 Skill 排列顺序也有讲究。我习惯把使用频率高、通用性强的 Skill 放在前面。虽然理论上模型应该平等对待每个选项,但实际测试下来,靠前的位置确实会获得略高的选择概率。把常用 Skill 前置,能进一步提升首轮命中率。
3.3 完整定义层的加载时机:延迟加载与缓存策略
第三层是完整的 Skill 定义,包括参数 schema、调用示例、返回值说明、异常处理逻辑。这一层的内容最重,但只有在确定要调用某个 Skill 时才加载。
实现上,我用的是一个简单的“按需注入”机制:Agent 在第二层选定 Skill 后,系统动态地把该 Skill 的完整定义拼接到当前上下文中,然后发起实际调用。调用完成后,这个完整定义可以选择保留或移除。如果保留,后续同一会话中再次调用该 Skill 时就不需要重新加载;如果移除,则每次调用都要重新注入。
我的做法是保留最近 3 个调用过的 Skill 完整定义,更早的则移除,只保留名称在概览层。这样既避免了重复加载的开销,又不会让上下文无限膨胀。实测下来,这个策略在多数场景下能把额外 Token 开销控制在 15% 以内。
提示:完整定义层的加载一定要做缓存。我试过不做缓存直接每次重新拼接,在连续调用场景下延迟增加了将近 40%。加上简单的 LRU 缓存后,延迟回落到几乎无感。
3.4 层间跳转的触发逻辑:谁来判定“该往下走了”
分层加载的另一个关键点是:谁来决定什么时候从第一层跳到第二层,什么时候从第二层跳到第三层?
我的方案是让 Agent 自己判断,但给出明确的触发条件。具体来说,在系统提示里我会写清楚:
- 当用户请求涉及具体操作时,先判断属于哪个领域类别
- 确定领域后,在该领域的 Skill 列表中做选择
- 选定 Skill 后,系统会自动加载完整定义,Agent 无需关心加载过程
这里有个细节:不要强制 Agent 必须走完三层。有些简单任务,Agent 在第一层就能直接定位到唯一匹配的 Skill,那就没必要走第二层。我遇到过一些情况,Agent 在领域分类层看到“文件与存储”就直接说“我要调用文件读取 Skill”,这时候系统应该允许它直接跳到第三层加载完整定义,而不是强制它先看一遍第二层的列表。
这种灵活性需要通过提示词来引导,而不是硬编码在流程里。我的做法是在系统提示中加一句:“如果你已经能确定具体要调用的 Skill 名称,可以直接请求加载该 Skill 的完整定义。”
4. 完整实操流程与核心环节实现
4.1 环境准备与基础结构搭建
先说一下我用的基础环境。Agent 框架用的是自己搭的一套轻量级调度器,核心就是一个循环:接收输入 → 判断是否需要调用 Skill → 加载对应 Skill → 执行 → 返回结果。Skill 的定义统一用 JSON Schema 描述,存在一个本地目录里,每个 Skill 一个文件。
目录结构大概是这样:
skills/ categories.json # 领域分类定义 file_ops/ read_csv.json # 具体 Skill 定义 write_csv.json ... network/ http_get.json ...categories.json里定义每个领域的名称、描述和对应的子目录。系统启动时只加载这个文件,不加载任何具体 Skill 的定义。这是实现分层加载的第一步:启动时只加载索引,不加载内容。
4.2 第一层加载:构建领域索引
启动后,系统读取categories.json,生成第一层的领域索引。这个索引会被拼接到系统提示的最前面,格式大概是:
你可以使用以下领域的技能: 1. 文件与存储:处理本地文件的读取、写入、格式解析和目录管理。 2. 网络与接口:发送 HTTP 请求、调用外部 API、抓取网页数据。 3. 数据处理:清洗、转换、聚合、统计结构化数据。 ...注意这里只给了领域名称和一句话描述,没有任何具体 Skill 的信息。Agent 在第一轮决策时,只能看到这些领域选项。
4.3 第二层加载:按领域展开 Skill 列表
当 Agent 判断当前任务属于某个领域后,它会输出一个类似[LOAD_DOMAIN: 文件与存储]的标记。系统捕获到这个标记后,读取对应子目录下的所有 Skill 文件,提取每个 Skill 的名称和一句话描述,拼接到上下文中。
这一步的实现要注意:只提取名称和描述,不要加载参数定义。我一开始图省事直接把整个 JSON 文件内容都拼进去了,结果第二层的 Token 开销跟全量加载差不多,分层就白做了。后来改成只提取name和summary两个字段,Token 开销直接降了 70%。
4.4 第三层加载:注入完整 Skill 定义
Agent 在第二层选定具体 Skill 后,输出[LOAD_SKILL: read_csv]标记。系统根据这个标记找到对应的 JSON 文件,把完整的参数定义、调用示例、返回值说明拼接到上下文中,然后发起实际调用。
这里有个实操细节:完整定义注入的位置很重要。我试过放在系统提示末尾、放在用户消息后面、放在单独的 system 消息里,最后发现放在紧邻调用指令之前效果最好。因为模型在生成调用参数时,注意力会集中在最近的上下文上,把定义放在这个位置能显著提升参数填充的准确率。
4.5 参数计算与选择过程
分层加载本身不涉及复杂的数值计算,但有一个参数需要你根据实际情况调整:每层的 Token 预算。
我的经验值是:
| 层级 | 建议 Token 预算 | 说明 |
|---|---|---|
| 第一层 | 200-400 | 领域名称+描述,控制在 9 个类别以内 |
| 第二层 | 500-1000 | 每个 Skill 约 30-50 Token,控制在 20 个以内 |
| 第三层 | 800-1500 | 完整定义,包含参数和示例 |
这个预算不是硬性限制,而是用来做监控的。如果某一层的实际 Token 消耗持续超过预算,说明该层的粒度需要调整——要么是类别太多,要么是 Skill 描述写得太啰嗦。
4.6 实操现场记录:一次完整的调用链路
拿一个实际例子走一遍。用户输入:“帮我把这个 CSV 文件里的重复行去掉,然后保存成新文件。”
第一轮:Agent 看到领域索引,判断这属于“数据处理”和“文件与存储”的交叉领域。它输出[LOAD_DOMAIN: 文件与存储]。
第二轮:系统加载文件与存储领域的 Skill 列表,Agent 看到有read_csv、write_csv、deduplicate_csv等。它判断需要先读取再去重再写入,输出[LOAD_SKILL: read_csv]。
第三轮:系统加载read_csv的完整定义,Agent 生成调用参数:{file_path: "input.csv", delimiter: ",", encoding: "utf-8"}。执行后返回数据。
第四轮:Agent 继续输出[LOAD_SKILL: deduplicate_csv],系统加载定义,执行去重。
第五轮:Agent 输出[LOAD_SKILL: write_csv],系统加载定义,执行写入。
整个链路走了五轮,但每一轮加载的内容都是精准匹配当前需求的。对比全量加载时 Agent 经常在第一轮就试图同时调用三个 Skill 的情况,分层加载的决策路径清晰太多了。
5. 常见问题与排查技巧实录
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent 跳过第二层直接猜 Skill | 第一层描述中包含了具体 Skill 名称 | 检查领域描述文本 | 移除所有具体 Skill 名称,只保留领域级描述 |
| 第二层选择准确率低 | Skill 描述过于模糊或相似 | 对比相似 Skill 的描述文本 | 用“动作+对象+约束”三要素重写描述 |
| 第三层加载后参数填充错误 | 完整定义注入位置太远 | 检查定义与调用指令的距离 | 将定义紧邻调用指令放置 |
| 连续调用时延迟明显 | 未做缓存,每次重新加载 | 监控 Token 拼接耗时 | 加入 LRU 缓存,保留最近 3 个 Skill 定义 |
| 上下文 Token 超限 | 第三层定义未及时移除 | 统计各层 Token 占比 | 调用完成后移除早期 Skill 的完整定义 |
5.2 独家避坑技巧
技巧一:给领域分类加“负向示例”。在领域描述里加一句“不包含 XX 类操作”,能有效减少误判。比如“文件与存储”类别里加一句“不包含网络文件下载”,就能避免 Agent 把 HTTP 下载任务归到这个类别。
技巧二:第二层 Skill 描述定期做“去重检查”。我每个月会跑一次脚本,计算所有 Skill 描述之间的语义相似度,把相似度超过 0.85 的挑出来人工审核。这些高度相似的 Skill 往往是调用混乱的重灾区,要么合并,要么把差异点写得更明确。
技巧三:用“调用日志”反推分层是否合理。我会记录每次调用的完整链路:用户输入 → 领域选择 → Skill 选择 → 实际执行结果。如果发现某个领域被频繁选择但最终调用的 Skill 很少,说明这个领域的分类可能有问题;如果某个 Skill 经常在第二层被看到但很少被选中,说明它的描述可能不够吸引人或者功能确实冷门。
技巧四:不要追求“一次加载永久生效”。有些朋友为了省事,第一次加载完某个领域的 Skill 列表后就一直保留在上下文里。这样做短期看是省了加载开销,但长期看会让上下文越来越臃肿,最终还是回到全量加载的老路。该移除的时候要果断移除。
5.3 一个真实的排查案例
之前遇到过一个情况:Agent 在处理“把图片里的文字提取出来”这个任务时,反复在“多媒体”和“文本与内容”两个领域之间跳转,最后选了一个不相关的 Skill。
排查过程是这样的:先看第一层,发现“多媒体”的描述是“处理图片、音频、视频文件”,“文本与内容”的描述是“摘要、翻译、改写、关键词提取”。问题出在“提取文字”这个动作既涉及图片处理又涉及文本输出,两个领域都有理由选。
解决方案是在“多媒体”领域描述里加上“包括从图片中提取文字信息”,同时在“文本与内容”领域描述里加上“不包含从图片中提取文字”。这样 Agent 就能明确知道该往哪个方向走。改完之后,这个任务的首次领域选择准确率从不到 50% 提升到了 90% 以上。
这个案例说明,分层加载的效果很大程度上取决于边界定义的清晰度。领域之间的职责边界越明确,Agent 的决策就越果断。
6. 分层加载的扩展思路与个人体会
这套机制跑顺之后,我又做了一些扩展。比如在第二层和第三层之间加了一个“参数预检”环节:Agent 选定 Skill 后,先不加载完整定义,而是加载一个轻量级的参数列表,让 Agent 确认参数是否齐全。如果缺少必要参数,可以提前向用户询问,避免加载完完整定义才发现参数不够、又要重新来一遍。
另一个扩展方向是跨领域的 Skill 组合。有些任务天然需要多个领域的 Skill 配合,比如“从网页抓取数据 → 清洗 → 保存到本地”就涉及网络、数据处理、文件存储三个领域。我的做法是在第一层加一个“组合任务”类别,里面预置一些常见的跨领域工作流,Agent 可以直接选择工作流,系统再按顺序加载各个领域的 Skill。这样比让 Agent 自己一步步跳转效率高不少。
我个人在实际操作中的体会是:分层加载的核心价值不在于“省 Token”,而在于让 Agent 的决策路径变得可追踪、可调试。全量加载时,Agent 为什么选错 Skill 往往很难定位;而分层加载后,你可以清楚地看到它在哪一层做了错误判断,是领域分类不清晰,还是 Skill 描述有歧义,排查起来有的放矢。
最后分享一个小技巧:如果你刚开始搭建分层机制,不要一上来就追求完美分类。先用一个粗糙的版本跑起来,积累一段时间的调用日志,再根据实际数据去调整分类和描述。我第一版的分层结构跟现在差别很大,都是在实际使用中逐步打磨出来的。