周毓杰

先修尺子,再量模型:一次代码 Wiki 失败如何变成 Agent Benchmark

Open Nua Engineering

判断一个 Agent 模型“会不会用工具”,最危险的方法,是给它一个简单任务,看到一次成功就下结论。

真实的软件工程任务没有这么干净。模型要理解已有工作区,在受控文件系统里探索,区分源码与生成物,修改一批相互关联的文档,保留来源证据,并在 token、调用次数和时间预算内通过质量门。任何一层出错——模型、provider、Prompt Adapter、工具挂载、预算统计或 grader——最后看起来都可能只是“模型失败了”。

Open Nua 的第一个 Agent benchmark,就来自这样一次真实故障。

一千万 token 之后,我们仍然不知道是谁错了

当时的任务是修订一份 Plugins Code Wiki。旧 Wiki 已经存在,但很多页面没有 sources;结构审阅还留下了两类跨页面要求。Agent 需要读取受控源码,修订骨架,再逐页补齐 evidence。

实际运行却出现了几种完全不同、但表面相似的失败:

  • 模型反复读取一个不存在的目录,工具持续返回空结果;
  • 修正虚拟路径后,模型又可能重复写文件,成功写入却没有增加 evidence;
  • 长任务直到消耗数百万甚至上千万 token 才停止;
  • 远程模型与本地模型的轨迹差异很大,但旧流程无法可靠地区分模型问题和系统问题;
  • 非流式请求使 TTFT 和输出 TPS 不可用,又削弱了性能对比。

第一处关键发现是:目录循环并不纯粹是模型弱。运行时摘要没有把真实虚拟根说清楚,模型是在错误的地图上反复探索。第二处关键发现是:工具返回 success 也不等于任务取得进展;重复写入相同内容,仍然是循环。

问题因此发生了变化。我们不再急着问“哪个模型更强”,而是先问:

能不能把这次失败冻结成一个相同起点、可重复执行、可确定性评分,并且能分层归因的任务?

冻结世界,而不是读取当前工作树

一个有效 benchmark 的第一条规则,是每次运行面对同一个世界。

我们从失败 checkpoint 派生出一个按内容摘要寻址的 fixture,固定了:

  • 107 个 source-only 文件,共 888,238 bytes;
  • 62 个规划 Wiki 页面及其旧草稿;
  • 已有 evidence、缺失 evidence 和结构审阅遗留要求;
  • 每个文件的内容 hash、fixture digest、构建方式和脱敏审计结果。

fixture builder 明确拒绝打包 bundle、缓存、二进制、secret 和宿主绝对路径。运行时也不能读取当前 checkout 来“补答案”;否则两次评测之间的源码变化会污染比较。

fixture 固定事实和质量不变量,但不固定最终中文措辞。不同模型可以采用不同写法,只要来源真实、结构完整、质量门通过。

调用真实产品,而不是另写一套玩具

benchmark runner 只暴露一个很小的运行接口:

runBenchmark({
  caseId,
  modelProfile,
  runType: "qualification" | "formal",
  trials,
  budget,
})

runner 不理解 Desktop 数据库、provider secret 或可观测平台。它把每次 trial 交给一个窄 Adapter,由 Adapter 调用产品中的真实 Prompt、文件工具和代码 Wiki 生成流程。这样测到的是模型在 Open Nua 实际工具协议下的表现,而不是为了 benchmark 临时简化的代理环境。

代码 Wiki benchmark 的事实链

这里最重要的边界有三个:

  1. 源码快照、Wiki 工作区和任务技能都可以读取,正常加载技能不能被误判为越权。
  2. 只有 Wiki 输出区可以写,模型不能修改 source fixture 或 skill。
  3. Trace 和任务运营页面是观察入口,不是评分事实源。即使可观测服务离线,本地 artifacts 仍必须足够完成复核。

每次 run 都留下五类不可变事实:

  • manifest:case、profile、预算和 runtime 身份;
  • events:脱敏后的模型、工具、验证与生命周期事件;
  • grader:确定性违规和各项分数;
  • summary:机器可读聚合;
  • report:人类可读报告。

把一个昂贵长任务拆成三道资格门

最初的 qualification 几乎就是“再跑一遍完整 Wiki 修订”。它虽然真实,却不适合做入口测试:一个不会正确读取源码的模型,也可能先烧掉上千万 token。

后来我们把资格测试拆成三道有序 gate:

  1. Protocol:在隔离的只读探针中,成功发现并读取源码;
  2. Evidence sample:只修复 Host 选定的 5 个页面,同时锁定页面结构;
  3. Focused QA:由 Host 检查声明路径、fixture bytes、内容 hash 和 evidence ledger。

任一阶段失败就停止,不再启动后续阶段,更不会进入三次 formal trial。这个设计同时回答两个问题:模型是否理解工具协议,以及它是否能在一个小而真实的修订批次里收敛。

