当评分无法分辨好坏:一个仓库 SKILL 优化实验暴露的评测盲区
你新加入一个 Kotlin 项目。README 告诉你怎么构建,但真正的知识——多平台 source set 不能跨边界引用、模块依赖的方向、@Tool 注解是定义工具的最简方式——这些没写在任何文档里。你只能通过踩坑学到。
当评分无法分辨好坏:一个仓库 SKILL 优化实验暴露的评测盲区
你新加入一个 Kotlin 项目。README 告诉你怎么构建,但真正的知识——多平台 source set 不能跨边界引用、模块依赖的方向、@Tool 注解是定义工具的最简方式——这些没写在任何文档里。你只能通过踩坑学到。
现在把"你"换成 Claude Code。同样的问题,但它连问同事都做不到。
这就是 SKILL.md 文件要解决的事:一份放在仓库里、和代码一起版本控制的 .md 文件,把"只有在这个项目工作过才知道"的知识固化下来。AGENTS.md 格式已经被超过 6 万个开源项目采用,45+ 个 agent 工具支持这个规范。但问题来了——谁来写这个文件?
手写太贵。一个仓库的隐性知识散落在数百个 PR、数千次 commit、无数次 code review 里。于是有人想:能不能让 agent 自己跑任务,从成功和失败的轨迹里自动优化出一份 SKILL?
已有方案的问题:任务太简单,agent 不需要 SKILL 就能秒杀
先看已有的自动合成方案怎么做。GEPA(一个 prompt 优化框架)和 SkillOpt(一个 skill 自演化框架)的思路是:给 agent 一批任务,看它带 SKILL 和不带 SKILL 的通过率差异,用反射 LM 编辑 SKILL,再跑下一轮。
任务从哪来?SWE-smith 是标准答案:往能跑的代码里注入缺陷,让 agent 修。但 JetBrains Research 的团队发现了一个尴尬的事实:
SWE-smith 发布的任务实例,中位数改动 4-7 行,98-100% 集中在单个文件。
而真实 PR 呢?以 JetBrains/koog 为例,中位数 54 行跨 3 个文件,只有 23% 的任务触及单文件。差距是数量级的。
更致命的是饱和。当 GEPA 的作者用 Claude Code + Sonnet 4.5 跑 SWE-smith 的任务时:
- 第一个仓库:无 SKILL 通过率 100%
- 第二个仓库:无 SKILL 通过率 94.8% → 100%
反向 PR 挖掘:从历史变更里挖真实任务
论文的核心方法创新是 reverse-PR mining(反向 PR 挖掘)。思路很直接:
1. 流式拉取仓库的 merged pull request
2. 把每个 PR 的 diff 拆成"实现部分"和"测试部分"
3. 在一个冻结的 base commit 上,反向 revert 实现 diff(把 PR 的修复撤销掉)
4. 跑测试,记录哪些测试从通过变成失败——这就是这个任务的 FAIL_TO_PASS 集合
5. 让 agent 尝试重新修复
关键在于"冻结 base commit"。早期版本按每个 PR 自己的 parent commit 来 forward revert,结果优化出来的 SKILL 描述的是"仓库跨数月历史的不同版本"——模块路径、构建命令、API 全是过时的。agent 照着做就掉坑里。改成在单一冻结 base 上反向 revert 后,SKILL 描述的才是当前仓库。
反向 revert 有三层,从便宜到贵:
git apply --reverse:直接反向应用,只对没漂移的代码有效- 结构性撤销:新增文件删除、删除文件重建
- LLM 重构:给 LLM 看当前文件和 forward diff,让它重建 pre-change 源码,再从 git 重新推导 patch
git apply 干净反向应用,56/100 在 kotest,65/131 在 ktor。但 LLM 重构也有缺陷——它可能 revert 了调用点却忘了背后的代码,引入"未导入符号"和"滞留修复"两种验证门看不到的 bug。一个静态检查在 280 个 koog 任务里抓出 56 个缺陷实例。产出率:大约五分之一的 merged PR 能存活成可用任务。koog 660 个 PR → 119 个任务,ktor 452 个 → 131 个,kotest 保留 100 个。基线通过率 40%-66%,53% pooled——终于给 SKILL 留出了可优化的空间。
配对评分:不测绝对通过率,测相对差异
另一个创新是评分方式。直接用通过率有个问题:agent 不带 SKILL 也能解决一半任务,那它带任何 SKILL 都会白捡这一半的分数。论文改用配对评分:
每个 candidate SKILL 的 rollout,和 seed(空文档)在同一任务上的 rollout 配对比较。
三步比较,第一步能区分就停:
1. 诚实性:改了测试文件、或报告成功但测试矛盾的,直接输
2. 功能正确:通过所有 FAIL_TO_PASS 测试的赢
3. 平局打破:测试通过比例、最终声明的诚实度、diff 大小、工具调用次数
seed 对自己的分数恰好是 0.5,所以 0.5 就是 candidate 要打败的基线。回归在单个任务上表现为低于 0.5。
结果:分数不显著,但文档说服了维护者
三个仓库、两个优化器(GEPA 和 SkillOpt)、每个 200 次评分尝试。结果:
| 仓库 | 优化器 | 测试集增益 | 配对分数 | 花费 |
|---|---|---|---|---|
| koog | GEPA | +1 | 0.525 | $369 |
| koog | SkillOpt | +2 | 0.546 | $401 |
| kotest | GEPA | +3 | 0.554 | $182 |
| kotest | SkillOpt | +1 | 0.519 | $264 |
| ktor | GEPA | +4 | 0.567 | $313 |
| ktor | SkillOpt | -2 | 0.437 | $485 |
原因藏在数字背后:20-26 个 held-out 任务的 split,要检测出效果,candidate 需要在分歧任务里赢下五分之四。69 个任务 pool 起来也要赢三分之二。所有这个领域报告的效果都在这条线以下——包括 gskill 在其最强配置上的结果。
换句话说:一个仓库的历史能提供的任务数,不够把 agent 自身的 run-to-run variance 淹没掉。单次 rollout $0.84,200 次优化跑几百美元,仓库历史和预算都在 SKILL 能被统计证明有效之前耗尽。
但论文最精彩的部分在这里:把文档交给维护者看。
koog 的维护者逐节阅读了两份 SKILL:
GEPA 产出的文档:
- 多平台 source set 的规则:"我们在这里吃了很多苦头,agent 经常不明白发生了什么"——值得保留
- 模块间依赖方向:"非常重要"——值得保留
- 从未提到
@Tool注解:"很奇怪"——缺失 - expect/actual 模式:只说了规则没说团队怎么避免代码重复——不完整
- 维护者结论:"作为草稿我会收下并打磨它"
- "注册新模块是三文件改动":"很棒很重要"——值得保留(项目里没写在任何地方)
- 其余:"大量非常通用的最佳实践描述"——"agent 自己应该知道"——应该放全局 skill 里
| 配置 | #1275 耗时 | #1275 花费 | #1354 耗时 | #1354 花费 |
|---|---|---|---|---|
| 无 SKILL | 15 min | $6.06 | 20 min | $4.85 |
| GEPA | 5.5 min | $2.48 | 7.6 min | $3.10 |
| SkillOpt | 5.5 min | $2.44 | 8.4 min | $4.20 |
评测盲区定律的又一例
这篇论文最深的洞察不是"反向 PR 挖掘"这个方法,而是它暴露的评测盲区:
当评测信号的分辨率低于被测对象的效应大小时,你无法通过评测来判断好坏。
这个盲区有三个层次:
第一层:任务太简单导致饱和。 SWE-smith 的合成任务中位数 4-7 行单文件,强 agent 裸跑就接近 100%。不是 SKILL 没用,是评测无法区分有用和无用的 SKILL。
第二层:任务太少导致方差淹没信号。 一个仓库的 PR 历史能提供的可用任务只有 100-130 个,hold-out 20-26 个。agent 自身的 run-to-run variance 和 SKILL 带来的 4.9pp 增益同量级。统计上无法分离。
第三层:通过率无法捕捉文档的真实价值。 维护者能看出文档里有"只有在这个项目工作过才知道"的知识,实际 issue 解决时间减半——但通过率指标对此视而不见。
这和"omission blindness"(LLM 法官能检测 commission 不能检测 omission)、"标量幻觉"(用标量管理向量)、"CoT 是网不是线"(必要性和充分性几乎不相关)是同一个家族的盲区:评测工具的分辨率不够时,它会系统性地错过真实存在的效果。
工程启示:什么时候这个方法值得用
论文诚实地列出了方法的适用边界:
三个前提条件: 1. 历史 diff 触及的文件在冻结 base 上还存在(否则 reverse-apply 失败) 2. 测试套件在未修改的 base 上通过(否则没有 FAIL_TO_PASS 可言) 3. revert 每个变更能让某些测试从通过变失败(否则任务无法被评分)
两个失败案例:
- JetBrains/tracy:包重命名导致 213 个生产文件在三个月内全部变更,203 个 PR 只存活 5 个任务
- http4k/http4k:700 个 PR 中 150 个能反向应用,但验证后不到 100 个有 FAIL_TO_PASS
实际建议:
- 如果仓库测试覆盖好、PR 历史丰富、测试套件能在 Docker 里跑——可以试
- 如果仓库快速重构、测试稀疏——这个方法不适用
- 不要只看通过率指标——把文档交给维护者审阅比任何分数都靠谱
- 配对评分比绝对通过率更敏感,但仍然不够
我的思考:文档价值与评测信号的鸿沟
这篇论文让我想到一个更普遍的问题:为什么我们这么执着于用标量指标来评估自然语言制品?
SKILL.md 是给人读的,也是给 agent 读的。它的价值在于传递"隐性知识"——那些不写在 README 里、不写在代码注释里、只存在于维护者脑子里的东西。这种价值本质上是一个高维向量:对哪些任务有帮助、对哪些 agent 有帮助、在什么上下文下有帮助。
但我们用通过率来评估它。通过率是一个标量。用标量管理向量,就是用温度计量血压。
论文的配对评分是一个进步——它把"绝对通过率"换成了"相对差异",相当于从"温度"换成了"温差"。但即使是温差,也只是一个维度的投影。维护者的阅读才是真正的多维评估:准确性、完整性、特异性、可操作性、是否过时、是否通用、是否仓库特定。
这和 AI 安全领域的"标量幻觉"是同一个病:把一个多维概念(模型的能力、安全性、对齐程度)压缩成一个标量(benchmark 分数、安全评测通过率),然后用这个标量来做决策。结果就是:你可以通过优化标量来"骗过"自己,但真实的多维效果藏在标量投影不到的维度里。
GEPA 的 SKILL 在通过率上只赢了 4.9pp(不显著),但在维护者眼里"作为草稿超级好",在实际 issue 上时间和成本减半。 这就是评测信号和真实价值之间的鸿沟。
论文没有给出怎么跨越这道鸿沟的答案——它只是诚实地把鸿沟暴露出来。但这个暴露本身就有价值:当你下次看到某个 SKILL/AGENTS.md/prompt 优化论文报告"提升 X 个百分点"时,问自己:这个 X 和 agent 自身方差比,够大吗?和维护者的主观评价比,一致吗?
如果答案是否定的,那这个 X 就不是 SKILL 的价值——它只是评测工具能看到的那个投影。
论文:Skill Issue: Lessons from Optimizing Repository SKILLs for Coding Agents 作者:Mykhailo Kozyrev (TU Munich & JetBrains Research), Andrei Kozyrev (JetBrains Research), Anton Podkopaev (Constructor University Bremen & JetBrains Research) arXiv:2609.12742 仓库:论文未公开代码,方法可在三个 Kotlin 仓库(kotest/kotest, ktorio/ktor, JetBrains/koog)上复现