Gavel 深读:别把技能目录塞进上下文,让模型从自己的中间层路由

深读 Gavel 的 glance、verdict 与 product of experts:为什么技能路由既不该挤占任务上下文,也不该完全外包给检索器;并复算索引显存与路由成本。

给 Agent 装 20 个 Skill 时,把名称和简介列进系统提示词很自然;装 4 万个时,同一做法会变成一张 400 万 token 的菜单。把选择工作交给外部检索器可以清空菜单,却又把最懂当前任务的那个模型排除在外。

Gavel 的答案很反直觉:不要让 LLM 在上下文里读目录,也不要另请一个模型选 Skill;从它本来就在计算的中间层状态里,把选择信号读出来。

我在 9 月 15 日的 Digest 里只写了 Gavel 的结论。继续读完方法和附录后,我认为真正值得留下来的不是 90.9% 这个分数,而是一种新的系统边界:模型的前向传播不只产生下一个 token,也可以成为检索、路由和触发的内部接口。

但这个接口并不免费。论文没有把技能索引的显存列进成本表,标准云端聊天 API 也拿不到隐藏状态。本文会把机制、论文报告值和我亲手复算的两笔账分开讲。

名词速查

术语一句话解释
Skill一份按需加载的操作说明,通常包含 SKILL.md、脚本和参考资料
渐进式披露先把所有 Skill 的名称与简介给模型,选中后才读取正文;站内的上下文工程导论讲过这套“先目录、后正文”
hidden stateLLM 每一层为每个 token 算出的中间向量;它不是文字,却压缩了模型此刻对文字的表示
linear projection一个矩阵乘法,把高维中间向量投到更适合比较的小空间
late interaction查询和文档先各自编码,最后才逐 token 比较;既能离线建索引,又比单向量保留更多细节
glance / verdictGavel 的粗筛与复核:先扫全库,再让同一个 LLM 精读少量候选
product of experts把多个证据的概率相乘;在对数空间里就是把分数相加
Hit@1 / R@20第一名是否正确 / 前 20 名里是否包含正确答案
gate实时 Agent 中决定“现在要不要尝试加载 Skill”的触发器;它和“应该选哪个”不是同一个问题

一句话根本约束

Gavel 之所以长成“隐藏状态粗筛 + 同模型精排”两段式,是因为技能路由必须同时满足三个互相拉扯的约束:不能让全库文本占用任务上下文,不能把判断能力完全外包给较弱的独立检索器,也不能为每个新 Skill 重新训练模型。

这句话可以被反驳。如果上下文无限长且注意力不会被无关文本稀释,直接把全部 Skill 放进提示词最简单;如果独立检索器总能和 Agent 一样理解执行状态,就没有必要读取 Agent 的中间层;如果给每个 Skill 做一次完整 LLM 推理近乎免费,也不需要 glance,只做 verdict 就够了。

现实里三条都不成立。于是系统只能先用便宜但近似的信号扫全库,再把昂贵的完整注意力留给短名单。

路由方法使用谁的判断全库文本进任务上下文吗新 Skill 要训练吗最先坏在哪里
渐进式披露Agent LLM名称与简介会进入不需要库越大,菜单越长;正文细节又被简介丢掉
外部 retrieve-and-rerankembedding / reranker不进入通常不需要查询风格、文档类型或执行轨迹变化时容易域外失效
全库逐项问 LLMAgent LLM可不进入不需要每个 Skill 一次前向,成本随库规模线性爆炸
Gavel同一个 Agent LLM选中前不进入每个 backbone 训练一次读出器;新增 Skill 不训练需要隐藏状态、离线索引和额外显存

先拿到整张图:它不是一个检索器,而是一条路由流水线

Gavel 把一次选择拆成四个时刻:安装、粗筛、复核、执行。

flowchart LR
  A[安装 Skill<br/>正文跑一次冻结 LLM] --> B[中间层状态<br/>投影成 token key bank]
  B --> C[压成 epsilon-cover<br/>离线保存]
  D[Agent 正在执行任务] --> E[读取同一中间层<br/>生成 query vectors]
  C --> F[Glance<br/>逐 token 扫全库]
  E --> F
  F --> G[约 9 个候选]
  G --> H[Verdict<br/>同一 LLM 联读 Skill 与任务]
  H --> I[似然 + Yes/No 判断]
  F --> J[Product of Experts]
  I --> J
  J --> K[加载一个 Skill<br/>或拒绝加载]

