你用过 Claude Code 的技能系统吗。

每当你输入一句话,Agent 会从技能库里去找最匹配的那个 skill 来干活。

“生成一份 PDF 报告” → 调用 PDF skill。“把这段话翻译成英文” → 调用翻译 skill。

很丝滑。

但如果我告诉你,社区里已经有快一万个 Claude Code skills 了。而且还在高速增长。

当技能库从 50 个膨胀到 80000 个的时候,Agent 还能选对吗?

阿里巴巴的团队做了一个实验,结果让我头皮发麻。

所有人都在偷懒,而且偷懒的后果非常严重

现在主流的 Agent 框架——包括 Claude Code、Codex——在技能路由这件事上,用的都是一种叫"渐进式披露"的策略。

翻译成人话就是:路由阶段只看技能的名字和描述。真正的代码实现正文,是藏起来的。

为什么这么干?因为省钱。名字和描述加起来几百个 token,而技能正文随便几千行代码,全塞进去 token 直接爆炸。

所有人默认这么做没问题。

但从来没有人认真测过。
在这里插入图片描述
阿里团队干了这件事。他们在 8 万个社区技能上做了对比实验——把技能正文删掉,只看名字和描述,看看准确率还剩多少。

结果是这样的:

  • BM25(传统文本匹配):从 31.4% 直接归零。对,Hit@1 = 0.0%。名字里没有的东西,它永远找不到。
  • 8B 编码器(开源最强的向量检索模型之一):从 64.0% 暴跌到 25.3%。砍掉了六成的准确率。
  • 16B 检索+重排管线(最强基线系统):从 68.0% 跌到 24.0%。

没有一个幸免。

你可能会说,是不是技能描述写得太差了?

团队专门做了验证——只挑描述最详细的那 25% 技能来看,差距仍然超过 26 个百分点。

问题不在于描述质量。问题在于描述本身就不可能替代完整的实现文本。

一个"飞书消息发送"的技能可能只是个 50 行的 requests 封装,另一个可能是上千行的企业级 SDK。描述写成花,也区分不了这两者。

但 SkillRouter 不只是把正文塞进去那么简单

把技能正文全量喂给模型,是这篇论文的第一步。

第二步才是真正有意思的地方——训练。

很多人会觉得,这不就是个 RAG 嘛,向量化 → 检索 → 排序,有什么好说的。

但如果真是这样,随便用个开源 embedding 模型就能搞定,不需要专门训。

SkillRouter 在训练上做了两件事,而且这两个事直接把准确率从"能用"拉到了"好用"。
在这里插入图片描述

第一件事:假阴性过滤

8 万个技能里,有一大堆不同的技能其实干的事一模一样。

“PDF 生成器"和"PDF 创建器"和"PDF Maker”——三个技能,名字不同,代码几乎一样。

训练的时候,如果你要求模型区分"PDF 生成器"和"PDF 创建器"——把它们一个当正样本、一个当负样本——模型就会学乱。

它不知道该关注什么了。

SkillRouter 干了一件事:三层过滤。

第一层,名字去重——名字几乎一样的,直接合并。第二层,代码重叠度——Jaccard 相似度超过阈值的,视为同一个。第三层,语义相似度——embedding 向量太近的,也视为同一个。

三层筛下来,大约有 10% 的训练样本被标记为假阴性,直接扔掉。

效果?准确率直接涨了 4 个百分点。

这不是小优化。在大规模、高重叠的技能池里,假阴性是系统性毒药。

第二件事:列表式重排训练

这是整篇论文里我觉得最精彩的部分。

传统的重排模型怎么训?逐点打分。把一个候选扔给模型,问"这个相关吗",模型答 yes 或 no。简单粗暴。

但问题来了:当你面前放着 20 个看着都挺像的技能,每一个单独问"相关吗",模型很可能给三四个都打高分。这就崩了——你需要的是在候选之间做比较,不是判断每一个单独好不好。

SkillRouter 换了一个方式:列表式训练。一次性把 20 个候选扔给模型,让它学会比较——“这几个人里,谁最合适”。

这个改变有多大?

逐点打分:Hit@1 = 43.3%。列表式训练:Hit@1 = 74.0%

差了 30.7 个百分点。

同一个模型,不同的训练方式,准确率直接翻倍。这不是微调,这是两种完全不同的范式。
在这里插入图片描述

源码是怎么实现的?两阶段管道,6个文件

上面说的是论文里的关键发现。下面拆源码。

整个 SkillRouter 只有 6 个 Python 文件,核心是个经典的「检索 → 重排序」管道:

用户查询 → Embedding(0.6B) → 从8万技能里召回Top-20
                                   ↓
                            Reranker(0.6B) → 精排 → 最终结果

入口在 run_open_model_eval.py,精简后的核心逻辑:

# 第一阶段:Embedding 粗筛
query_embs = encode_texts(emb_model, emb_tokenizer, query_texts, ...)
pool_embs = encode_texts(emb_model, emb_tokenizer, pool_texts, ...)
sim_matrix = query_embs @ pool_embs.T       # 一行矩阵乘法,算余弦相似度
_, topk_idx = torch.topk(sims, k=20)        # 取最相似的20个

# 第二阶段:Reranker 精排
candidates = [pool[idx] for idx in topk_idx]
scores = score_candidates_with_reranker(rr_model, rr_tokenizer,
                                         query, candidates, ...)
