九凤
Loop 实战:Claude Code + Codex 可控闭环

Loop 实战:Claude Code + Codex 可控闭环

从 Maker-Checker 分工、Task Packet、机器门禁到批准证据,搭建一套真正可运行的 AI Coding 工作流。含两个真实缺陷案例(限流语义写反、并发 CAS 误判)和一份可直接抄走的最小施工单模板。

Vibe Coding

「vibe coding 工程化」系列第二篇。上一篇讲了为什么需要 Loop, 这一篇讲怎么搭:Claude Code 和 Codex 分别做什么、两个 AI 怎样协作而不互相自证、 怎么避免无限审查和文档膨胀,以及最小可用的 Loop 到底长什么样。 文末有一份可以直接抄走的施工单模板。

一、一个典型的失败场景

先描述一个你大概率遇到过的场景。

你让 Claude Code 做一个登录限流。它干得很利索:写了中间件、加了配置、补了测试。跑一遍——全绿。它汇报:

已完成登录限流。中间件已接入,测试覆盖阈值触发与不同 IP 隔离,全部通过。

听起来无懈可击。你会怎么做?大概率是合并。

但这次实现里有三个问题,一个都不会让测试变红:

第一,它实现的不是你要的东西。 你要的是「连续失败登录的计数」,它实现的是「按 IP 的全部请求限流」。成功登录也照样扣配额。而且这个桶跟支付等关键端点共享——意味着一个用户正常登录几次,可能把自己的支付额度耗掉。

第二,它顺手扩大了范围。 施工单里明确写着注册和两个密码重置接口「超出范围、待用户裁决」,但实际 diff 已经把三者一并改了。不是恶意,是「这些看起来也该限」。

第三,测试测的不是线上那条路。 测试直接调用了限流中间件函数,没有经过真实的路由注册。它证明了「这个函数在 miniredis 下阈值正常」,但证明不了本轮的路由改动

这三条,我不是编的——它们是同一轮独立审查里的三条 MAJOR。

而如果你问 Claude「这样对吗」,它会说对。不是它撒谎,是它刚刚用同一条推理链推出了这个实现,你让它自审,它只会把那条链再走一遍。

自审是同义反复,不是检查。

二、为什么要 Maker-Checker

银行几十年前就在用这个词:经手人(Maker)和复核人(Checker)必须是两个人。不是因为经手人不可信,是因为一个人对自己刚做的事没有距离

搬到 AI Coding 上,角色是四个:

角色是什么负责绝不负责
Claude CodeMaker按施工单实现、跑测试、汇报进度、按 findings 修复自审批准、扩大范围、擅自提交部署
CodexChecker真实 diff 独立审查、给机器可读结论替代编译器、决定产品风险
裁决者目标优先级、架构语义、风险接受、发布授权在两个窗口之间当二传手
Hook / Guard机器边界路径范围、危险操作、证据完整性判断业务语义

这里有个关键点,值得单独拎出来:

Checker 必须读真实 diff,不能读 Maker 的自述。

如果你把 Claude 的总结复制给 Codex,让它基于总结审查——那道缝就没了。它审的是「Claude 认为自己做了什么」,不是「Claude 实际做了什么」。上面那个限流案例里,Claude 的自述是「测试覆盖阈值触发与不同 IP 隔离,全部通过」,每个字都是真的,但它没说测试绕过了真实路由。

Codex 是怎么发现的?它把中间件的实现、控制器的成功路径、默认配额配置、路由注册链四处摆在一起看,然后指出:成功路径没清桶,且这个桶跟支付端点共享。

这不是「更聪明」,是「读的东西不一样」。

还有一个常见的误解要挡掉:

换个 session 让 Claude 自审行不行?换个模型自审行不行?

不行。只要它读的是上一步的产物 + 上一步的解释,它就还在同一条链上。缝的本质不是「换个模型」,是「换掉输入」——Checker 的输入必须是真实 diff,不是叙述。

三、最小可用 Loop:五个组件

不需要一上来就搭一套完整体系。最小可用的 Loop 只有五样东西:

1. 权威文档 —— 需求、架构、实施计划。它回答「为什么做、目标长什么样」。 如果你现在没有,那就先写一句话的架构不变量也行——但必须有一个不属于本轮、不能被本轮修改的东西,否则 Maker 会连目标一起「优化」掉。

2. Task Packet(本轮施工单) —— 这一轮的唯一目标、允许动的路径、验收标准、明确禁止。 它是执行会话的唯一主线指针。文末有模板。