从故障到有效 benchmark 的构建过程

我们发现“尺子”本身也会撒谎

把任务拆小之后,仍然不能立刻相信结果。评测协议经历了几次重要修订:

阶段暴露的问题修正
长任务基线qualification 混合完整任务,失败代价太高拆分源码协议、5 页 evidence sample 和 focused QA
路径校准合法读取技能被当成越界;阶段内嵌调用没有完整计数分离可读与可写边界,把嵌套调用纳入阶段预算
预算校准高缓存命中仍按 raw total 消耗预算;调用次数比 token 更早拦截使用 provider-aware token budget;调用次数变成宽松安全护栏
评分校准setup 与评分阶段串线;候选模型参与自己的 QA;工具失败可能重复计数setup 不计入评分阶段;QA 改为 Host 确定性审计;工具错误独立记录

其中最容易忽略的是 token 公平性。

OAuth reference profile 按 raw total 计入收敛预算;其他 provider 默认按:

budget tokens = uncached input tokens + output tokens

原始 input、cached input、output、reasoning 和 total 仍然全部保留,实际成本、模型调用、工具调用、wall time 也仍是独立护栏。这个口径不是为了“放水”,而是避免极高的缓存命中把重复上下文当成等价的新成本,同时继续用重复 discovery、重复 mutation 和 no-change write 检测真实循环。

另一个重要变化,是取消“模型给自己打分”。Focused QA 不再调用候选模型,而由 Host 直接验证:

  • 页面声明的 source 和 test 是否位于授权 fixture;
  • 文件是否真的存在,内容 hash 是否匹配;
  • evidence ledger 是否能证明本次实际读取或经 hash 验证继承;
  • 页面结构是否仍然锁定;
  • 是否存在未授权工具、重复调用或无变化写入。

只有这时,PASS 才不再是一句模型生成的文本。

把运行状态也变成产品事实

长任务还有一个朴素但重要的问题:人必须知道它到底在跑什么。

Desktop 在开始和结束时发布明确的 execution 事件;Gateway 保存 execution 级 usage ledger;管理服务把事件投影成任务运营事实;后台页面展示运行状态、模型与工具调用、token、命令参数和下钻明细。

这条链解决了两个早期问题:

  • 进程结束后 execution 仍显示“运行中”;
  • 页面只有随机 execution ID,看不出当时使用的 provider、model、reasoning 和预算参数。

但运营页面仍然不是 benchmark 的最终裁判。页面状态可以修复或重建,本地 run artifacts 才是不可变事实源。

第一轮有效结果:资格通过不等于默认模型

相同 fixture 下,第一轮有效 qualification 得到:

ProfileQualification模型调用工具调用Budget tokensRaw totalCached input 比例Wall
OpenAI OAuth referencePASS1751596,631596,63147.68%131.7 s
DeepSeek 4 FlashPASS478985,6412,609,80197.51%170.2 s

两次有效 run 都是零 grader 违规、零重复 discovery 或 mutation、零越权工具和零 tool runtime error。DeepSeek 的 raw total 很高,但绝大部分来自 cached input;按 profile 约定,其收敛预算只消耗 85,641 tokens。

这足以说明两个远程 profile 都具备承担该任务的基本工具能力,可以进入 formal trial;它还不足以说明谁应该成为 Desktop 默认模型。正式比较仍需要每个 qualified profile 完成多次 trial,观察成功率、方差、成本和性能。

本地 Gemma 候选还没有可与这两次结果并列的有效分数。历史 protocol probe 显示它没有进入源码读取阶段;而任何只产生 Adapter 错误、缺少 execution 和 phase 数据的运行,也不能算模型成绩。这里宁可留下“无有效分数”,也不能把测量失败包装成模型失败。

最终学到的五件事

1. 最好的 Agent benchmark 往往来自真实失败。
合成题容易测到模型是否记得答案;失败 checkpoint 更容易测到它能否探索、修订、恢复并收敛。

2. Fixture 冻结的是世界,不是答案。
固定源码、旧状态、digest 和质量不变量,但允许模型用不同路径和措辞完成任务。

3. 工具成功不是进展。
必须观察输入 hash、目标路径、写入前后内容、evidence 增量和阶段状态。

4. 先验证评测协议,再比较模型。
一次 Adapter failure、自评 QA 或错误路径策略,都不应该进入模型排行榜。

5. Qualification 是准入,不是选型。
它的职责是快速淘汰明显不适合的 profile,把昂贵的正式试验留给真正可能完成任务的候选者。

这只是 benchmark suite 的第一个 case。未来的新 case 仍应遵守同一原则:调用真实产品 seam,冻结事实输入,用 Host 做确定性审计,保留不可变 artifacts,并把模型、provider、工具运行时和编排故障分开归因。

先把尺子校准,再去讨论模型排名。