四个活动部件要分开看:

  1. 路由信号从哪里来:来自 Agent LLM 自己的中间层,而不是另一个 embedding 模型。
  2. 技能库怎样常驻:每个 Skill 安装时跑一次前向,得到可重复使用的 token key bank。
  3. 昂贵推理花在哪里:只让短名单进入 verdict,论文配置平均约 9 个候选。
  4. 什么时候触发:静态 benchmark 直接给出路由点;真实 harness 还要另训 gate。

改变其中一项,系统行为会跟着变:压缩更狠会省显存但提高漏召回风险;候选更多会增加 verdict 延迟;gate 更激进会多加载错 Skill,更保守则会错过本该加载的时刻。

最小数学地基:模型怎样把“一句话需要什么”变成匹配

先不看公式。把一个 token 的 hidden state 想成一张高维坐标卡,卡上同时混着语义、语法和“下一个字该是什么”等许多信息。直接比较两张卡不好用,所以 Gavel 训练两个矩阵:

  • Wq 从任务 token 里读出“我需要什么能力”;
  • Ws 从 Skill token 里读出“我能提供什么能力”。

投影后的两个向量做点积:对应维度相乘再相加。结果越大,方向越接近。一个极小例子:

任务 token「填写表单」: [0, 1]
PDF Skill 的 token「fill_form」: [0.1, 0.9]
Git Skill 的 token「rebase」:   [0.9, 0.1]

点积:0×0.1 + 1×0.9 = 0.9
点积:0×0.9 + 1×0.1 = 0.1

真实向量是 768 维,原理没有变。关键是 Gavel 不把整份 Skill 平均成一个向量。对任务里的每个 token,它在某个 Skill 的全部 token key 里取最高匹配;再让每个任务 token 只给自己最匹配的一小撮 Skill 投票。

这像查技术书时不比较两本书的“平均主题”,而是问:我的每个关键问题,书里有没有一句话能精准接住。少数决定性 token 不会被大量“请、帮我、然后”稀释掉。

为什么读大约 70% 深度的中间层

论文在 Qwen3-32B 的 64 层里选择第 45 个 block 的输出。作者先在 500 个 Skill 上计算 token 状态的矩阵熵:它在这一带从 4.01 降到 1.83,并在 layer 44 触底;再分别训练六个深度的读出器,R@20 也在这里最高。

这个结果有一个容易漏掉的反面:压缩谷底不是天然的 embedding 空间。 不训练投影、直接做余弦相似度时,layer 44 在作者测过的 34 个深度里倒数第二;训练 Wq/Ws 之后才变成第一。隐藏状态里“有信号”不等于“这个信号已经能直接拿来检索”。

附录里更狠的失败实验也支持这一点:直接拿原生 attention 的 query/key 做全库匹配,在 10,123 个 Skill 上 Hit@1 只有 0.001;训练后的 glance 是 0.918。论文作者的归因是,不同序列分别编码后,原生 attention 分数缺少跨文档可比的校准。

Glance:给冻结模型外挂一只没有 value 的注意力头

训练阶段只更新两个 768 × 5120 的无偏置矩阵,总计 7,864,320 个参数;Qwen3-32B 主干保持冻结。训练数据来自 SkillRet 的 51,104 条查询和 9,084 个公开 Skill,目标是让正确 Skill 的 token key 更接近任务 query,错误 Skill 更远。

安装一个新 Skill 时不再训练,只做三件事:

  1. 用固定模板渲染名称、简介和正文;
  2. 跑一次冻结 LLM,取选定中间层的每 token 状态;
  3. Ws 投影并保存为 key bank。

在线时,Agent 原本就要做前向传播。Glance 从同一层顺手取出任务 query,与全库 key bank 做一次大矩阵匹配。论文把它描述为给模型“嫁接一只注意力头”:有 query、有 key,但没有生成 value 的路径,因为它只负责排名。