3. Claude Code 执行 —— 按施工单实现,跑测试,汇报。

4. Guard + 测试 —— 机器边界。 Guard 只回答一件事:真实 diff 有没有走出施工单允许的路径。它不判断代码好不好。 测试回答另一件事:命令的退出码是什么

5. Codex 审查 + 证据包 —— 独立审查真实 diff,输出机器可读结论;批准时冻结当时的 HEAD、diff、测试输出、审查结论。

就这五样。没有看板,没有工时,没有周报。

四、一次完整任务怎么跑

确认目标(人)
   ↓
生成本轮施工单:唯一目标 / 允许路径 / 验收 / 禁止面
   ↓
Claude Code 实现 + 跑测试
   ↓
Guard 检查:真实 diff 有没有越界?   ──越界──► 打回
   ↓ 没越界
Codex 独立审查(读真实 diff)
   ↓
  ├── APPROVE_PHASE ──► 冻结证据包 ──► 交给人决定 commit / 下一 Phase
  ├── REQUEST_CHANGES ─► 只修 BLOCKER/MAJOR ──► 跑回归 ──► 复审(回到上一步)
  ├── ESCALATE_USER ──► 停下,把事实和建议交给人裁决
  └── STOP ──► 立即停止,保护工作树(安全/生产/不可逆风险)

四种结论各自对应明确的动作,而且每种都有「不该做什么」

结论应做不应做
APPROVE_PHASE停止修改,核对证据包,交给人继续修 NIT、顺手重构
REQUEST_CHANGES只修 BLOCKER/MAJOR 和明确要求的扩大范围、重写整个方案
ESCALATE_USER汇报事实、影响、建议、待裁决项执行方自己选架构语义
STOP立即停,保护工作树和环境绕过门禁继续试

「APPROVE 后不修 NIT」这条看着小气,其实很关键:批准态是一个冻结点。你在批准之后又动了代码,那个证据包就对不上了,而对不上的证据包等于没有证据包。

五、真实缺陷案例

讲两个。它们都满足同一个条件:实现看起来正确,测试也通过

案例一:限流语义写反了

就是开头那个。Codex 的原文(脱敏后):

MAJOR — 中间件实现的是共享桶的「按 IP 全部请求」限流,并非「连续失败登录」计数;成功路径也未清除该桶。成功登录同样消耗默认 60 次 / 20 分钟配额,且与既有支付等关键端点共享。当前无法满足「成功后是否清计数、正常用户不被误伤」的验收语义。

这为什么是单元测试抓不到的:测试验证的是「超过阈值会被拒」——这条它做对了。没有任何测试会去问「成功登录之后计数清了吗」,因为那不是限流中间件的常规测试项,那是需求语义

而 Codex 是把中间件、控制器成功路径、默认配额、路由注册四个文件摆在一起才看出来的。

注意最后一句:Codex 没有自己决定该怎么改。它指出「是保留纯 IP 请求配额,还是改为失败计数 + 成功清零,属于需用户裁决的安全产品策略」——这是 ESCALATE_USER,不是 REQUEST_CHANGES

Checker 的职责是发现并定性,不是替你选产品策略。

案例二:并发下的 CAS 冲突被误判

这个更深,出现在同一份设计的第五轮审查:

MAJOR — 候选领取的 CAS 冲突被错误地等同于候选耗尽。 设计规定二次 CAS 失败后按 NONE 收口,而 NONE 会被转成终局错误。但 CAS 失败也可能表示另一个 worker 已经 reserve/dispatch 了一个有效 attempt。正确性契约要求 insert-or-load 既有 attempt,并对处于 DISPATCHING 的活跃 attempt 延迟处理。需先重读活跃 attempt / 既有 claim / 任务状态,选择 join、ACK/drop、延迟或从最新 cursor 继续;只有证明无在途 attempt 且序列确实耗尽,才能返回 NONE

翻译成人话:两个 worker 同时抢同一个候选,抢输的那个把「别人抢走了」理解成了「没有候选了」,于是直接把整个任务判死。

单线程测试永远测不出来。 你得把两个 worker 在脑子里同时跑起来才看得见。

这两个案例合起来说明了 Checker 的价值边界:它抓的不是语法错误、不是风格问题,是跨文件的语义和并发——恰恰是编译器、类型检查、单元测试全都沉默的那一层。

如果你的独立审查只会挑错别字和排版,那这道缝没起作用,该改的是审查提纲。

