当 Agent 选错 Skill 时:四个修复方法
你的 Skill 库在只有 12 个 Skill 时运行得非常漂亮。可当数量超过 100 个,Agent 开始跳过那个显而易见的 Skill,转而选择一个仅仅“看起来相关”的选项。
你没有更换模型,也没有修改 Skill。你只是把菜单变长了。就在规模增长的某个节点,Agent 不再是在阅读一张短清单,而是在进行一次它从未被调校好、也很难胜任的搜索。Skill 本身没有变差,真正退化的是检索。

路由就是检索,而描述就是索引条目
Agent 从来没有阅读你的 Skill。它只看到一组描述,然后押注哪一项与用户请求最相似。
Skill 通过三层“渐进式披露”加载。启动时,Agent 只会把每个已安装 Skill 的名称与描述载入系统提示词。当一个请求看起来与某个 Skill 匹配时,它才会读取该 Skill 的完整指令。只有任务真正需要时,它才会进一步打开随附的脚本或参考文件。

第一层决定了一切,但大多数作者优化的恰恰是错误的层。他们反复打磨 Skill 内部的指令,可在选择发生时,Agent 甚至还没有看到这些内容;与此同时,真正用于选择的描述却写得十分含糊。
在路由阶段,Agent 并不是根据 Skill 的完整能力做推理。它只是把用户请求中的词,与 Skill 标签中写下的词进行匹配。
因此,你写的不是普通文档,而是一个索引条目。用户请求就是必须命中它的查询。一旦接受这个视角,接下来的问题就很清楚了:这是把搜索工程应用到自己的 Skill 目录上。

匹配失败只有两种形式:本应触发的 Skill 没有响应,这是“漏选”;本应保持安静的 Skill 却被选中,这是“误选”。后面的每一种修复方法,针对的都是其中一种错误。
在磁盘上,一个 Skill 只是一份文件。发现阶段只读取顶部的两个字段;只有 Skill 被激活以后,下面的正文才会被读取。因此,打磨正文不会改善选择效果。
SKILL.md
---
name: spreadsheet
description: A skill for working with spreadsheets and data files.
---
# 这条线以下都是 Skill 正文。
# 发现阶段不会加载它,只有选中 Skill 后才会读取。
# 所以正文不是决定 Skill 能否被选中的那一层。
为什么十几个 Skill 路由顺畅,上百个就开始崩溃
只有十几个 Skill 时,描述写得草率几乎没有代价,模型可以暴力遍历短清单。随着菜单变长,描述质量带来的惩罚会迅速放大。
目录增长时,两种压力会同时上升。
第一种是 Token 成本。任何工作开始前,每一条描述都已经进入提示词。Anthropic 曾测得,优化前的工具定义占用了约 13.4 万 Token,其中仅一个连接器就占据了相当大的部分。
第二种是区分难度。条目越多,描述相似功能的条目就越多,模型越难把它们分开。
现实中的失败也符合这一规律:最常见的错误是选错工具和填错参数,尤其容易发生在名称非常相似的功能之间,例如一个功能把消息发给用户,另一个功能把消息发到频道。只有几个 Skill 时,这类冲突很少发生;有几百个 Skill 时,它会成为常态。

大致可以把规模划成四个区域:十几个以内,描述怎么写影响不大;接近 30 个时,描述质量开始决定漏选和误选;超过 100 个后,近似条目相互碰撞,目录本身也开始挤占上下文;超过 1000 个后,模型甚至无法稳定看到正确条目,扁平选择随之失效。

这些分区并不是随意划定的。每一个节点,都意味着此前的方法开始失效,需要换一种解决方案。
修复一:把描述写成查询,而不是摘要
解释 Skill 能做什么的描述是文档;说明什么时候应该使用它的描述才是触发器。只有后者能让 Skill 被选中。
这是杠杆最高、成本最低的一项修复。Anthropic 的指导很直接:描述必须同时包含 Skill 做什么,以及哪些具体场景应该触发它。把摘要变成触发器的关键词,就是“当……时使用”,并在后面列出模型应当关注的真实场景。

四种改动最重要,而且不能混为一谈:
- 位置: 把“何时使用”放进第一句话。拥挤的会话可能截断描述,你无法预测截断发生在哪里。
- 人称: 使用第三人称。描述会被注入系统提示词,第一人称会模糊说话者是谁,影响发现。
- 具体性: 写真实请求中会出现的文件类型、动作和领域词汇,不要只写“有用”“强大”之类形容词。
- 立场: 可以稍微积极一点。
最后一点看似反直觉。模型往往会少触发 Skill,即使匹配也倾向于跳过。因此 Anthropic 的 Skill Creator 建议把描述写得主动一些,例如:“只要用户提到仪表盘或指标,就使用此 Skill,即使用户没有明确说要做仪表盘。”这是在纠正模型偏向沉默的已知倾向,也就是前面提到的“漏选”。
描述还有严格的长度预算。开放规范把描述限制在约 1000 个字符以内,客户端列表中描述与触发文本合计到约 1500 个字符时也可能被截断。触发条件、排除项和关键词都在争夺同一空间,因此触发器必须放在最前,措辞也必须简洁。