这里的共同祖先不是新的。它是搜索系统里“候选生成 → 精排”的两阶段级联,也是 ColBERT 式 late interaction 的再实例化。真正的新意是:候选生成与精排不再由两个外部模型包办,而是从 Agent LLM 的两种计算形态里读同一个判断。

为什么还要压缩:一份 Skill 不能每个 token 都永久占一把钥匙

每个 token 存一个 768 维 key,4 万份 Skill 很快就会变成另一种上下文爆炸,只不过这次爆在显存里。

论文观察到,同一段正文常有许多方向相近的 key。它用 farthest-first traversal 保留一组代表点:任何被删 key 到某个保留 key 的欧氏距离都不超过 epsilon。这叫 epsilon-cover

它给出的保证很克制:对任何单位 query,压缩后的最大点积不会虚高,最多比原 bank 低 epsilon。但如果两个 Skill 本来就卡在 top-k 边界两侧,微小下降仍可能让名次互换。论文把 epsilon=0.83 固定在训练分割上,报告约 8.5× 压缩,且下面各 benchmark 最多损失 1.6 个百分点;这是作者实验值,我没有复现。

我复算的第一笔账:4 万 Skill 的 key bank 仍可能吃掉十几 GiB

论文成本附录采用的平均 Skill 长度是 100 token 元数据 + 1,895 token 正文。结合 768 维投影和 8.5× 压缩,可以做一个论文没有列出的条件估算:

skills = 40_285
tokens = 100 + 1_895
dim = 768
compressed_keys = tokens / 8.5

for bytes_per_value in (2, 4):
    gib = skills * compressed_keys * dim * bytes_per_value / 1024**3
    print(bytes_per_value, round(compressed_keys, 2), round(gib, 2))

我实际运行得到:

2 234.71 13.53
4 234.71 27.05

也就是每个 Skill 平均仍有约 235 个 key。若用 FP16/BF16 保存,40,285 个 Skill 约 13.5 GiB;若用 FP32,则约 27.1 GiB。未压缩的对应值约为 115 GiB / 230 GiB。

这是我的条件估算,不是论文报告的部署显存。 论文只明确训练 hidden states 使用 FP32,没有交代在线 key bank 的 dtype、量化、分片和索引开销。它仍揭示了一个工程事实:Gavel 把“菜单占上下文”的成本转移成了“索引占显存/内存”,没有让容量问题消失。

Verdict:只有短名单配得上完整注意力

Glance 很快,但它把 Skill 和任务分开编码。两者真正放在一起时,完整 self-attention 可以做更细的条件推理。逐个复核全库太贵,所以 verdict 只处理 glance 分数距第一名不超过固定 margin 的候选,论文配置平均约 9 个。

对每个候选,它复用安装时 Skill 前缀的计算,把任务接在后面,一次前向同时读两个信号:

  • 任务似然 L:Skill 作为前缀时,模型认为真实任务文字有多自然。若某 Skill 真能解释任务,后面的任务 token 通常会更可预测。
  • 相关性判断 V:在末尾问“这个 Skill 是否提供任务所需能力”,读取 Yes 与 No 的 log-odds,而不是让模型生成一段自由文本。

再加上 glance 分数 g,最终分数是:

S(sx)=g(sx)+αL(sx)+γV(sx)S(s|x)=g(s|x)+\alpha L(s|x)+\gamma V(s|x)

这里的加法不是随意投票。三个分数都被解释成同一个后验概率在不同尺度上的读法:glance 是对比式证据,L 是生成式证据,V 是判别式证据。对数分数相加,指数还原后就是概率相乘,所以叫 product of experts。

它的好处是任何一个专家都能否决一个“看起来像、其实不能用”的候选;风险是三个信号并不真的独立,alpha=1.0gamma=0.025 也仍是验证集上校准出来的汇率,不是普适常数。

消融实验里,只保留模型自报的 Yes/No 判断,SkillRet 的 adjudicated 分数从 0.9460 掉到 0.7170;去掉 V 只保留 g + L,则是 0.9266。至少在作者实验中,“让模型说它觉得相关”是最不可靠的单一裁判,生成式似然和粗筛证据都不能省。

实验到底证明了什么

先看书面任务。下面都是论文报告值,我未复现;adj. 表示当系统选到非标注答案时,再由模型裁判判断它是否同样适用。