六、怎么防止 Loop 反过来拖慢你

这是这篇最重要的一节。加了流程之后最常见的失败,不是流程不管用,是流程把人拖死了。

六条硬规矩:

1. 一个 Phase 只有一个目标。 不是「一个 Phase 一个模块」,是一个 Phase 一类问题。施工单里「当前唯一目标」那一栏只允许写一件事,写不下就说明该拆。

2. 一轮实现完成后才审查。 不要写一点审一点。Checker 的成本在「读懂整个变更」,你给它半成品,它只能给你半成品的意见。

3. 最多三轮。 同一个审查提纲超过三轮,通常说明施工单、设计或审查边界有问题,而不是代码还差一点。这时候该回头改施工单,不是继续修代码。

我自己破过这条红线:一份架构设计跑到第五轮才收敛。回头看,该改的是规则——架构设计本来就是在一轮轮反驳里逼近正确的,拿实施任务的三轮上限去卡它是规则错了。所以这条规则现在是「实施任务三轮,设计任务另议」。

4. APPROVE_PHASE 之后不修 NIT。 批准态是冻结点。想修那些小问题,进下一个 Packet。

5. 小任务不走完整 Loop。 读代码、查 bug、改文案、单文件低风险改动——直接干,跑针对性验证,结束。给这种事写施工单是自残。

6. 旁支问题只记账,不切换主线。 实施过程中一定会发现别的问题。四个去处,选一个:

分类什么时候用动作
IMPLEMENT_NOW不修就完不成当前目标,且仍在允许范围内本轮修掉并测试
ACCEPTED_RISK风险明确、当前目标可交付、人已接受记账,不阻塞
LATER有价值但不属于当前目标进下一个 Packet
ESCALATE_USER改变架构语义、范围、优先级或发布风险立即停,给证据和建议

发现旁支问题不等于要立刻修。 这条不写死,Loop 会在第二天变成一个无穷无尽的 TODO 收集器。

最后还有一条兜底信号:连续两轮只在改施工单、审查提纲、台账,没有代码增量——那不是流程严谨,那是空转。 出现这个,降低强度,别加文档。

七、抄走就能用:最小施工单模板

这是全篇最实用的部分。把下面这份存成 packet.md,每轮开工前填一遍:


## 当前唯一目标
<只允许写一件事。写不下 → 说明该拆成两轮>

## 架构不变量(本轮不得破坏)
- <一句话一条。这是 Checker 的判据>

## 允许修改范围
- `src/xxx/**`
- `test/xxx/**`
> 目录写成 path/**,别写自然语言。这一栏要能被机器解析成 allowlist

## 明确不做
- <下一 Phase 的事>
- <发现了但不属于本轮的旁支>
- 不 commit / 不 push / 不部署,除非本轮明确要求

## 验收命令
- guard: <比对真实 diff 是否越界的命令>
- test:  <具体命令,要能给出退出码>

## 需要独立审查重点看的风险
- <这一轮最可能出问题的地方,1-3 条>

几个填写要点,是踩出来的:

「允许修改范围」比「目标」更重要。 目标是一句自然语言,Maker 可以有一百种理解;允许范围是一组路径,机器可以直接比对真实 diff。能被机器执行的约束,才是约束。

「明确不做」不是客套。 上面那个限流案例里,范围蔓延正是因为施工单在「不做」里写了注册接口,却又在「目标」里包含了它们——两栏自相矛盾,Maker 当然选择做

「需要独立审查重点看的风险」不要复制全部历史。 写 1-3 条本轮最关键的问题就行。给 Checker 一百条,它会平均用力;给它三条,它会往深里挖。

验收命令必须能给出退出码。 「跑一下测试确认没问题」不是验收命令,go test ./... -run TestXxx 才是。

结论

双模型不是为了多一个模型。

Codex 不比 Claude 聪明,Claude 也不比 Codex 差。上面那些缺陷,换成两个 Claude、或者两个 Codex,只要满足这三条,效果一样:

  • 职责分离:Maker 不批准自己的产出;
  • 机器门禁:关键约束是能返回非零退出码的东西,不是一句「请遵守」;
  • 证据闭环:批准的不是「两个 AI 都说没问题」,而是真实 diff + 退出码 + 独立结论被冻结在一起。

Loop 的产物不是一句「通过了」。

是一个可验证、可恢复、可审计的批准态。


下一篇:主线治理——为什么 AI 总会偏离架构目标,以及「当前唯一目标」这一栏该怎么写才拦得住它。