ranked = sorted(zip(top_ids, scores), key=lambda x: x[1], reverse=True)

极其直白。

第一阶段的关键:把技能拼成完整文本再编码

Embedding 模型不只看技能名,而是把名字、描述、代码正文拼成一条长文本。

common.py 里:

def format_skill(skill: dict, desc_max: int = 300, body_max: int = 2500) -> str:
    name = skill.get("name", "")
    desc = (skill.get("description") or "")[:desc_max]
    body = (skill.get("body") or "")[:body_max]
    return f"{name} | {desc} | {body}"

注意第三个字段 body——这就是技能的全部代码正文。这是整个方案能跑通的基础。

编码完用 last_token_pool 取向量(检测 padding 方向,取实际最后一个有效 token),L2 归一化,然后就是那一行矩阵乘法。

不是 CLS token,不是什么 fancy 的池化。就是最后一位。

第二阶段的核心 trick:logit 差值打分

Reranker 是个 0.6B 的因果语言模型。但它不生成文本,只打分。

怎么打?

run_open_model_eval.py 里的关键代码:

logits = model(input_ids).logits[:, -1, :]
batch_scores = (logits[:, token_true_id] - logits[:, token_false_id])

logit("yes") - logit("no") 作为相关性分数。

模型被训练成在最后一个 token 位置只能说 yes 或 no。但 SkillRouter 不要最终的回答——要的是它在说 yes 和说 no 之间的倾向程度

差值越大,模型越确信这个技能相关。

prompt 模板用 ChatML 格式,写死了"答案只能是 yes 或 no":

prefix = (
    '<|im_start|>system\nJudge whether the Document meets the requirements '
    'based on the Query and the Instruct provided. Note that the answer can '
    'only be "yes" or "no".<|im_end|>\n<|im_start|>user\n'
)
suffix = '<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n'

<think>\n\n</think> 留了个空槽位,让模型在那里"思考"一下,然后下一个 token 直接出 logit。

不用交叉编码器,不用算注意力矩阵。20 个候选,一个一个过,每个只算一次 forward。

为什么是 0.6B?

两个模型加一块 1.2B 参数,一张游戏显卡就够。

下面这个对比表是我觉得最有冲击力的:

系统 参数量 Hit@1 推理延迟
最强基线 Qwen3 16B 16B 68.0% 2900ms
SkillRouter 1.2B 1.2B 74.0% 496ms

参数少 13 倍。准确率反而高 6 个百分点。速度快 5.8 倍。

还有一件事——微调后的 0.6B 编码器(65.4%)甚至超过了未微调的 8B 编码器(64.0%)。

在这个场景下,任务针对性的训练比堆参数更值钱。

所有推理都在本地。不需要 API,不需要付费,不需要把内部代码发给 OpenAI。

模型在"认真读",不是"字数多所以看的多"

论文里还做了一个注意力分析,挺值得提的。

你可能会想,Reranker 是不是因为技能正文长,所以"被迫"分配了更多注意力?不是的。

团队用注意力热力图证明:模型的中间层会先看技能名字做初步对齐,最后一层才回到正文做最终判断

这是一个有策略的阅读过程,不是被文本长度牵着走。

路由质量对了,弱 Agent 也能变强

最后说一个我觉得最能说明问题的实验。

团队在 4 个编程 Agent 上做了端到端测试:Kimi-K2.5、glm-5、Claude Sonnet 4.6、Claude Opus 4.6。

结果发现,更好的路由确实能提高任务成功率。但提升幅度不一样:

  • Kimi-K2.5 / glm-5:平均提升 +0.89pp
  • Claude Sonnet / Opus:平均提升 +3.22pp

路由质量的提升,对能力越强的 Agent 效果越明显。

这很好理解——弱 Agent 自己都执行不好任务,给再好的技能也白搭。强 Agent 执行能力强,瓶颈恰恰就在"选没选对工具"上。

换句话说,当你的 Agent 够强的时候,路由就是整个系统的天花板。

有一个案例特别典型:任务是"审计项目依赖的安全漏洞"。

基线路由选了一个看起来相关但功能不对的社区技能。结果 4 个 Agent,全部 0/12 失败

SkillRouter 选对了技能。4 个 Agent,全部 12/12 成功

路由对了,从零分到满分。

写在最后

行业里有一个默认假设:技能路由嘛,看名字匹配一下就行了。大家嘴上说着"渐进式披露",其实就是把技能正文藏起来省 token。

这篇论文用数据证明了这条路是错的。在大规模技能池里,正文不仅是重要的——它是决定性的。

而把正文用对(假阴性过滤 + 列表式重排),一个小小的 1.2B 系统就能打趴 16B 的基线。

我们正在进入一个阶段:Agent 的技能不再是 10 个、20 个手写工具,而是成千上万个社区贡献的 skill。当技能多到一定程度,"选哪个"本身就变成了一个需要被严肃对待的系统问题。

下一次你的 AI Agent 在几百个技能里选错工具的时候,你知道问题出在哪了。

不是 Agent 笨,是路由层的所有人都在偷懒。


论文:https://arxiv.org/abs/2603.22455
GitHub:https://github.com/zhengyanzhao1997/SkillRouter

以上,既然看到这里了,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标⭐~谢谢你看我的文章,我们,下次再见。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