一份违约风险分析报告,在评测记录里的分数从 0 变成了 88。报告本身一个字都没改,变的是评分器:它原来要求报告里必须出现“低风险特征”这五个字,而这份报告写的是“低风险 / 弱关联特征”。
更值得说的是后半段。报告解锁、交给 Judge 之后,Judge 给出的第一个严重问题是一个真实的统计错误。也就是说,那个错误的 0 分并没有保护我们,反而把一个真正需要修的缺陷挡在了视线之外。
这是 Open Design Agent Gateway 评测里我印象最深的一次纠错。它让我重新想“Agent 变好了”到底需要什么证据:任务有没有完成,内容对不对,评价本身可不可信,以及结果值得付出多少成本。
一个 0 分是怎么来的
Gateway 的评测分两层。第一层是确定性门禁,检查能明确判断的结构条件;只有通过门禁,产物才会交给第二层的 Judge 按量表打分。早期版本里,违约风险分析这道题的门禁是这样写的:
await check("default-risk Markdown", async () => {
const contents = await markdown(workspace, "task_违约风险分析.md");
terms(contents, [
"高风险特征", "低风险特征",
"credit_score", "debt_ratio", "late_payments", "loan_amount", "defaulted",
"样本观察表",
]);
// …至少 3 条编号建议、至少 4 行表格
terms() 做的是精确的子串匹配。Hermes 的报告把第四节写成“低风险 / 弱关联特征”,门禁记下 missing required terms: 低风险特征,直接判为不合格。不合格的产物不会进入 Judge,分数被记成 0。早期门禁(在新标签页打开来源) · 0 分的来源(在新标签页打开来源)
修复(在新标签页打开来源)之后,门禁只检查字段名和样本表这类结构条件;风险解读和“三条建议”的要求移到 Judge 的量表里,由语义评审负责。代码里特意留了一句注释:
// Structural gate only. Risk interpretation and recommendation coverage belong
// to the unchanged semantic Judge rubric, not exact Chinese heading matches.
复查只针对门禁结论发生变化的产物,在副本上重新评审,量表和 Judge 都不变,没有重新生成任何产物。同一份 Hermes 报告因此拿到了 88 分。V1.5 复盘(在新标签页打开来源)
然后是那个转折。88 分的评审意见里,第一条严重问题是:报告在给“灰色地带”客户提建议时,把 21%–31% 这个单变量分箱的违约率,当成了三个条件同时满足的人群的违约率;而那个联合人群实际是 7 人中 5 人违约。这个错误后来成了优化计划里的第一类问题。一个关键词门禁,把质量评审最该看到的东西拦在了门外。
分数由什么组成
Judge 的量表有四个维度:任务完成度 40 分,事实与数据正确性 30 分,成品质量 20 分,证据与执行纪律 10 分。四项分别给整数分,再简单相加,并不是乘上权重的加权分。每一条扣分意见都必须引用可以定位的文件、字段、页码、段落或执行轨迹:量表与提示词(在新标签页打开来源)
required: ["severity", "description", "evidence"],
properties: {
severity: { type: "string", enum: ["critical", "major", "minor"] },
Judge 本身以只读方式运行:codex exec --ephemeral --sandbox read-only,环境变量里名称像密钥、令牌的都会被去掉,提示词禁止写文件、联网和读取工作目录之外的内容。它看到的输入包括任务、量表、门禁结果、产物变化、解析后的 DOCX/PPTX/XLSX/CSV 内容,以及脱敏后的执行轨迹。只读 Judge(在新标签页打开来源)
同一轮复查还记录了另一个数字:一份 Pi 报告从 94 分变成 93 分,差的 1 分全在正确性一项。这不是能力变化,而是同一个 Judge 对同一份产物的评分波动。另一份 OpenCode 报告两次都是 91 分,但各维度的分配变了。总分相同,并不代表评价相同。
所以我现在看到一次分数变化,会先问它属于哪一种:新代码跑出了新产物,还是旧产物被重新检查?用的是不是同一套任务、量表和 Judge?项目的复查脚本在开头写着一条规则:门禁有变化的才重评,没有变化的保留原分,“不从多次评审里挑更高的分数”。复查规则(在新标签页打开来源)
让不同的尝试可以比较
分数能比较,前提是条件被固定下来。评测执行器会为每次尝试记录模型配置的哈希、输入文件的哈希、任务工具目录的哈希和项目提交;冻结批次还在运行前后核对了整个运行时的哈希。条件固定(在新标签页打开来源)
V1.5 前两批全量评测都没有进入最终比较。第一批跑到中途,确认 OpenCode 根本没有加载共享的 Skill:接口传的是 SKILL.md 文件,OpenCode 需要的是目录。第二批运行中,从公开的会话接口能看到 Pi 读取了旧批次的产物记录、复查汇总、旧的 Judge 输出和旧的 PPT;原因是 Pi 会自动加载上层目录的上下文文件,而评测工作区就放在仓库里面。中断与污染记录(在新标签页打开来源)
修复是三件事:Pi 的驱动加上 --no-context-files;每次执行都放在仓库和证据目录之外的临时目录;执行结束后复制归档,并比对前后的文件哈希清单。Pi 上下文开关(在新标签页打开来源) · 执行目录与归档(在新标签页打开来源) 这些措施减少了无关历史进入任务的机会,但目录隔离不是操作系统沙箱。复盘也坦白,污染发生的那一条会话没有保存完整的原始轨迹。
27 次执行与 853 分
第三批冻结后,三个引擎各跑九道题,共 27 次真实 DeepSeek 执行。模型是 deepseek-v4-flash,两路并发,Judge 固定为 gpt-5.6-sol、推理强度 medium;执行、门禁和评分各完成 27 项,0 次重试,0 次失败。WeLink 那道题因为这个环境没有 WeLink 而没有纳入。批次条件(在新标签页打开来源)
报告里的 853/900 是这样算出来的:每道题取三个引擎中的最高分,再把九道题加起来。计算方式(在新标签页打开来源)
harnessScores[harnessId] = numeric.length > 0 ? Math.max(...numeric) : null;
// …
totalBestScore: Object.values(cases).reduce((sum, item) => sum + (item.bestScore ?? 0), 0),
所以它不是任何一个引擎的成绩,也不对应一个真的会自动择优的路由器。三个引擎各自的总分是 OpenCode 841、Pi 828、Hermes 833。这批次是开发阶段的自评,比赛环境里的验证是另外的批次,不能混成一条证据。
同一道题的三个阶段
把违约风险分析这一道题单独拿出来,能看到比总分更多的东西。下表是同一道题在三个阶段的总分:
| 阶段 | Hermes | Pi | OpenCode |
|---|---|---|---|
| 早期保存的批次 | 0(门禁未通过) | 94 | 91 |
| V1.5 复查:同一份产物,新门禁 | 88 | 93 | 91 |
| V1.5 冻结批次:新代码重新运行 | 91 | 87 | 95 |
Hermes 在新一轮里修正了联合比例的错误,却出现了新的严重问题:它用同一批样本的违约率构造风险分数,排序时再用 defaulted 打破平局,最后引用“排在前 12 位的全部违约”作为证据。这是用标签证明标签,也就是目标泄漏。Pi 反而退步了:分析“中风险层”时用的是整个信用分段,里面混着本该排除的红色预警客户,分子分母和它声称的人群对不上。OpenCode 拿到 95 分,只剩几条次要问题。剩余问题记录(在新标签页打开来源)
这道题给了我三条很实际的提醒:门禁上的一个 0 分,可能掩盖一个内容错误;总分相同,维度构成可以完全不同;工具修好之后,错误的类型会移动,从“算错了”变成“推断错了”。
让失败指向一次具体的改进
V1.5 的问题分类揭示了三种性质不同的失败,各自需要不同的改法。优化方案(在新标签页打开来源)
计算口径。 联合条件和单变量比例混用,靠改提示词解决不了。修复(在新标签页打开来源)增加了一个通用的人群统计工具:按显式条件分组,返回每组的样本数、事件数、对应的数据行号和 Wilson 95% 区间。新一轮里,工具返回的数字是对的,模型却仍可能选错人群、引错口径。工具正确,不等于报告的解释正确。
证据使用。 把搜索摘要当成已经读过的正文,需要的是真实的正文读取:保存正文内容、哈希和读取时间。复测中仍有只抽到 77 个、91 个字符导航文本的情况,模型却声称做了全文核验,这项能力还要继续改。
内容与呈现。 中文字体缺失、页脚文字重叠、图表结论写得过强,需要分开检查。只改渲染子进程的字体配置就恢复了中文,说明那是环境问题;但三份演示文稿都保留了“规模差收窄”这个图表上看不出来的趋势判断,那是内容问题。报告中的逐页复核是由 Codex 在 Judge 之外完成的,不是人工评审。
把这三类都归结为“模型不够强”,会丢掉最有用的诊断信息。
之后:把“0 分”改成“未评分”
比赛后期,这次教训被写进了代码。9 月 17 日起,门禁不通过的产物不再记 0 分,而是记为 null,显示为“未评分”;只有不合格数为零时,一个批次才算评分完成。提交记录(在新标签页打开来源) 一个没有经过质量评审的产物,本来就不该有质量分。
同一天的另两份记录很能说明评测还缺什么。Kimi 的第一轮里,一份报告用“违约”来分析违约,却因为缺少字段名 defaulted 没有通过门禁,这一次规则保留了,结果记为未评分。Kimi 记录(在新标签页打开来源) Qwen 的九道题在同一模型、同一 Judge 和同一量表下拿到 836/900,但一次额外的文本复核发现了一处 Judge 漏掉的前后不一致;记录保留了原分,并明确写着这不是同批次比较,排名差距也小到不足以稳定。Qwen 记录(在新标签页打开来源)
Judge 会漏判,门禁会误判,而这两件事都只能在把结果拿出来逐条看时才会被发现。
还缺什么
现有的 Gateway 链路已经比凭印象挑结果可靠:执行和评分分开,条件被冻结,错误的门禁被修正,退步和中断也留在记录里。但要把“这次改动有效”说得更有把握,还缺几件事。下面是我对下一阶段评价的设计,不是已经完成的实验。
- 先定义这次要改善什么。 正确性、完成率还是耗时?先设最低质量线,再看成本,避免一个加权总分掩盖关键失败。
- 保留开发集之外的任务。 开发样例用来定位问题,另留一批没有参与修改的任务检查泛化,并按任务类型分别报告。
- 重复运行,保留所有尝试。 同题、同条件成对比较,报告每次结果和波动。一批九道题给不出可靠的稳定性结论。
- 用人的判断校准 Judge。 先由人独立评一部分材料,再对照模型和人的分歧,加入同义表达、错误引用和诱导评分这类反例。固定 Judge 的身份,不等于证明它判得准。
- 记录成本与价值。 同时记下耗时、费用、重试和人工返工;质量分更高,是否真的减少了工作,需要真实使用场景来回答。
这些想法不是我凭空提出的。Anthropic 的评测文章区分了任务、单次尝试、评分器、轨迹和最终结果,建议代码、模型和人工评分互相补充,并强调重复尝试和评分器的人类校准。Demystifying evals for AI agents(在新标签页打开来源) Palantir 的 AIP Evals 把测试用例、目标函数和评价函数组织成评价套件,支持版本比较和多次运行差异的观察。AIP Evals 概览(在新标签页打开来源) · 评价套件(在新标签页打开来源) 我的项目没有使用 AIP Evals,参考同样的方法也不等于获得了同样的验证强度。
另外两个项目同样要按这个标准看。DevFlow 的检索评测计算 Recall@K、nDCG@K 和 MRR,并统计越权命中;但默认验证用的是人工指定的三维向量和写死的重排分数,真正参与这三项指标的只有两个用例,“混合检索比词法检索高 0.5”是夹具本身决定的结果。它是一个契约回归测试,而不是对真实向量服务的测量。检索评测实现(在新标签页打开来源) · 夹具向量与重排分数(在新标签页打开来源) ISC-Q 这边,我能说明自己如何组织、精炼和纠偏知识,却没有同口径的前后对比,不能声称问答准确率或整理效率已经提高。实践范围
我希望评测最终支持的,是可以解释的工程决定:保留哪项改动,修哪一类失败,哪些结果还不能对外承诺,以及什么时候该停下这一轮优化。V1.5 在达到本轮条件后冻结了批次,把剩下的问题留给有针对性的后续改进,而不是反复运行,直到出现一个好看的分数。
本文回查了 Gateway 比赛阶段的固定提交 4296525 中的评测实现与 V1.5 报告、9 月 17 日之后主干上的评测记录,以及 DevFlow b7903c3 的检索评测代码和夹具。评测的逐项维度记录保存在项目的本地归档中,未公开;文中只引用公开报告里的总分,并概述 Judge 的主要意见。本文写作时没有新增付费模型运行,历史分数都属于开发阶段的自评。