代码层面可以这样理解:Agent 只看到这张列表,再用请求去匹配描述,并不会读取描述下面的 Skill 正文。
# 发现阶段只把一组 (name, description) 交给模型。
catalog = [
Skill(
"spreadsheet",
# 触发条件优先、第三人称、稍微积极
"Use when the user opens, cleans, or charts an xlsx or csv file, "
"or mentions pivot tables, even if they never say 'spreadsheet'.",
),
Skill(
"pdf",
"Read and fill PDF documents. Use when the user mentions PDFs, "
"forms, or document extraction.",
),
]
def select(request, catalog):
# 模型匹配的是描述,不是 Skill 正文
return max(catalog, key=lambda skill: match(request, skill.description))
修复二:明确告诉模型不应该去哪里
一个过度积极的 Skill 不会主动告诉你“这里发生了路由错误”。它通常表现为输出质量很差,让你花上一整天修改正文和示例,而真正的问题是:这个 Skill 根本不应该被选中。
这就是第二种失败模式——过度触发,也是整篇文章里最容易修复的问题。一个过于宽泛的描述会在不适合的请求上触发,生成薄弱结果,并把自己伪装成质量问题。你反复调指令、调示例,结果仍然没有改善,因为错误发生在选择层,而不是执行层。
一位编写 Kubernetes 策略 Skill 的开发者遇到过这个问题。通用描述让该 Skill 在普通故障排查请求中也被触发,结果很差。他只增加了一句负向边界:“不要用于一般性的 Kubernetes 故障排查”,一次迭代就解决了此前排查一天的问题。

值得养成的诊断习惯是:为每个 Skill 写 10 个应该触发的提示词,再写 10 个不应该触发的提示词,在评估输出质量前先运行它们。过度触发会立刻暴露,一句负向边界通常就能补上缺口。这样就把“触发问题”和“质量问题”拆开了,它们本来就应该在不同位置调试。
写负向边界时也要小心。全部大写的 MUST、ALWAYS 和 NEVER 是值得重新斟酌的信号。它们更像脆弱的绝对命令,而不是适用于边界情况的判断指导。

这个测试框架很小,完全可以和每个 Skill 放在一起:
should_fire = [...] # 10 个应该触发的请求
should_not_fire = [...] # 10 个必须忽略的相邻请求
def triggering_test(skill):
misses = [prompt for prompt in should_fire if not fires(skill, prompt)]
false_hits = [prompt for prompt in should_not_fire if fires(skill, prompt)]
return misses, false_hits
# 在评估输出质量前先运行它
misses, false_hits = triggering_test(policy_skill)
if false_hits:
# 修描述,而不是 Skill 正文
policy_skill.description += " Do NOT use for general troubleshooting."
修复三:先缩小搜索空间,再开始搜索
模型并不擅长一次从 300 个 Skill 中选择,但它很擅长从 3 个中选择。因此,不要再要求它一次面对 300 个选项。
当扁平列表本身已经成为问题时,就把路由拆成多个层级:先选择类别,再选择类别中的具体 Skill。每一步都只需在少量候选项中决策,近似条目的范围被逐层缩小,漏选和误选都会下降。

这是一个需要主动构建的模式,而不是打开某个开关就能获得的能力。原生 Skill 发现仍是扁平的,所以层级要体现在目录的组织和暴露方式中。相关研究支持“先粗后细”的检索方式:先选类别,再选工具,最后选接口;也可以先聚合父级,结合关键词匹配与稠密向量检索,再展开排名最高的少量父节点。
这并非只在玩具数据集上有效。有一项近期基准覆盖 70 个服务器、527 个工具和真实的多步骤问题,已经处于扁平列表明显失效的规模。结论是:当目录超出单个提示词可以清晰容纳的规模,两层决策就不再是技巧,而会成为默认架构。