BenchmarkGavel raw Hit@1Gavel adj.最强对照 adj.差值
SkillRet0.84590.94600.9077+3.8 点
SRA-Bench0.82230.87340.7398+13.4 点
Eval-Core easy0.69330.94670.9200+2.7 点
Eval-Core hard0.69330.93330.9200+1.3 点

最有信息量的是 SRA-Bench:训练过的 0.6B Skill 检索器从 SkillRet 换到网页式程序文档后,raw Hit@1 只有 0.2462,低于 BM25 的 0.4820;Gavel glance 是 0.6272。它支持的不是“dense retrieval 不行”,而是路由器如果和 Agent 分开训练,查询与 Skill 文体变化时可能先发生域外失配。

论文也没有只报模型裁判分。所有书面 benchmark 上,Gavel 的 raw Hit@1 仍高于对照;作者还盲标了 200 个裁判样本,人类与裁判在“是否给系统记分”上同意 171 个,即 86%。不过裁判判定“系统答案更好”的 65 例里,人类只同意 42 例;这部分明显比“标注 gold 更好”更软。

SkillTraj:真正难的是需求在半路才出现

SkillTraj 含 372 条由 GPT-5.6 Sol 生成、再经 Claude Sonnet 4.6 筛选的模拟轨迹,覆盖四种路由点:

需求从哪里出现样本数Gavel adj.最强对照 adj.差值
用户后续请求1160.96550.8793+8.6 点
工具返回的新证据1060.72640.5283+19.8 点
Agent 自己写出的计划1050.90480.6857+21.9 点
错 Skill 失败后的恢复450.88890.7778+11.1 点

外部检索器在这里必须二选一:吃完整轨迹,会把多个旧话题平均进去;只吃最后一条消息,又可能丢掉关键工具证据。Gavel 已经坐在完整前向传播里,可以给最近 token 更大权重,而不必先把轨迹压成一段检索 query。

但这套数据的生成器、筛选器和 adjudicator 都是模型。它证明“在这套受控模拟轨迹上,信号能从隐藏状态里读出”,还不能证明真实仓库、真实故障日志和真实用户中有同样幅度的收益。

90.9% 不是任务成功率

作者把 Gavel 接进 mini-swe-agent,并另训一个 gate 决定何时尝试路由。在 Skill-Use 的 177 个可执行任务上,论文报告 Qwen3-32B + Gavel 正确加载 Skill 的 trigger rate 为 0.909;同一 Qwen3-32B 用渐进式披露只有 0.011。

这个结果说明“模型原本不会主动停下来读 Skill”可以被外部 gate 修复。它没有说明 90.9% 的任务最终完成,也没有把 gate 的训练算进“只训练两个投影”的口号。路由至少要拆成三项指标:

何时触发  →  选中哪个 Skill  →  加载后任务是否通过验收

Gavel 的主要贡献集中在前两格。第三格仍由 Agent、Skill 内容、工具和环境共同决定。这与别再把 Harness 当实现细节的主张一致:运行时控制层改变结果,却不能替最终 verifier 作证。

我复算的第二笔账:路由费的交点没有看上去那么远

论文按 GPT-5.6 Sol 的输入价格估算:一次 gate 触发平均复核 9 个候选,每个候选读 100 + 1,895 + 239 token,因此:

Gavel 成本 ≈ 每次触发 $0.080
渐进式披露成本 = 100n 个菜单 token × 首次写入价与后续缓存读取价

我按论文给出的原公式复算了三个会话:

def gavel(m):
    return m * 9 * (100 + 1895 + 239) * 4 / 1_000_000

def progressive_disclosure(n, calls):
    return 100 * n * (5 + 0.40 * (calls - 1)) / 1_000_000

for fires, calls in [(1, 10), (3, 50), (5, 100)]:
    threshold = gavel(fires) * 1_000_000 / (100 * (5 + 0.4 * (calls - 1)))
    print(fires, calls, round(gavel(fires), 4), round(threshold, 1))

输出:

1 10 0.0804 93.5
3 50 0.2413 98.1
5 100 0.4021 90.2

