核心观点 写代码的 agent 不能审自己的代码。独立审查是缺陷门禁,不是设计评审:另一个 agent 只报缺陷与严重级别,findings 逐条处置,有采纳就复审,high 级清零才许合并。审查范围、收口标准、降级规则都要在开工前写死,否则「等得久」「改动小」会成为跳过审查的理由。
一个 agent 写完代码,它对这段代码的理解来自它刚才做的事。让它回头审查,它看到的是自己的意图,不是代码实际做了什么。这与人的情况类似,但对 agent 更严重:它的上下文里就装着「我为什么这样写」,而缺陷恰恰藏在意图与实现的缝隙里。
Agent X-Ray 的做法是把两件事交给两个独立的 agent:Claude Code 实现,codex 审查。CLAUDE.md 用一句话定了这个分工:「Claude Code solo 开发,codex 独立审查;不做视觉 review」。

实现 agent 交出代码,审查 agent 交回 findings;循环直到 high 级清零,门才打开。
「独立」有三层含义:
第三点容易被忽略。项目里实测过:codex 只认仓库级的 .agents/skills,不认 .claude/skills。漏了镜像同步的表现不是报错,而是审查悄悄退回到旧的清单——少查东西,没人知道。所以 dev.ps1 skills 这条命令存在,CLAUDE.md 也写了「skill 升级或新增后必须重新同步镜像」。
审查循环写在 CLAUDE.md「开发模式与轮次流程」里,展开是六步:
第 4 步是这个循环与「审一次就合并」的根本差别。整改本身会引入新缺陷——项目里多次出现「复审的 findings 全部指向上一轮整改本身」的情况(R3 的复审第 1 轮 3 条全是这样)。不复审,整改就是没有被验证的改动。
什么时候可以停?所有者在 2026-08-28 裁定了收口标准,写在 CLAUDE.md:
第三条是有来历的。R1 是 spike 轮,代码允许粗糙;但 codex 的第一条 P1 findings「SSE 脱敏应为白名单而非黑名单」被采纳了,理由是安全文档是强约束,不因为「这是 spike」而推迟。对抗式复审又抓到两条(富字段递归复制未知结构、无单事件总量上限),也都改了。spike 允许粗糙的是产品完成度,不是安全底线。
审查范围是一个成本与覆盖面的取舍。所有者 2026-08-31 的裁定:

前两轮审整条分支的全量 diff,第三轮起只审上一轮整改后的 diff。
为什么这样定:一条上百个文件的分支全量重扫一轮要十分钟上下,而第三轮起复审的职责只是「确认整改本身没引入新缺陷」。把范围收到整改 diff,既符合流程原本的措辞(对整改 diff 再发一轮复审),也避免审查器每轮在同一批未改动的代码上重新起意。
代价要认:整改 diff 之外的问题这几轮不会再被扫到。所以前两轮必须是全量,那是覆盖面的来源;第三轮起是门禁,不是覆盖。
这条裁定在 R5 第 7 轮首次按新范围执行,那一轮零 findings 收口。之后每轮任务卡的「代码审查」段都会注明第几轮起收窄了范围。
每一条 findings 都要有一个明确的去向,写在任务卡里:

采纳就整改;不采纳要写理由;低危的记 BACKLOG。所有者可以推翻审查者,但同样要留痕。
采纳整改。 大多数 findings 走这条路。整改要写清改了什么、为什么这样改。R6 的一条 P1「清空 baseUrl 时旧配置不会被撤掉」,处置记录里写了三件事:核实(引用 pi 源码注释「重新注册会把定义了的值合并到之前的注册上、保留未定义的」)、后果(所有者以为撤掉了中转,key 与 prompt 还在发往那个端点)、整改与实测(改为先注销再按新配置重建;用桩中转实测:配上端口后新会话命中一次,清空后命中数不再增加)。
不采纳,写明理由。 两种情况。一是误报:R6 第 2 轮「Caddy 的正则引擎不支持非捕获组,生产会拒绝启动」——用真 Caddy 跑 validate,再起容器实跑六条路径,全部正确;不采纳,并把这次实跑当作配置的验收证据留档。二是所有者裁定不修:R-VISITOR 的「预发环境 MCP 走明文 HTTP」,所有者裁定边界仅限内网预发,生产不适用,记 BACKLOG。
记 BACKLOG。 低危改进项,或者修法属于新增机制、不在本轮整改范围。R4 有一条这样处理,R-TOOLCARDS 也有一条:「展开体超过六行时截断提示被裁切区盖住」属于画板三条裁定互斥的设计取舍,写明理由记 BACKLOG。
三个去向的共同点是留痕。审查记录不是给这一轮看的,是给下一轮的审查者和下一个会话的 agent 看的:同一处再被扫到时,不必重新裁定。
从项目的审查记录里挑三条,看缺陷门禁能抓到什么。
R1,P1,脱敏应为白名单而非黑名单。 原实现是「删掉已知的凭据字段」;审查者指出安全文档要求的是「只放行已知安全的字段」。整改后 provider 相关事件只剩 {type} 或 {type, status},泄漏扫描零命中。这条 findings 改变了整个事件流的脱敏形态,后来的「采集点即脱敏点」原则就是从这里长出来的。
R-WEBSEARCH,测试抓到的真 bug。 上游 4xx 的响应体被原样放进了错误对象的消息里,而网关会把请求头回显进错误体——明文 key 就这样进了一个会被传递、被别处 catch、将来被某个人直接打印的 Error。教训写进了任务卡:凭据要在构造错误的地方就抹掉,而且通用模式(前缀匹配)兜不住纯十六进制的自定义网关 key,必须再叠一道本次 key 的精确替换。
R-BUN,部署脚本的参数白名单。 初审四条 P1 里有一条关于迁移脚本:dotenv 文件不能作为 shell 执行、参数要过白名单、SHA 要硬校验。这些在本机跑通的脚本里看不出问题,在服务器上就是注入面。
三条的共同点:都不是「代码风格」,都是「在某个真实条件下会出错」,而且都附带可验证的复现或整改证据。
R-SKILLS 的一条经验:codex 第 1 轮报「访客读面的两条查询不在同一快照」,实现 agent 只改了那一处;第 2 轮在管理端读面又来了同一类。
一条 findings 指向一类模式时,先在同一份 diff 里 grep 同类位置一起改,少跑一轮。这不是审查者的责任——它只报它扫到的——是整改者的责任。
审查者不共享实现者的上下文,所以项目知识要通过文件给它:
不做这一步的后果不是审查失败,而是审查「看起来正常但少查了东西」。这种失败最难发现。
审查工具本身也是软件。R-WEBFETCH 首轮遇到一次:后台发起的审查任务读完 diff 后 38 分钟零日志,状态永远是 running。原因是后台启动方式让审查进程随启动器一起退出,任务记录没有写完成状态。
处理办法记进了项目记忆:改为脱离工具生命周期启动,把结果写到文件,用监视器轮询文件是否非空;判断「进程是否还活着」时先看有没有心跳输出再下结论。
耗时基线也记了下来(同一台机器):全量分支 diff 九到十分钟,只审整改 diff 六到九分钟,零 findings 那一轮反而最久(17 分钟)。知道基线才能区分「还在跑」和「已经死了」。
到 2026-09-04,各轮审查记录的摘要(取自 ROUNDS.md 进度表):
| 轮 | 审查轮数 | findings | 处置 |
|---|---|---|---|
| R1 | 2(初审 + 对抗式复审) | 4 | 全采纳 |
| R-BUN | 4 | 14 | 全采纳,末轮零 |
| R3 | 3 | 8 | 全采纳,末轮零 |
| R4 | 5 | 8 | 7 采纳、1 记 BACKLOG,末轮零 |
| R5 | 7 | 17 | 15 采纳、2 所有者裁定不采纳并留兜底,末轮零 |
| R6 | 5 | 18 | 16 采纳、1 实跑证伪不采纳、1 记 BACKLOG |
| R7 / R8 |
两点观察。第一,findings 的数量与改动规模相关但不成比例:R6(管理面)18 条、R-WEBFETCH(一个 skill 加一个容器)12 条,后者的大部分 findings 落在一块自建机制上——那是第 14 章的主题。第二,零 findings 的轮次确实存在(R-TABS 首轮),它的改动小、边界清楚、机制复用既有的。
审查工具不可用时怎么办?CLAUDE.md 写死了:降级到内置的代码审查只认硬失败——CLI 未安装、未登录、启动失败;降级原因写进任务卡。「等得久」「改动小」不是理由。
三轮由所有者裁定跳过审查的记录(R9、R10、R11)都不是降级,是显式偏离。R9 的任务卡把过程写得很清楚:实现 agent 给出分支 diff 实况——九百多行新增里约六百行是文档,代码与脚本约九十行,分布在四处——并指出最值得被人看一眼的两处;所有者据此仍裁定跳过。任务卡最后一句:「本轮无审查记录。这是与流程的一次显式偏离,记在此处以免将来被误读成审查通过。」
跳过可以,但要让「没审」这件事本身可查证。
给自己的项目配一个独立审查者(另一个 agent 或另一个人),写下三条规则:收口标准(什么级别的问题清零才许合并)、审查范围(哪几轮全量、之后审什么)、降级规则(什么情况可以不审,怎么留痕)。然后用一次真实改动跑一遍循环,把 findings 的去向逐条写下来。
| 3 / 3 |
| 6 / 3 |
| 全采纳 |
| R-VISITOR | 3 | 6 | 4 采纳、1 所有者裁定不修、1 记 BACKLOG |
| R-WEBSEARCH | 4 | 6 | 全采纳 |
| R-TITLE | 5 | 4 | 2 采纳、1 所有者裁定不采纳并回滚、1 随回滚作废 |
| R-TOOLS | 2 | 1 | 采纳 |
| R-IMAGEGEN | 3 | 3 | 全采纳 |
| R-TABS | 1 | 0 | 首轮零 |
| R-SKILLS | 3 | 5 | 全采纳 |
| R-WEBFETCH | 7 | 12 | 首轮 4 条采纳;第 2–6 轮 8 条最终由所有者裁定回退(见第 14 章) |
| R-PERF / R-TOOLCARDS | 2 / 2 | 1 / 2 | 采纳;1 记 BACKLOG |
| R9 / R10 / R11 | — | — | 所有者裁定跳过,理由与残留风险写在任务卡 |