成本真实但很小:你多增加一次选择,并需要维护分类体系,确保 Skill 位于正确分支。换来的好处是,模型再也不必在一次决策里面对完整目录。
def route(request, tree):
domain = select(request, tree.categories) # 从约 10 个领域中选择,例如 data
skill = select(request, tree.skills_in(domain)) # 再从约 10 个 Skill 中选择,例如 xlsx
return skill
# 两次小决策,取代一次几乎不可能的百选一
修复四:当文字失效时,在下面加一层检索器
超过 1000 个 Skill 后,再好的描述也无能为力,因为模型根本看不到其中的大多数条目。解决办法不再是继续润色,而是停止一次性把所有内容都展示给模型。
在规模上限处,需要改变架构:不再把每一个条目都塞进提示词,而是根据请求先召回一小组候选项,再让模型从短名单中选择。还可以增加可选的重排步骤,在候选项进入模型前先调整顺序。这就是普通的信息检索,也是文档搜索已经使用多年的“召回—排序”管线,只不过现在检索目标换成了 Skill 目录。
RAG-MCP 研究给出的增益很明显:在大型工具集中,一层基础检索把工具选择准确率从 13.62% 提升到 43.13%,同时把平均提示词 Token 从约 2134 降到 1084,几乎减少一半。这不是调参带来的小幅改进,而是系统进入了另一种工作区间。
研究也诚实地指出了上限:当注册表达到数千个条目时,检索精度本身也会下降。这时应该增加重排,而不是继续依赖单一召回。

Anthropic 已经把这种方法用于工具。开发者可以把工具定义标记为延迟加载,让模型一开始只看到一个搜索工具和少数关键工具;需要更多能力时,模型再通过关键词排名搜索,得到 3~5 个候选引用,并把它们展开成完整定义。
据报告,这种方式减少了约 85% 的 Token;在一个模型上,选择准确率从 49% 提升到 74%,另一个模型则从 79.5% 提升到 88.1%。在编码客户端中,一旦工具描述超过上下文窗口的 10%,系统就会自动切换到搜索索引。

这里存在一个重要的不对称,它决定了你应该把精力花在哪里:如今工具已经拥有检索器,而 Skill 发现仍依赖把全部描述扁平注入提示词,下面没有重排阶段去补救一个糟糕标签。因此,前面讲的文字设计对 Skill 更重要,而不是更不重要。比关键词召回更前沿的做法——训练专门的选择模型——确实存在,但目前仍主要停留在研究阶段。
整个管线可以压缩为三个阶段,完整目录从来不会整体进入提示词:
def route_at_scale(request, index):
candidates = index.recall(request, k=5) # 只召回短名单
candidates = rerank(request, candidates) # 可选:进入模型前先重排
return model.select(request, candidates) # 从 5 个中选,而不是从 5000 个中选
Anthropic 针对工具提供的具体形式如下:长尾工具被标记为可发现,而不是预先加载。
tools = [
{"type": "tool_search_tool_bm25_20251119", "name": "tool_search"},
{"name": "get_weather", "description": "...", "defer_loading": True},
# 保留少数关键工具,其余延迟加载
]
# 搜索返回 3~5 个候选项,需要时再展开完整定义
按照自己的规模行动,而不是照搬别人的方案
最昂贵的错误,是为 40 个 Skill 构建检索器;第二昂贵的错误,是手工调整 4000 个 Skill 的描述。
修复方法必须与自己所处的规模匹配。
少于约 30 个 Skill 时,只打磨描述就足够了:把触发条件放在最前,使用第三人称,并稍微积极一点,抵消模型不触发的倾向。再重的系统只会拖慢你。
超过 100 个后,近似条目冲突与 Token 成本会同时到来。为容易越界的 Skill 增加负向边界,再增加类别层,让模型每一步都只从少数候选项中选择。大多数严肃的内部目录都处在这个区间,这也是“漏选/误选”双错误模型最有价值的地方。
超过 1000 个后,文字已经发挥完全部作用。构建“召回—重排”层,或者使用平台提供的工具搜索,让模型从短名单中选择,而不是面对整面书架。描述仍然重要,因为检索器会索引它们,但它已经不再是请求与正确 Skill 之间唯一的防线。

贯穿全文的逻辑很简单:可控变量一开始是你写下的文本,直到规模迫使它变成你搭建的架构。
如果这周只做一件具体的事,就挑出最常用的 3 个 Skill,分别运行“10 个应该触发、10 个不应该触发”的测试。20 分钟得到的路由认知,往往比一周盲目修改指令更多。

全文可以压缩成一个函数:先确定自己的 Skill 数量,再选择下一步。
def next_move(skill_count):
if skill_count < 30:
return "写好描述,然后停下"
if skill_count < 1000:
return "增加负向边界和类别层"
return "构建召回与重排,或使用平台工具搜索"
延伸阅读
- Anthropic:高级工具使用——Tool Search、延迟加载,以及 Token 与准确率收益
- Anthropic:Skill 编写最佳实践——第三人称描述、触发器,以及多 Skill 选择
- Anthropic Skill Creator:为什么描述应稍微积极,以抵消触发不足
- RAG-MCP:通过检索提高工具选择率并减少提示词 Token
- Red Hat:Tool RAG——召回、重排和分层工具选择
- 一个真实案例:一句负向边界如何修复 Skill 的过度触发
下面是一篇与本文标签相关的文章。
真实的 Loop Engineering 是什么样的?
从 Ralph 循环、主流编码框架的 /goal 命令和真实开发案例出发,重新理解循环工程的价值、局限与适用场景。
继续阅读……