第二个场景正好复现论文的例子:3 次触发约 $0.241;100 个 Skill、50 次调用的渐进式披露约 $0.246,交点约 98 个 Skill。

论文还给出简化式 n > 2000m/T。它在调用次数足够大、首次写入价可以忽略时成立;对于 10 次调用、1 次触发,它给出的近似阈值是 200 个 Skill,而原公式是 93.5 个。短会话里不要直接套这个近似。

更重要的是,美元并非真正瓶颈。论文用每个 Skill 100 token 的菜单估算:40,285 个 Skill 是 400 多万 token;即使只允许元数据占上下文的 2%,128K 窗口也只能放约 26 个这样的简介,1M 窗口也只有 200 个。Gavel 省下的是长期占据注意力的菜单,不只是几毛钱。

这篇论文最容易被标题藏掉的五条边界

  1. “冻结 LLM”不等于零训练。 主干冻结,但每个 backbone 要训练一对投影;端到端 harness 还另训 gate。换模型后,hidden state 几何、层数和投影尺寸都会变,索引也要重建。
  2. 标准聊天 API 用不了。 Gavel 要读中间层、复用 Skill 前缀并在旁路批量 prefill。论文给出的云端方案是由模型提供商托管路由器,不是客户端多写一段 prompt。
  3. 技能库越大,显存账越重要。 Glance 的矩阵乘法对全库做扫描;8.5× 压缩解决了很多问题,但 4 万 Skill 的 key bank 仍不是一个轻量附件。
  4. 评测 gold 不完整。 同一种能力在公开库里常有重复 Skill,所以作者引入 adjudication;这修正了错杀,也引入模型裁判偏差。
  5. 触发、选择、执行仍是三件事。 论文主要测前两件。正确加载说明书之后,代码是否正确、文件是否生成、外部状态是否符合要求,必须另验。

还有一个部署问题论文只在 Discussion 里带过:skill 文档需要上传给模型提供商,由它生成和保存 bank。对含内部流程、凭据说明或私有 API 的 Skill,这不仅是算力架构选择,也是数据边界选择。Skills 锁住提示词供应链讲过正文治理;Gavel 又给这条供应链增加了“索引跟哪个模型版本绑定”的问题。

边界卡:什么时候值得做,什么时候先别做

你的场景我的判断先做什么
自托管模型,数百到数万 Skill,能访问 hidden states值得做原型先测 top-20 recall、bank 显存、gate 误触发率
只有几十个 Skill,简介写得清楚暂时不值保留渐进式披露,先修 description 与验收
只能调用闭源聊天 API当前不能直接实现用外部检索器,但把触发/选择/执行三项日志分开
Skill 经常新增,模型很少换Gavel 的安装时索引很合适新 Skill 跑一次前向;版本化 bank
模型频繁升级或 A/B 多个 backbone维护成本容易被低估每个模型分别校准、重建索引、跑固定回归集
Skill 含高敏感内部知识先审数据边界决定 bank 在哪里生成、保存、删除和审计

小结

  • 根本约束:既要借用 Agent LLM 的理解力,又不能把全库塞进它的任务上下文,也不能为每个新 Skill 重训;所以 Gavel 只能把模型计算拆成可离线索引的 glance 与少量在线 verdict。
  • 崩点:最朴素的全菜单方案在 4 万 Skill 时需要约 400 万 metadata token;最朴素的全库精排则需要 4 万次完整前向。两者都在进入生产前就崩了。
  • 共同祖先:它仍是搜索系统的候选生成与精排,只是索引器、精排器和正在执行任务的 Agent 第一次共享同一个冻结 backbone。
  • 可带走的动作:先把现有 Skill 系统的错误分成“没触发、选错、执行失败”三桶。只有前两桶足够大,才值得为隐藏状态路由付模型耦合、索引显存和重建成本。

一句可复述的话:Gavel 不是让 LLM 多读一遍 Skill,而是把 LLM 已经算过、却通常被丢掉的中间状态,变成上下文之外的技能目录。

如果要验证它,第一步不是复现完整论文。拿你现有路由日志的 100 条失败样本,花半小时按三桶重标一次;如果大多数其实是“Skill 已正确加载,但交付没验收”,换再强的 router 也治错了病。

参考来源

产品与工程实践

论文

站内延伸