GPT-6 出了 Sol 和 Luna,我测了三轮才把 codex 的活分下去
起因
我现在用 codex 大概是这么用的。一个主 agent 当 coordinator,负责弄明白我要什么、拆任务、最后验收。它下面会派一堆 subagent 去干具体的活,比如翻代码找东西、照定好的方案改代码、盯着一个具体的风险做 review。每个 subagent 都能单独指定模型和 reasoning effort。effort 就是让模型每一步想多深,档位从 low、medium、high 一直到 xhigh 和 max。
之前我图省心,所有 role 全部用 GPT-6 Astra 配 high,coordinator 是它,翻代码的是它,改一行配置的也是它。这样确实省心,但用量烧得非常快。很多活其实用不着 Astra,比如列出某个函数的所有调用点、照着现成的写法改几个文件,让最贵的模型去干这种活,有点浪费。
GPT-6 这次给 codex 加了 Sol 和 Luna 两个模型,都比 Astra 轻,也比 Astra 便宜。官方文档建议 subagent 从 Sol/medium 和 Luna/high 起步。我想要的其实就三件事,活干得快一点,没必要的 token 少花一点,干出来的质量不能掉。把一部分活交给小一点的模型,算是少花冤枉推理钱的一个办法。但单价便宜了,token 也不见得就少,整体也不见得就快,最后省不省,还要看它输出多少、重试几次,coordinator 那边又多出来多少活。第三件事更没人能替我打包票,所以还是得自己测。
最开始我先拿一个很小的活试了试,让 Luna/medium 和 Sol/high 各改一遍同一个脚本,两边都过了事先写好的 20 个检查。但这活太小了,只能算冒烟测试,看不出它们接不接得住更复杂的活,所以才有了后面三轮。
有件事先说在前面,这篇里没有耗时、token、成本的数字。你在 codex 的 subagent 工具里指定模型和 effort,它会照收,但它没法另外给你一个凭证,证明后端跑的真是这个模型,也没有靠得住的单次计费数据。所以我能测的只有一件事,同一个任务交给不同的模型和 effort,最后交上来的东西对不对。省了多少钱、快了多少,得实际用一段时间才知道,而且还要把 coordinator 返工和 review 花的成本一起算进去。
怎么测的
一共七条路线,Astra/high 当对照,Sol 和 Luna 各跑 medium、high、xhigh 三档。每一轮的做法都差不多:
- 每条路线开一个全新的 agent,不带任何历史对话,明确指定模型和 effort,在一个独立的 git worktree 里干活。worktree 可以理解成同一个 repo 的另一份工作目录,改动互不干扰,跑完直接删掉。
- 七个 agent 拿到的任务描述一模一样,写清楚要做什么、能改哪些文件、不能碰哪些文件、要交什么报告。
- 评分用的测试在派活之前就写好冻结了,agent 看不到,这个一般叫 hidden test。交之前 agent 可以自己写测试、自己 debug、自己反复改;但交上来以后,hidden test 的结果不会告诉它,也不给它再修一轮的机会,交上来什么就评什么。
- agent 自己报告的「测试全过」不算数。评分只看冻结的测试、原有的回归测试,还有评测代码实际跑出来的值。
跑评测、读 diff、判分这些,都是 codex 的 coordinator 干的,我没有逐行去看代码。我这边管的是定方向和追问。我嫌测试不够硬,就让它加强,后来又让它拿真实的 PR 测;我也专门问过它,有没有去看实际跑出来的值。
每条路线每轮只跑一次,所以它不是一个能在统计上衡量可靠性的 benchmark。每个格子里就一个样本,能看出某种活在某个配置下出过什么问题,但看不出出问题的概率有多大。三轮各回答一个不同的问题,下面一轮一轮说。
第一轮:一个大 monorepo 里的三道题
第一轮用的是我自己一个产品的 monorepo,大概七千个 tracked 文件。三道题是连着的,都围着同一个流式对话的接口:
- 追代码:讲清楚一次流式回复断线以后是怎么恢复的,从服务端写入、鉴权、解析、重放,一路到结束通知。评分按事先定好的 12 个事实点来。
- 写实现:给一个解析 SSE 流的函数加上取消功能。SSE 就是服务端一条一条往下推事件的那种流。要求很细,各种换行符、半截的行、被切在两个包之间的中文和 emoji 都要处理对;用户不管什么时候取消,都要干净地停下来,把锁和监听器释放掉。冻结的测试有 29 个,另外还要过 42 个原有的回归测试和类型检查。
- 做 review:给一个还没合进去的 patch 做 review,里面事先埋了 3 个 bug,每个都要说清楚具体怎么触发、会有什么后果,不能只说一句「这里可能有问题」。
| 路线 | 追代码 | 实现 | 补充的 EOF 取消用例 | review 抓到的埋点 | 改动范围 |
|---|---|---|---|---|---|
| Astra/high | 12/12 | 29/29 | 过 | 3/3 | 守住 |
| Sol/medium | 12/12 | 29/29 | 挂 | 3/3 | 守住 |
| Sol/high | 12/12 | 29/29 | 过 | 3/3 | 守住 |
| Sol/xhigh | 12/12 | 29/29 | 挂 | 3/3 | 守住 |
| Luna/medium | 12/12 | 29/29 | 挂 | 3/3 | 守住 |
| Luna/high | 11/12 | 29/29 | 挂 | 3/3 | 越界 |
| Luna/xhigh | 12/12 | 29/29 | 挂 | 3/3 | 守住 |
光看冻结的测试,七条路线全过,42 个回归测试和类型检查也都过了,review 也没人报假 bug。结果这么整齐,看起来题出得有点松。
后来 coordinator 把交上来的源码一份一份读下去,发现有个边界情况大家处理得不一样。流结束的时候,缓冲区里可能还剩最后一条没换行的事件,解析器会把它 flush 出来交给调用方。如果调用方拿到这条以后取消了,下一次再去要数据,就应该收到一个 AbortError。任务描述里写了这个要求,连「最后一条没换行的事件」这条路径都点了名,但冻结的 29 个测试刚好没测到。
所以 coordinator 看完代码以后补了一个用例,七条路线都跑了一遍,也没给任何一条修的机会。结果只有 Astra/high 和 Sol/high 过了,另外五条都漏在同一个地方。它们在交出最后一条事件之前查了一次取消,交出去再回来就没再查,所以下一次调用直接正常结束了。
这里有两点得说清楚。第一,这个用例是看完代码才补的,不是事先冻结的,所以单独列了一栏,没混进那 29 个里。第二,Sol/xhigh 挂了、Sol/high 过了,也说明不了 high 就比 xhigh 强,因为就一个样本,分不清是 effort 的原因还是运气。能看出来的只是,effort 往上调了,这个漏洞也没有跟着自己补上。
Luna/high 还有两个问题。追代码那题,它说 repo 里没有专门测这段流式消费逻辑的测试,其实有,一个文件里就有 26 个测试。另外,任务里写明了只能新建测试文件,它却把新测试加进了一个原有的测试文件。加进去的测试倒是没有削弱任何断言,但还是越界了。它自己没搜到,东西不见得就不存在,这一条后面会变成一条派活规则。
第二轮:三个 repo 的跨项目题
第一轮的三道题都在同一个子系统里,覆盖面太窄了。第二轮换成工作上的三个 repo,一个前端平台、一个 backend、一个放数据库 schema 的 repo,出了四道题,有几道要跨 repo 改:
- 前端图表的数值裁剪,39 个检查,其中一条是真实值和画出来的值要分开。
- backend 一个异步任务的取消原因要原样传下去,27 个检查。
- 前端 TS 和 backend Python 之间一个按日期筛选的接口契约,新老版本各种组合都要兼容,64 个检查。
- backend 加一个新的类型化字段,同时要在数据库 schema 里加迁移,39 个检查,其中包括「不相关的源码不能动」这种范围检查。
这一轮结果很整齐。七条路线功能全过,原有的 204 个回归测试每条都过,限定范围内的类型检查也都过了。只有 Luna/high 和 Luna/xhigh 在第四题扣了一分,拿了 38/39。这一分扣在范围检查上,它俩都改了原有的测试文件,SQL 本身没问题。
但这个范围问题里有个干扰,我得老实说。四道题各自的描述里提到了原有的测试文件,可总的任务说明里写明了只能新建测试文件,两处是冲突的,以总说明为准。所以这两个结果只能说明,在这两次运行里,它们没处理好指令之间的冲突。要说 Luna 平时就不守范围,或者 medium 比 high 好,这两个结果都撑不起来。第三轮就把测试该放哪的说法统一成了一种。
我专门问过一句,有没有去看实际跑出来的值,光看测试过没过是不够的。coordinator 把每条路线的实际输出都记了下来,也都看了。比如图表里,真实值 -90 画在 -10 的位置,真实值 45 画在 30,差值 35 也留着;123456789.123456789 这个小数从 TS 传到 Python 再传回来,一位都没丢;从数据库读回来的时候,老数据的 null 和新数据的 0 也分得开。这些值都是评测代码自己跑出来的,不是 agent 在报告里写的。
中间还出了两个跟模型无关的小事故。一次是机器负载太高,有个测试撞上了 5 秒的默认超时,源码和断言都没动,统一把超时调到 30 秒重跑就过了。另一次是评测这边统计输出的脚本少数了一行 ok,因为那一行被异步的诊断信息切开了。测试框架自己的汇总本来就是全过,要修的是统计脚本。这两次都不算模型的问题。
跑完第二轮,我对 Sol 写实现放心了不少。需求写清楚、边界划好的活,七条路线全都能做对。但这一轮也有个明显的短板,题是专门出的,需求非常明确。
第三轮:盲测复现历史 PR
第三轮是我最在意的。第二轮跑完我还是觉得测试不够硬,专门出的题需求太干净了,所以让它拿真实的历史 PR 再测一次。最后从工作上挑了三个 PR,合成两道题。repo 退回到修复之前的那个提交,agent 只拿到当时的问题描述。修复之后加上的那些测试藏在 agent 看不到的地方,等它交完才拿进来跑。agent 也不许翻历史提交,不许看原来的 PR。每条路线有 32 分钟的有效工作时间。
第一题在前端。页面跳转的时候要保留状态,但换一个账号登录以后,上一个账号的资料和问卷不能漏给下一个账号。同时,匿名用户第一次登录前填的草稿要保住,同一个账号刷新页面,状态也要保住。原来的修复带了 9 个测试。评测先看了看这 9 个测试能不能把修好和没修好分开。修复前的代码能过 5 个;只打一半原修复,反而只过 4 个,因为状态一持久化,账号串号的问题暴露得更明显了;完整的原修复 9 个全过。
第二题跨数据库和 backend。一个权限受限的数据库账号可以读 session,但一去刷新最近活跃时间就报权限不足,问题出在一个审计触发器上。要做的是给触发器一个有边界的权限,同时普通账号还是不能读写审计记录本身。修完以后,还要把 backend repo 里的迁移快照、快照的校验值、跑集成测试的 runner 一起同步好。runner 要真的建库,跑到新迁移,再跑 backend 原来的测试。所以这题要改的东西横跨两个 repo,改完还得真的跑通一遍。
| 路线 | 前端 9 个原测试 | 数据库迁移、行为、权限 | runner 中的真实调用路径 |
|---|---|---|---|
| Astra/high | 9/9 | 过 | 过 |
| Sol/medium | 9/9 | 过 | 挂,backend 查询报权限不足 |
| Sol/high | 9/9 | 过 | 过 |
| Sol/xhigh | 9/9 | 过 | 过 |
| Luna/medium | 9/9 | 迁移自检没过,后面没跑到 | 迁移没过 |
| Luna/high | 9/9 | 过 | 挂,兼容视图更新返回空 |
| Luna/xhigh | 9/9 | 过 | 挂,兼容视图更新返回空 |
前端那题七条路线全过。数据库那题,SQL 本身修对了的有六条,但在 agent 自己的集成 runner 里,连 backend 真实的调用路径也跑通的,只有 Astra/high、Sol/high、Sol/xhigh 这三条。
我最想说的,是没做完的那几份是怎么交上来的。Sol/medium 自己给新迁移写了一个测试,测试里直接调了一个 SQL 函数,跑下来是绿的。但 backend 原本的代码不是这么调的,它是通过 repository 那一层去查 session 的。拿原来的 backend 测试在它自己建的库里一跑,直接报权限不足,因为那条路径要的几个授权它漏掉了。
Luna/xhigh 的测试也是绿的,但它是直接去更新底表,还专门加了一条策略,只对一个写死的测试钱包生效。backend 真实的代码走的是一个兼容视图,根本不直接碰底表。所以它的绿灯只能证明它自己搭的那条路是通的,证明不了问题修好了。拿 backend 原来的测试一跑,兼容视图的更新直接返回空。
自测走捷径全绿,真实调用方走兼容视图被权限拦住
另外两份 Luna 反而挺老实。Luna/high 在报告里自己说了只完成了一部分,它的 runner 搭出来的兼容视图不完整,更新返回空。Luna/medium 的迁移在自检那一步,把 PostgreSQL 自己保留的一条成员关系也当成违规拒掉了,整个迁移就停了。它在报告里也写了没做完,只是对剩下那条关系的描述有点偏。说实话,这种直接告诉我没做完的,比一份看着全绿其实没修好的,处理起来省心太多了。
这一轮评分这边也出了问题,得交代一下。评分用的 runner 适配层是照着原来那个 PR 的写法写的,它默认环境变量名是固定的那一个,也默认 backend 测试在一个阶段里一起跑完。Astra/high 和 Sol/xhigh 给受限账号单独开了一个连接,这么做很合理,结果适配层把它当成了 owner 连接,判了失败。Luna/high 把 backend 测试拆成两个阶段跑,这么写本身也没问题,适配层一样不认。
这几处的锅在评分器,不该算在实现的写法头上。所以后面补了一个兼容的适配层。它先确认 agent 给的那个连接真是一个没有特权的受限账号,再把它接进原来的测试夹具里;如果测试拆成了几个阶段,就把各个阶段的结果加起来,三个原测试都过了才算过。agent 的 SQL、源码和原测试的断言一个字都没改,七条路线统一用这个适配层。原修复用它照样能过。另外还有一个对照组,快照已经修好了,但 runner 还是老版本的,没有新版本的测试阶段,它虽然退出码是 0,照样判失败。原来那份误判的结果也留着。这个兼容处理是交完之后才补的,不是事先冻结的,所以这里单独说一下。
还有几处跟代码对错无关的小问题。Sol/medium 和 Luna/medium 把报告写在了 repo 根目录,没写到指定的位置;Luna/xhigh 更离谱一点,把报告写到了自己 worktree 外面的主 checkout 里。这些都是文件放错了地方,没弄坏源码,所以单独记了一笔,没算进上面的表。
两份 xhigh 的提交,在前端那题都多做了一件事,把用户资料的本地存储改成了按账号隔离的格式,老数据下次登录时就丢掉。Sol/xhigh 在报告里说了这一点。9 个原测试照样全过,所以不算错,但原修复里没有这个产品决定,得有人去 review 一下该不该这么做。这次两份 xhigh 提交都多做了这一步,但样本就两个,没法说 effort 开高了就一定会这样。只是多做的那部分,还是得有人去看。
最后说一下实际都看了什么,这部分也是 coordinator 做的。每份报告、前端完整的 diff、数据库的迁移、backend runner 相关的 diff、所有失败的输出和实际记下来的值,它都读了。runner 没跑通的那几份,它还去看了自测的源码,确认到底是在哪条路径上走偏的。但它没有把每一份自测代码、每个文件都逐行审一遍。
最后怎么定的
三轮下来,我把 codex 的分工改成下面这样:
| 活 | 模型 / effort |
|---|---|
| coordinator,理解需求、拆任务、最后验收 | Astra/high |
| 权限、鉴权、架构、原因不明的 debug 这类后果重的判断 | Astra/high |
| 边界清楚的实现、跨文件集成、盯一个具体风险的 review、有边界的调研 | Sol/high |
| 有边界的代码探索 | Sol/medium |
| 源码清点,比如列出所有调用点,要带文件和行号 | Luna/high |
| 精确的机械改动,有客观检查 | Luna/medium |
| 材料全给好的纯提取、格式化 | Luna/low |
纯机械的活,如果现成的脚本或工具就能做,就直接用工具,不用派 agent。xhigh 和 max 不当默认,也不拿来做「没做好就往上加一档」的自动升级,只在说得出具体难点的时候才用。因为三轮下来,xhigh 并没有稳定地比 high 做得好,还多带出来一些得 review 的决定。
表之外还有几条派活的规则,我觉得比表还要紧一点:
- 每个 subagent 的任务要写全:要什么结果、看哪些资料、能改哪些文件、交什么证据、什么情况下停下来回来。给一个不带历史的 agent 派活,为了省 token 把约束省掉,是亏的。
- 混用模型要用不带历史的 fork。在我这次用的 codex 原生 subagent 工具里,带着完整历史 fork 出来的 subagent 会继承父 agent 的模型和 effort,也不接受单独指定模型。想明确换成 Sol 或 Luna,就别带历史,或者只带有限的几轮。这是这个工具现在的行为,别的接口不一定一样。另外,每个 role 的配置文件里也可以单独写死模型和 effort,所以只改全局默认值是不够的;已经在跑的 agent,也不会因为你改了配置就换模型。
- 别让 Astra 把便宜模型干过的活再干一遍。派出去的活,如果 coordinator 回头又从头查一遍、从头写一遍,就等于付了两次钱。要看这么分活到底省没省,得把 review、返工和协调的成本都算进去,光看单价没意义。
- 搜不到,不见得就没有。「repo 里没有 X」这种结论,要附上它搜了哪些范围。第一轮 Luna/high 说没有的那个测试文件,其实就在那里。
- 验收要走真实的调用方路径。模型自己写的测试全绿,只能说明它自己搭的路是通的。第三轮两份没修好的提交,自测都是绿的。
不同用法怎么选
这是按我自己的用法测出来的,换一种用法,答案可能不一样:
- 一个 session 自己干到底,很少派 subagent:主 agent 用 Astra/high 就行,这篇大部分内容跟你关系不大。
- 大量派 subagent、用量烧得快的(跟我一样):可以参考上面的表。先把翻代码、清点、机械改动这类活挪到 Sol/medium 和 Luna,写实现挪到 Sol/high,coordinator 和验收留在 Astra。
- 涉及权限、钱、数据库迁移的:判断留给 Astra。Sol/high 可以去实现一个已经定好的方案,但方案本身和最后的验收,不要一起外包出去。
- 想省心直接全开 xhigh 的:不太建议。这几轮里它并没有做对得更稳,两份 xhigh 提交还都多做了没要求的改动,你还是得去看。
限制
最后把限制列一下:
- 每条路线每轮只跑一次,三轮加起来的这些题,也不是从所有工程任务里随机抽出来的。所以拿它估不了失败率,也分不清哪些是 effort 的影响,哪些是随机波动。
- 没有后端模型的独立凭证,也没有单次计费数据,所以提速、省 token、省钱这些,都没有测出来的数字。换便宜模型是为了省用量,这是预期,实际效果得用一段时间再看。
- 第一轮那个 EOF 取消用例是看完代码补的;第三轮的 runner 适配层是交完之后补的兼容处理;第二轮的范围问题里掺着指令冲突的干扰。这些上面都单独标出来了。
- 题目的需求都写得比较明确。需求模糊时的排查、架构设计、跑几个小时的长任务、线上安全这些,这次都没测。
- 三轮分别开了 8、27、27 个 worktree,全都删掉了,各个 repo 的主 checkout 跟测之前一样。实验里写的业务代码,一行都没提交,也没部署,最后落下来的改动是 codex 的分工配置。
后面如果有新模型出来,或者我想把某类活再往下挪一档,这套流程可以直接拿来再跑一遍,只是每次都得再确认一下,评分器测的是不是真实的那条路径。