给一个你从没见过的代码库打分,而且打分的对象还是 AI 写出来的东西——这件事比写测试本身难得多。Evil Martians 为 Rails 基金会的 LLM 基准项目「Agents on Rails」设计了 Stage 3 的整套黑盒评测,结论很直接:**评测框架本身才是最大的风险源**。一个配置不当的基准,测的是它自己,不是被测的代码。他们的做法可以拆成三条可复用的原则:先有一个更快的白盒孪生版本用来校准,再用变异测试找出验证盲区,最后在生产级环境里跑浏览器验证。
为什么白盒测试到第三阶段就不够用了
前两个阶段的题目都带代码级契约:改一个 API、实现一张 feature 工单,题目怎么说,Rails 的单元测试和集成测试就能怎么验。Stage 3 换了个玩法——每一步是一个多步骤的产品故事,下一个会话只能看到上一个 Agent 留下的东西,还要判断它能不能在自己的成果上继续加码。
题目一旦只写产品需求、不写模型和控制器怎么组织,验证就不能再去测这些结构。原文说得很直白:**如果指令没告诉你代码该怎么组织,测试就不该去检查它**。
Campfire 聊天机器人那道题就是个典型。做法是把已有的功能从代码库里删掉,让 Agent 重写一遍——上游实现天然成了黄金参考解。但 Campfire 开源太久,几乎所有模型都能背出代码,于是他们改了需求:不光管理员,任何用户都能加机器人。这一个改动把安全面彻底换了;而 Rails 的测试环境又天然宽松(强制 SSL、日志过滤这些只在 production 生效),只能起一个接近生产的真服务器来验。
第一版 e2e 有多难用
技术栈是 Minitest + Capybara + Cuprite + 无头 Chromium,最初那版基本是把 Rails 系统测试直译过来。结果三个字:**脆、慢、贴实现**。运行时间是分钟级而不是秒级,断言紧贴着某一种 HTML 结构,稍微换个标记就红。评测本身要反复迭代,这些多出来的分钟会滚成几个小时,最后你分不清是模型不行还是测试坏了。
他们的应对不是硬扛,而是造了一个「混合版」:同样的指令、同样的黄金解,但验证大部分留在应用内部,只留少量集成级检查。然后拿黄金解和几个真实模型把同一批场景跑几遍,再用浏览器测试回放,规则是——**混合版能过的,e2e 版也必须能过,除非 e2e 抓到了一个真 bug**。对不上就说明是测试的问题,去修测试助手。
用变异体给测试本身做体检
模型多了以后,浏览器验证还是不够稳——Agent 实现 UI 的创意远超想象,测试反而因为太严而误杀。于是他们不再去堆模型 trial,改用变异测试,把变异体分成两类:
| 变异体类型 | 改了什么 | 期望结果 |
| --- | --- | --- |
| 等价变异体 | 只改 UI,不改行为,比如机器人列表换成表格、「已停用」做成徽章 | 全部测试必须通过 |
| 故障变异体 | UI 看着正常但行为坏了,比如限额只是隐藏按钮、服务端仍能无限建 | 至少一条测试必须失败 |
Campfire 那道题最后沉淀出 13 个变异体(8 个等价 + 5 个故障)。效果是:33 次 trial 里只有 2 次需要重新校准评分器。这一步的价值在于,把「测试写得对不对」从感觉变成了可测量的东西——不然你永远在猜。
从零建应用:锚点从哪来
Campfire 好歹有现成登录流程和页面结构当稳定点,Quizzy(从零写一个现场答题应用)连这个都没有。三条路摆着:A 在指令里塞满可测锚点,B 给 HTML/CSS 原型照着做,C 干脆用 AI 驱动浏览器自动化。C 最贵也最不确定,只适合兜底。
他们选了 A+B 的组合:在需求里加一节 QA 要求,用无障碍语义做归一化——
- 列表每一项各自独占一个 `li` 或表格行
- 开关就用 checkbox
- 确认框用浏览器原生 `confirm()`
再配两三屏 HTML/CSS 原型。指令没有被过度规定,验证又保持确定性。同一个 Quizzy 界面,通过 trial 的实现长得完全不一样,但测试都能认。
五个能直接抄走的 e2e 技巧
这部分是全文最实用的,尤其对写 Playwright、Cypress 的人:
1. **提交后等网络,而不是等文字**:`submit_and_wait` 会把表单提交包一层,监听网络直到那个 POST 真正完成才继续。乐观 UI 更新会让文字先出现,请求却可能后脚被导航打断。
2. **`eventually` 轮询**:没有确定性等待锚点时,wait-try-repeat 是唯一办法,但超时要压到贴近被测系统的水平。
3. **流量回放测鉴权**:录下管理员会话的所有写请求(方法、URL、头、body),换到匿名会话重放,理论上必须全部被拒。greenfield 应用爱用什么鉴权方案都行,这招通吃。
4. **CDP 断网 + 手动关 WebSocket**:Chrome 的 offline 模式不会关掉已经建立的持久连接,必须注入脚本劫持 `WebSocket` 构造函数把实例存起来,断网时自己 close,才能真的测「手机掉线后重连补数据」。
5. **加 `--disable-back-forward-cache`**:前进后退缓存会把旧页面冻结、WebSocket 还开着,服务端永远收不到断开事件,答题应用里的参与者就一直显示在线。
还有一个反直觉的提醒:AI 生成的测试天生防御过度,`submit_and_wait` 的默认超时敢写 30 秒。本地起的服务不可能这么慢。超时按被测系统调,别按模型的悲观假设调——每次失败白等半分钟,一天下来就是几十分钟。
顺手做的一次栈对比:Rails 对 Rust
评测做好了,就能拿来比技术栈。Campfire 那道题上,Claude Opus 5.5 和 GPT-6 Astra 两个栈都能过,分数差不多,但代价差很远:
| Rails → Rust | Opus 5.5 | Astra |
| --- | --- | --- |
| 成本 | $5.67 → $20.52(3.6 倍) | $7.14 → $14.17(2.0 倍) |
| 耗时 | 35 → 85 分钟(2.4 倍) | 27 → 59 分钟(2.2 倍) |
| 输入 token | 8.1M → 55.2M(6.8 倍) | 1.9M → 5.8M(3.1 倍) |
贵在「读」上:输入 token 涨了 3 到 7 倍,输出只涨 1.5 到 2 倍。原因不是语言本身,而是那个 Rust 版本本来就是 AI 生成的「Rails 形状的移植」——代码量约 13 倍,还自己重造了一套 Rails 框架,Agent 得连框架一起学。
真正从零写的 Quizzy 就是另一回事:前沿模型基本与栈无关,Opus 在 Rails 拿满分、Astra 在 Rust 拿满分;但便宜的模型只在 Rails 上过,DeepSeek V4.1 Flash 和 GPT-6 Luna 的 Rust 版本直接失败。框架把 HTTP 头、实时更新这些活都包了,小模型不用自己发明,也就不会漏。

收尾:一份能直接用的检查表
- 写产品需求,不写代码结构;指令越像技术负责人的工单越好
- 先做一个更快的白盒孪生版,用它校准慢的 e2e
- 用变异测试找盲区:等价变异体必须过,故障变异体必须红
- 用失败方式不同的模型做首轮校准,别只用一家的
- 在生产级环境里评分,测试环境太宽松
代码变便宜之后,值钱的是流程和方法论本身。写评测这件事恰好是这套转变的缩影——从写代码,转向设计平台、夹具和验证方法。
内容与图片版权归原作者所有 · 原文: https://evilmartians.com/chronicles/the-agents-on-rails-files-designing-black-box-evals-for-coding-agents