Codex 返回一份 patch,宣布「我已完成所有改动」,结果你发现:一半 call site 还在用旧签名、两个 import 指向已经被改掉的函数、类型能编译但有一个 test 永远跑不完。Patch 没收完——Codex 自己宣布收完了。
这不是「智商」问题,是「完成定义」问题。Codex 在它自己判断「完成」时停手;你不把「完成」写成可度量的标准,它就按自己最浅的理解收手。修法是把「done」绑到 verifier(typecheck / tests / lint / exit code)上——必须先报告这些通过了才能说完。
常见原因
按命中率从高到低:
1. Task 根本没定义 done
「把 getUserById 改成查不到返回 null」——done 是什么?能编?通过现有测试?补新测试?所有调用方都改了?没说的话 Codex 按最浅那条理解。
如何判断:回看 prompt——里面没出现 “done”、“complete” 或某个可度量 verifier(typecheck / tests / lint),那就是 Codex 自己定义的 done。
2. 多文件 patch 中途丢上下文
跨 6 个文件的改动,前 3 个 Codex 还记得清,到第 6 个时,第 1 个文件的细节已经模糊了。service.ts 的签名改对了,但 controller.ts:142 没动——因为它已经记不清 service.ts 长啥样。
如何判断:数「应改的文件数」vs「实际改的文件数」。「应改」>「实际」且漏掉的那些是 session 早期 grep 出来的,就是上下文衰减。
3. 工具(test / typecheck)静默失败、Codex 当成功
pnpm typecheck 2>&1 | tail -10 返回了 10 行看着挺干净的,但真正的报错在 200 行之前。Codex 看到 tail 是空,就判定通过、收工。
如何判断:本地用同一个 verifier 跑全输出。本地 exit code 非 0、Codex 却以为通过——输出截断遮住了错误。
4. Codex 撞到思考预算上限、急刹
某些 Codex harness 每轮有思考预算上限。Codex 感到预算见底就提前宣布「差不多了」、把 patch 一交就走。
如何判断:最后一轮的推理明显比之前几轮短。或者 patch 完成了 80%,最后 20% 缺——这种「时间不够」形状。
5. Codex 在一个模糊调用点上对冲了
改名 getUser → getUserById 全局。tests/fixtures/old-data.ts 里把 getUser 当字符串写在 JSON 快照里——Codex 不确定动不动,跳了,也没说跳了。
如何判断:patch 应用后 grep 一下旧名字。还有匹配且 patch 里没提,就是 Codex 静默跳过。
6. 任务把多个独立关切打包成了一个
「重构 auth、加 password reset、更新文档」——三个 task 塞在一个 prompt 里。Codex 把第一个做完,第二个做一半,第三个忘了,说「完成」。每个子任务里都做到了 80/20,但任务之间没。
如何判断:把你 prompt 里的 bullet 列出来对照,一项被认真做了、其他做了 30%——这种任务本来该拆三次。
最短修复路径
按收益从高到低,Step 1 一步搞定大多数”没收完”。
Step 1:在 prompt 里把 done 绑给 verifier
模板:
任务:[一句话目标]
DONE 的定义(必须三条都过、且无新增 error):
1. `pnpm typecheck` — 报告 exit code + 最后 20 行
2. `pnpm test -- --reporter=verbose` — 报告 pass/fail 计数
3. `pnpm lint --max-warnings 0` — 报告 exit code
说「完成」之前必须把这三条命令的输出贴出来。
任何一条 fail 不要说完成,先修再重跑。
「把输出贴出来」这条最关键——逼 Codex 真去跑命令,而不是脑补通过。
Step 2:多文件改动要求”文件覆盖报告”
加这段:
说完成之前:
1. 列出本任务需改的所有文件。
2. 每个文件给一行 diff 摘要。
3. 确认没漏 call site:
grep -rn "oldFunctionName" --include="*.ts" --include="*.tsx" .
还有匹配就是漏了,去修。
Step 3:你自己跑一次 verifier,别信汇报
Codex 返回后亲自跑:
pnpm typecheck && pnpm test && pnpm lint
echo "Exit code: $?"
exit code 非 0,patch 就是没完成——Codex 怎么说不算。把真实错误贴回去:
Verifier 没过,输出:
[贴]
你说完成了但 typecheck 报上面这些。修,重跑。三个 verifier 都 exit 0 之前不要说完成。
Step 4:按关切边界拆 session
一个任务覆盖三件事就拆三次:
Session 1:把 getUser 改名 getUserById,到处都改完。停。
Session 2:在新签名里加 null 检查。停。
Session 3:补 null 返回的测试。停。
每次 session 有一个能装进上下文的小 done。
Step 5:80% 完成时定向收尾,别重跑整个任务
Codex 收了 80%,剩 20% 不要重跑整任务,定向钉住:
你已经改完 service.ts,但下面这些 call site 还在用旧签名:
- controller.ts:42
- routes/api.ts:118
- tests/fixtures.ts:7
只改这三个位置。改完跑 typecheck。报告 exit code。
Step 6:给 verifier 的输出预算放宽
Codex 因为输出截断漏看 error 时,去掉 | tail -50:
# 差——遮住前面的错
pnpm typecheck 2>&1 | tail -50
# 好——全输出,按 error 模式过滤
pnpm typecheck 2>&1 | grep -E "error TS|^[a-z].*\.tsx?:" | head -100
按 error 模式过滤,不要按行数截——Codex 看到的就是真错误,不是后面的总结行。
预防建议
- 每个 Codex prompt 末尾都带一个「DONE means」块,列 verifier + 要贴的输出
- 永远本地再跑一次——agent 的”完成”不是真相,exit code 才是
- 按关切边界拆 prompt:一个任务 / 一个 done / 一个 session
- 多文件改动要求 grep 一遍旧符号,作为”没漏 call site”的证据
- Codex 80% 完成时定向钉住剩余文件,不要重跑整个任务
- CI 作为最终闸——即便 Codex 自己 verifier 过了,merge 前再过一遍