当 Agent 选错 Skill 时:四个修复方法

你的 Skill 库在只有 12 个 Skill 时运行得非常漂亮。可当数量超过 100 个,Agent 开始跳过那个显而易见的 Skill,转而选择一个仅仅“看起来相关”的选项。

你没有更换模型,也没有修改 Skill。你只是把菜单变长了。就在规模增长的某个节点,Agent 不再是在阅读一张短清单,而是在进行一次它从未被调校好、也很难胜任的搜索。Skill 本身没有变差,真正退化的是检索。

不断增长的 Skill 目录

路由就是检索,而描述就是索引条目

Agent 从来没有阅读你的 Skill。它只看到一组描述,然后押注哪一项与用户请求最相似。

Skill 通过三层“渐进式披露”加载。启动时,Agent 只会把每个已安装 Skill 的名称与描述载入系统提示词。当一个请求看起来与某个 Skill 匹配时,它才会读取该 Skill 的完整指令。只有任务真正需要时,它才会进一步打开随附的脚本或参考文件。

Skill 的三层渐进式披露

第一层决定了一切,但大多数作者优化的恰恰是错误的层。他们反复打磨 Skill 内部的指令,可在选择发生时,Agent 甚至还没有看到这些内容;与此同时,真正用于选择的描述却写得十分含糊。

在路由阶段,Agent 并不是根据 Skill 的完整能力做推理。它只是把用户请求中的词,与 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 时,它会成为常态。

Skill 数量增长后路由准确率开始下降

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

不同 Skill 规模对应不同的修复方案

这些分区并不是随意划定的。每一个节点,都意味着此前的方法开始失效,需要换一种解决方案。

修复一:把描述写成查询,而不是摘要

解释 Skill 能做什么的描述是文档;说明什么时候应该使用它的描述才是触发器。只有后者能让 Skill 被选中。

这是杠杆最高、成本最低的一项修复。Anthropic 的指导很直接:描述必须同时包含 Skill 做什么,以及哪些具体场景应该触发它。把摘要变成触发器的关键词,就是“当……时使用”,并在后面列出模型应当关注的真实场景。

描述的第一句话应该直接写触发条件

四种改动最重要,而且不能混为一谈:

  1. 位置: 把“何时使用”放进第一句话。拥挤的会话可能截断描述,你无法预测截断发生在哪里。
  2. 人称: 使用第三人称。描述会被注入系统提示词,第一人称会模糊说话者是谁,影响发现。
  3. 具体性: 写真实请求中会出现的文件类型、动作和领域词汇,不要只写“有用”“强大”之类形容词。
  4. 立场: 可以稍微积极一点。

最后一点看似反直觉。模型往往会少触发 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 在错误场景中触发

值得养成的诊断习惯是:为每个 Skill 写 10 个应该触发的提示词,再写 10 个不应该触发的提示词,在评估输出质量前先运行它们。过度触发会立刻暴露,一句负向边界通常就能补上缺口。这样就把“触发问题”和“质量问题”拆开了,它们本来就应该在不同位置调试。

写负向边界时也要小心。全部大写的 MUSTALWAYSNEVER 是值得重新斟酌的信号。它们更像脆弱的绝对命令,而不是适用于边界情况的判断指导。

用十个正例和十个反例测试 Skill 路由

这个测试框架很小,完全可以和每个 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

这是一个需要主动构建的模式,而不是打开某个开关就能获得的能力。原生 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,几乎减少一半。这不是调参带来的小幅改进,而是系统进入了另一种工作区间。

研究也诚实地指出了上限:当注册表达到数千个条目时,检索精度本身也会下降。这时应该增加重排,而不是继续依赖单一召回。

召回和重排组成的大规模 Skill 检索流程

Anthropic 已经把这种方法用于工具。开发者可以把工具定义标记为延迟加载,让模型一开始只看到一个搜索工具和少数关键工具;需要更多能力时,模型再通过关键词排名搜索,得到 3~5 个候选引用,并把它们展开成完整定义。

据报告,这种方式减少了约 85% 的 Token;在一个模型上,选择准确率从 49% 提升到 74%,另一个模型则从 79.5% 提升到 88.1%。在编码客户端中,一旦工具描述超过上下文窗口的 10%,系统就会自动切换到搜索索引。

Anthropic 的 Tool Search 与延迟加载机制

这里存在一个重要的不对称,它决定了你应该把精力花在哪里:如今工具已经拥有检索器,而 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 之间唯一的防线。

不同目录规模对应的 Skill 路由策略

贯穿全文的逻辑很简单:可控变量一开始是你写下的文本,直到规模迫使它变成你搭建的架构。

如果这周只做一件具体的事,就挑出最常用的 3 个 Skill,分别运行“10 个应该触发、10 个不应该触发”的测试。20 分钟得到的路由认知,往往比一周盲目修改指令更多。

对常用 Skill 运行十个正例和十个反例

全文可以压缩成一个函数:先确定自己的 Skill 数量,再选择下一步。

def next_move(skill_count):
    if skill_count < 30:
        return "写好描述,然后停下"
    if skill_count < 1000:
        return "增加负向边界和类别层"
    return "构建召回与重排,或使用平台工具搜索"

延伸阅读

下面是一篇与本文标签相关的文章。

真实的 Loop Engineering 是什么样的?

从 Ralph 循环、主流编码框架的 /goal 命令和真实开发案例出发,重新理解循环工程的价值、局限与适用场景。

继续阅读……