当 Agent 里的 Skill 数量超过 100 个之后,调用命中率会明显下降。这不是模型突然变笨了,而是我们把太多能力平铺在一起,让模型在每次决策时都要面对一个巨大的、彼此相似的候选集。最近我在维护一个 Agent 项目时,就遇到了这个现象:技能库从 30 个涨到 120 多个后,原先很多一次就能命中的调用开始频繁选错。同一个请求,有时候触发 A,有时候触发 B,偶尔还会调用一个看起来毫不相关的技能。后来我意识到,这已经不是一个提示词优化问题,而是一个信息架构和路由工程问题。
所以这篇文章不是教你怎么写单个 Skill,而是讨论一个更麻烦的问题:当 Skill 数量过百,Agent 该怎样保证调用命中率。我会从底层原因、元数据设计、路由策略、描述写法、测试方法和长期维护六个维度展开,尽量把这条路径讲透。
1. 先理解命中率为什么会在过百之后崩掉
很多团队的直觉是:Skill 越多,Agent 能力越强。这个直觉在十几个 Skill 的时候基本成立。但一旦数量过百,问题就不再是“能力不够”,而是“决策空间太大”。
1.1 模型选择 Skill 的过程,本质上是一次有约束的文本生成
要理解命中率,先要放下一个误解:模型并不是在“搜索”技能库,而是在生成下一个最合理的动作。
当你把所有 Skill 的描述都塞进系统提示词时,模型面对的是几百行文字。它需要在这些文字里找到与用户请求最匹配的一项,然后输出对应的调用指令。这个过程不是精确匹配,而是基于上下文的概率生成。
如果候选只有 10 个技能,模型几乎不会选错。因为每个技能的描述差异很大,用户请求和技能之间的关联很清晰。但当候选达到 100 个以上时,问题就不一样了:很多技能在“功能描述”上高度相似,比如“生成周报”“生成日报”“生成会议纪要”,单看描述都能对上“帮我把工作内容整理一下”这个请求,模型就会在几个相近选项之间摇摆。
这时候的命中率下降,不是模型能力退化,而是任务本身的判别难度上升了。
1.2 上下文过载后,相似描述会互相干扰
还有一个常被忽略的因素:上下文窗口和注意力机制。
系统提示词里的内容越多,每个 Skill 描述实际分到的注意力就越少。更麻烦的是,描述之间会互相干扰。比如 A 技能写了“生成 Python 脚本”,B 技能写了“执行 Python 代码”,C 技能写了“分析 Python 项目结构”。这三个描述放在一起,模型很难准确判断用户说“帮我写一段 Python 脚本”时应该调用哪一个。
从工程经验看,当技能总数超过 50 个后,继续往系统提示词里堆描述,收益会快速衰减;超过 100 个后,甚至会出现负面效果:模型更容易被无关技能带偏,或者为了“显得有用”而强行调用某个技能。
1.3 命中率下降的三种典型表现
在实际项目里,命中率下降通常不是突然归零,而是以三种形式出现:
- 选错:用户请求是 A,Agent 调用了 B。比如要“创建任务”,它调用了“删除任务”。
- 犹豫:模型先调用一个技能,结果发现不合适,又尝试另一个,反复横跳,浪费大量 token。
- 幻觉调用:模型调用了当前 Skill 列表里不存在的技能,或者把两个技能拼在一起,产生一个错误指令。
这三种情况我都遇到过。最隐蔽的是第二种——它不会直接报错,但会让整个 Agent 的执行链路变得非常慢,而且用户会明显感觉到“这个 Agent 不稳定”。
注意:当命中率下降时,先不要急着优化单个 Skill 的描述。首先要判断是选错、犹豫,还是幻觉调用,因为这三类问题的解法完全不同。
2. 给 Skill 建立“可路由的元数据”,而不是写更多描述
很多人的第一反应是给每个 Skill 写更详细的描述。这个方向有问题。描述写得再长,如果模型需要在 100 个长描述里找答案,识别难度依然很高。
真正需要做的,是先给 Skill 建立一套可被路由的元数据。
2.1 先区分“使用说明”和“用于选择的检索信息”
一个 Skill 通常包含两部分信息:
- 使用说明:这个技能怎么执行、有哪些参数、依赖什么环境。
- 用于选择的检索信息:什么场景下应该调用它、什么场景下不该调用它、它和哪些技能容易混淆。
传统的 Skill 文件往往把这两部分混在一起。如果让模型直接读完整文件来做选择,信息量太大,而且很多执行细节对“选择”没有帮助。
我建议的做法是:单独维护一个 Skill 索引层,只保留和“选择”相关的字段。模型决策时先看索引,等确认调用后,再加载完整的 Skill 定义。
2.2 一个可落地的 Skill 元数据字段
下面是一组我在实际项目中使用的字段结构,供参考:
| 字段 | 作用 | 必填 |
|---|---|---|
| skill_id | 技能唯一标识,建议用短横线命名,如generate-weekly-report | 是 |
| name | 人类可读名称,用于日志和调试 | 是 |
| category | 技能所属分类,如report、code、data_analysis | 是 |
| summary | 一句话描述,用于模型快速判断 | 是 |
| trigger_examples | 典型触发请求示例,2 到 5 个即可 | 是 |
| avoid_when | 明确说明什么情况下不要调用这个技能 | 否 |
| dependencies | 依赖的工具、文件或前置条件 | 否 |
| priority | 当多个技能匹配时,是否优先调用,可选值为normal、high | 否 |
重点是summary和trigger_examples。这两个字段直接决定模型能不能在候选列表里快速定位。
举个例子:
skill_id: generate-weekly-report name: 生成周报 category: report summary: 根据本周工作记录生成周报文档 trigger_examples: - 帮我写这周的周报 - 生成本周工作总结 avoid_when: 用户要求的是日报,或只要求整理数据,不需要生成叙述性报告 priority: normal这样的结构比一段三百字的描述更容易被模型理解,也更适合后续做检索。
2.3 用分类和命名把决策空间压缩到 10 以内
元数据里最容易被忽略的是category。我强烈建议把 100 个 Skill 先归到不超过 10 个大类里,比如:
| 分类 | 示例技能 |
|---|---|
| report | 周报、日报、月报、会议纪要 |
| code | 代码生成、代码审查、代码重构 |
| data | 数据查询、数据分析、图表生成 |
| document | 文档转换、文档摘要、文档翻译 |
| automation | 任务创建、提醒设置、流程编排 |
分类的价值在于:当模型面对 100 个技能时,它很难选出正确的一个;但如果先判断“这是一个报告类需求”,再在 report 分类下的 10 个技能里选,压力就小很多。
实际操作时,可以把分类信息也放进系统提示词,或者作为路由逻辑的一部分。无论哪种方式,目标都是让模型不要直接面对全量技能。
3. 从“让模型自己猜”升级成“显式路由 + 检索召回”
如果说元数据是基础,那路由策略就是真正让命中率稳定下来的关键。
我不建议在技能数过百之后,还依赖“把所有描述塞进上下文,让模型自由发挥”的方式。更好的做法是让系统先做一轮筛选,再把少量候选交给模型决策。
3.1 轻量方案:两级分类路由
第一种方案不需要引入额外模型,利用规则和分类即可完成。
整体流程是:
- 先让模型判断用户请求属于哪个
category。 - 只把该分类下的 Skill 列表提供给模型。
- 模型在 10 个左右候选里做最终选择。
比如用户说“帮我把这份文档翻译成英文”,第一步先判断属于document,第二步把document分类下的所有技能描述给模型,模型再选具体是“文档翻译”还是“文档格式化”。
这个方案实现简单,效果提升也很明显。缺点是如果分类判断错了,后面再怎么选都是错的。所以分类层需要做得足够粗,确保大多数请求都能归到正确的类目下。
3.2 进阶方案:先向量检索,再让模型做最终选择
如果 Skill 数量继续增长,或者用户请求的表达方式非常多,两级分类可能会遇到瓶颈。这时候可以考虑引入检索增强。
基本思路是:
- 把所有 Skill 的
summary和trigger_examples向量化,存入向量库。 - 用户请求进来后,先通过 embedding 检索出最相关的 top 5 到 top 10 个 Skill。
- 把检索结果连同原始请求一起交给模型,由模型做最终选择。
这套方案的好处是召回能力更强。用户即使换了说法,也能通过向量相似度找到相关技能。它也有代价:需要维护向量索引,需要处理检索失败的情况,还需要控制检索结果的多样性,避免 10 个候选全是同一个类型的技能。
我一般建议先做两级分类,等确实出现“分类正确但相似技能太多”的问题时,再叠加向量检索。不要一上来就追求复杂的检索系统。
3.3 兜底机制:强制确认和人工切换
无论路由策略多好,都无法保证 100% 命中。所以兜底机制非常重要。
一种常见的做法是:当模型选择某个 Skill 时,如果置信度不高,先向用户确认一下,而不是直接执行。
实际操作可以在提示词里加一个约束,比如:
如果你不确定用户请求匹配哪个技能,请列出最多两个候选,并询问用户。另一种兜底是提供人工切换入口。比如 Agent 界面里显示当前命中的 Skill,用户可以手动指定其他 Skill。这个机制虽然看起来不够“智能”,但在生产环境里非常实用,它能避免错误调用带来的更大损失。
值得注意的是:单纯把候选列表从 100 减小到 10,命中率就能提升不少。这种提升不来自模型变强,而是来自决策难度降低。
4. 写 Skill 描述时,重点写“什么时候用”,而不是“能做什么”
很多 Skill 文件里会把“能做什么”写得很详细,比如“本技能支持多种格式、支持自定义模板、支持批量导出”。这些对使用阶段可能有价值,但对“选择”来说反而是噪音。
4.1 描述的核心任务:让模型判别,而不是让模型理解
在技能数量很多时,模型读取描述的目的只有一个:判断这个技能是否匹配当前请求。所以描述要写成“判别式”,而不是“解释式”。
换句话说,不要写“本技能可以生成一份内容详尽的周报,支持自定义时间范围、项目维度、人员维度……”,而应该写“当用户要求生成周报时使用。用户需要提供时间段和工作内容,或者提供工作记录文件”。
用更具体的话说:
- 写得好的描述:用户提出 A 需求时调用。
- 写得不好的描述:本技能是用于生成周报的智能助手,能够帮助你高效完成……
识别标准很简单:把描述里的功能形容词全部删掉,如果句子依然能告诉模型“什么时候调用”,那就是一个合格的判别式描述。
4.2 写清楚“不要用它”的场景
大多数人在写描述时只会写“什么时候用”,很少写“什么时候不用”。在 100 个 Skill 的候选空间里,“什么时候不用”往往比“什么时候用”更能帮助模型排除干扰。
比如一个“文档摘要”技能,可以加上一局:
avoid_when: 如果用户只是要求翻译文档,不要调用本技能。这看起来像废话,但在实际测试中,正是这种“排除性提示”能显著降低相似技能的混淆率。
我建议每个容易混淆的技能都写avoid_when。如果几个技能的trigger_examples有大量重叠,优先补全avoid_when,而不是继续加长summary。
4.3 相似 Skill 的合并、拆分与区分约束
技能库里最容易出现的问题是:看起来不同的技能,实际服务的是同一类需求。比如“生成周报”和“生成工作小结”,用户可能觉得是一回事。
如果发现两个 Skill 的触发场景重叠超过 60%,可以考虑合并。合并后通过参数区分不同输出格式。比如统一成一个“工作汇报生成”技能,通过report_type: weekly|summary参数控制。
如果确实需要保留两个技能,就必须在描述中给出明确的区分点。比如:
- A:用户要求“按周维度生成汇报”时调用。
- B:用户要求“站在个人角度写一段简短总结”时调用。
这种显式区分,比让模型自己领悟“周报”和“小结”的区别可靠得多。
5. 用调用测试集衡量命中率,而不是靠几次功能测试
优化命中率这件事,最怕“凭感觉”。有时候你改了一版描述,测试几句话感觉变好了,但一上线又崩。原因是样本太少,而且没有覆盖到真实的坏case。
所以,Skill 数量过百之后,我强烈建议建立一套“调用级测试集”。
5.1 构建一个覆盖不同场景的测试集
测试集不需要很大,但必须覆盖几类关键场景:
- 每个分类下至少 2-3 条典型请求。
- 每个容易混淆的技能对至少 1 条“边界请求”。
- 至少 5 条跨分类模糊请求,比如“帮我把这个数据整理成表格再写段说明”,可能同时涉及数据处理和文档生成。
- 至少 3 条故意带噪音的请求,比如包含多层含义或模棱两可的表达。
测试集的价值在于,你可以通过反复运行同一个请求,观察 Agent 的调用结果是否稳定。如果同一条请求第一次调用 A,第二次调用 B,那就说明路由策略存在不稳定因素。
5.2 记录每次调用的完整链路
为了定位命中率问题,日志至少需要记录:
用户请求 模型分类结果(如果有多级分类) 检索候选列表(如果使用了检索) 最终选中的 Skill 执行结果或报错信息 耗时和 token 消耗有了这些日志,你才能回答一个关键问题:模型选错,是检索阶段没有召回正确技能,还是召回了但最终选择错误?
排查顺序可以这样走:
- 复现一次失败调用,拿到完整日志。
- 检查检索阶段的候选列表里是否包含正确技能。
- 如果候选列表里没有,说明是检索或分类问题,要去调整召回逻辑。
- 如果列表里有但模型还是选错,说明是最终决策问题,要去优化描述和候选排序。
- 如果模型反复横跳,说明候选之间的区分度不足,需要增加
avoid_when或合并技能。 - 如果模型直接调用了不存在的 Skill,说明提示词约束失效,需要检查输出格式限制。
这个排查链路能少走很多弯路。
5.3 用混淆矩阵找出容易打架的技能对
在测试集跑过一轮后,可以把结果做成一个简化版的混淆矩阵。横轴是应该调用的 Skill,纵轴是实际调用的 Skill。对角线上是命中,非对角线就是选错的情况。
比如你发现用户请求“写周报”时,有 3 次调用了“生成工作项列表”,有 2 次调用“生成项目进度同步”。这说明这两个技能和“写周报”之间存在较强的混淆,需要重点处理。
处理方式通常是两种:
- 调整描述,让“工作项列表”明确排除“叙述性周报”场景。
- 调高
生成周报的优先级,或者把容易混淆的技能归到同一分类下,让模型先选大类,再细选。
6. 过百之后,不要忘记长期维护和准入门槛
Skill 数量过百,不是一次性建设,而是一个持续演进的过程。随着项目增长,你会不断新增技能,也会废弃旧技能。如果缺乏维护机制,命中率会再次下降。
6.1 新增 Skill 也要过“路由评审”
我见过很多项目,新增 Skill 时只验证“这一个技能能不能跑通”,完全不看它会不会和现有技能冲突。结果就是技能库越来越大,互相覆盖的也越来越多。
建议新增 Skill 时,至少回答三个问题:
- 用户请求在什么情况下会想到这个技能?
- 它和现有技能的重叠度有多高?
- 如果两个技能都匹配,模型应该如何区分?
如果第三个问题答不上来,说明这个新 Skill 还不够清晰,或者它应该作为现有技能的参数,而不是独立存在。
6.2 定期下线低命中率技能
每次跑完一轮调用测试,除了看哪些技能被错误调用,还要看哪些技能长期没有被调用。
如果一个 Skill 在过去 30 天内的调用次数为 0,或者命中率一直低于某个阈值,就应该标记为「低频」。低频技能保留在候选列表里,不仅不会提升体验,反而会增加模型的决策干扰。
处理方式有两种:直接归档,或者把它们从默认候选列表里剔除,改为通过搜索或用户显式指定才可用。归档不是删除,而是降低它们的“可见度”。
6.3 把 Skill 定义和路由策略拆成独立模块
最后一个建议是关于工程结构的。
不要把路由逻辑写死在某个 Agent 的 System Prompt 里。更合理的做法是:
skills/目录下存放每个 Skill 的完整定义。skill_index.yaml存放用于选择的元数据索引。router.py或router.ts等独立模块负责召回和排序。- Agent 启动时加载一次索引,请求进来时走路由逻辑,再调用具体 Skill。
这样做的价值在于,你可以单独测试、单独优化路由模块,而不需要重新调整 Agent 主流程。等 Skill 数量到几百个时,这套结构几乎是必须的。
说到底,Skill 数量过百后,Agent 调用命中率的问题,已经不是“这个模型强不强”的问题,而是“你有没有把决策空间管理好”的问题。真正有效的思路,不是让模型在庞大列表里大海捞针,而是通过元数据、分类、检索和测试,把正确的候选在正确的时间推到模型面前。
如果你的项目也走到了这一步,我的建议是先别急着调描述、加示例。先把所有 Skill 的元数据梳理出来,做一个最简单的分类路由,然后用一批真实请求去跑回归。你会发现,很多命中率问题的答案,其实早在你决定“把所有技能都塞进提示词”的那一刻就注定了。