Agent技能过百后调用命中率下降?元数据设计与路由策略实战

📅 2026/8/27 2:40:50
Agent技能过百后调用命中率下降?元数据设计与路由策略实战
当 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 命中率下降的三种典型表现在实际项目里命中率下降通常不是突然归零而是以三种形式出现选错用户请求是 AAgent 调用了 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 的元数据梳理出来做一个最简单的分类路由然后用一批真实请求去跑回归。你会发现很多命中率问题的答案其实早在你决定“把所有技能都塞进提示词”的那一刻就注定了。