让 Claude Code 非交互驱动 Codex:给你的 AI 找个异构审稿人
先说结论:同一个模型写代码、再让它自己审一遍代码,效果比你以为的差得多。
不是它不认真,是它的盲点是”分布级”的——训练数据、对齐方式、推理习惯决定了它在哪类问题上会稳定犯错,而它自己看不见这一类。你让它复查十遍,十遍都觉得没问题。
治这个当然不止一条路:测试、静态分析、类型检查、人工 review 都在治它,而且更便宜。但它们各有各的盲区,而且都不太擅长”这段逻辑在语义上是不是错的”。我补的是另一层:让一个不同训练分布的模型来挑刺——具体做法是让 Claude Code 当主脑写活,非交互地驱动 Codex CLI 当审稿人。
跑了一段时间,这是我目前投入产出比最高的一条 AI 工程改造。这篇把原理、完整步骤、和一个你能照着跑通的例子写全。文中所有命令都在 Windows Git Bash + codex-cli 0.144.4 上实测过。
一、为什么要”非交互驱动”
多模型互审这个道理,很多人知道。常见做法是:开个 Codex 窗口,把代码贴过去,问一句”帮我看看”。
这么干能用,但有两个问题:
- 人是瓶颈。要人复制粘贴,这道审查就只在你想起来的时候发生。想不起来 = 没有。
- 进不了流程。凡是需要人操作的环节,都没法挂进”改完就自动审一遍”这种链路里。
(顺带说一句:“交互式就一定丢上下文”是个稻草人——你在仓库目录里起一个交互式 Codex 会话,它照样能读 diff 和仓库结构。真正的差别不在上下文,在于能不能被程序调用。)
非交互驱动(headless,一次性命令行调用)解决的就是这个:一条命令进去,结果出来,可以被脚本调、被 Agent 调、被别的自动化调。审查从”我记得的时候做一次”变成”每次都做”。
原理上就三件事:
codex exec/codex review是一次性非交互入口,不进 TUI,跑完就退;- 沙箱权限分档(
read-only/workspace-write/danger-full-access),审查用只读档——下面会讲这个为什么关键,以及为什么它不是默认的; - review 的输出惯常带结构:
[P1]/[P2]优先级 +文件:行号+ 说明,很适合整段回灌给另一个模型逐条修。
二、职责必须分开:Codex 只找茬,不改
这是整套打法里最容易做错的一条。
不少人接上第二个模型以后顺手就让它”发现问题顺便改了”。这样做你就失去了”独立第二双眼”这个核心价值——它改完的东西,又变成了”自己写、自己认可的代码”,你只是把单模型闭环换了个模型,闭环还是闭环。
(严格说,它先前指出问题的价值不会消失;但如果修复也归它,你就没有任何一双独立的眼睛看那份修复了。)
正确的分工:
Codex 审、Claude 改。 Codex 只输出问题清单(只读沙箱,物理上改不了),清单回灌给 Claude 逐条修,修完再跑一次 review 复核,直到干净。
三、四个坑(都是实打实撞出来的)
坑 1:stdin 不管好会出意外。
codex exec 在没给 prompt(或 prompt 写成 -)时,会从 stdin 读指令;而已经给了 prompt 时,如果 stdin 是管道,管道内容会被当成一个 <stdin> 块追加进 prompt。这两种在脚本里都不是你想要的——前者可能挂在那儿等输入,后者会悄悄污染 prompt。
→ 一律 </dev/null 闭掉,确定性最高。
坑 2:非 git 目录被拦。
在不是 git 仓库的目录里跑会报 Not inside a trusted directory 直接退出。
→ 加 --skip-git-repo-check,或者就在已信任的项目里跑。
坑 3:-s 和 -a 是顶层参数,必须放在子命令前面。
这个位置搞错会直接报错退出,很多人第一次都栽在这儿:
codex exec -a never "..." # ❌ unexpected argument,退出码 2
codex -a never exec "..." # ✅ 合法
顺带:exec 本身默认就是 approval=never(不然没法非交互),所以非交互场景一般根本不用传 -a,权限用 -s 控就够。
坑 4:codex review 自己没有 -s,而且不传就不是只读。
codex review -s read-only --uncommitted # ❌ 参数错
codex -s read-only review --uncommitted # ✅
这条最值钱:你不显式给这个 flag,“审稿人改不了你的代码”就只是个愿望。想让职责分离有物理保障,flag 必须落到命令里。
还有个不算坑但影响体验的:推理档位默认偏高,跑简单活慢到像卡死。加 -c model_reasoning_effort=low 能明显变快(我这台上从”等到怀疑人生”变成十几秒)。具体默认档位随模型、版本、本地配置变,别当固定值,按自己机器实测。审查这种高价值场景才值得开高档。
四、完整步骤
步骤 0:装好并登录
codex --version # 我这边是 codex-cli 0.144.4
codex login status # 应该回 Logged in
本地开发机用订阅登录最省事。如果你要挂到独立的 CI runner 上,那台机器仍然需要单独配认证(CLI 也支持 codex login --with-api-key),别以为本地登录了流水线就自动能用。
步骤 1:把坑固化成一个封装脚本
别每次现拼命令行——手拼的会手滑,脚本不会。存成 ~/cx.sh(下面示例都用这个路径):
#!/usr/bin/env bash
# cx.sh — 非交互驱动 Codex 的封装:把坑固化掉
# ask 非交互提问,只回纯文本答案(只读沙箱,默认低档位求快)
# edit 让它在指定目录改文件(写沙箱,限制在工作目录内)
# review 异构审代码 diff(只读沙箱)—— 高价值场景
# 环境变量:CODEX_BIN(默认 codex)/ CX_EFFORT(low|medium|high|xhigh)
set -uo pipefail
CODEX_BIN="${CODEX_BIN:-codex}"
sub="${1:-}"; shift || true
case "$sub" in
ask)
prompt="${1:?usage: cx.sh ask \"<prompt>\" [workdir]}"; wd="${2:-$PWD}"
out="$(mktemp -t cxans.XXXXXX)"; err="$(mktemp -t cxerr.XXXXXX)"
"$CODEX_BIN" exec --skip-git-repo-check -C "$wd" -s read-only \
-c model_reasoning_effort="${CX_EFFORT:-low}" -o "$out" "$prompt" </dev/null >/dev/null 2>"$err"
rc=$?
# 成功只吐干净答案;失败才放出 stderr(否则认证/网络错误会被静默吞掉)
if [ $rc -ne 0 ] || [ ! -s "$out" ]; then
echo "[cx.sh] codex exec 失败 (rc=$rc),stderr:" >&2; cat "$err" >&2
fi
cat "$out"; rm -f "$out" "$err"; exit $rc ;;
edit)
prompt="${1:?usage: cx.sh edit \"<prompt>\" [workdir]}"; wd="${2:-$PWD}"
exec "$CODEX_BIN" exec --skip-git-repo-check -C "$wd" -s workspace-write \
-c model_reasoning_effort="${CX_EFFORT:-medium}" "$prompt" </dev/null ;;
review)
scope="${1:---uncommitted}"; wd="${2:-$PWD}"
cd "$wd" || exit 1
# -s read-only 必须放在 review 前面(顶层参数)
# shellcheck disable=SC2086
exec "$CODEX_BIN" -s read-only review $scope </dev/null ;;
*) echo "usage: cx.sh {ask|edit|review} ... (see header)" >&2; exit 64 ;;
esac
几个设计点:
ask用-o <file>把最终答案单独写文件再cat,是为了避开终端横幅和进度噪声,让输出干净到能直接喂给下一个程序。- 但 stdout 重定向到
/dev/null时别顺手把 stderr 也吞了——那样认证过期、网络挂了、配置写错,你只会看到一片空白。所以失败路径单独把 stderr 放出来。 ask和review都固定只读沙箱。提问就是提问、审查就是审查,不给改文件的能力。
步骤 2:三种用法
# 一、当纯问答/第二个脑子
bash ~/cx.sh ask "这段正则在什么输入下会灾难性回溯" ~/proj
# 二、当编码 worker(它真的会改文件)
bash ~/cx.sh edit "把 utils.py 里的同步 IO 改成异步,保持接口不变" ~/proj
# 三、异构审查(最该用的)
bash ~/cx.sh review --uncommitted ~/proj # 审未提交的改动,最常用
bash ~/cx.sh review "--base main" ~/proj # 审当前分支相对 main 的差异
bash ~/cx.sh review "--commit abc123" ~/proj # 审某次提交
五、可照做示例:从零跑通一次异构审查
造一个带两个典型 bug 的仓库(⚠️ 第一行会删掉 /tmp/cxdemo,确认这个目录你不要了再粘):
rm -rf /tmp/cxdemo && mkdir -p /tmp/cxdemo && cd /tmp/cxdemo && git init -q
cat > app.py <<'EOF'
import subprocess
def run_backup(user_dir):
# 把用户传来的目录打包
cmd = "tar czf /tmp/bak.tgz " + user_dir
subprocess.run(cmd, shell=True)
def last_n(raw, n):
out = []
for i in range(len(raw) + 1):
if i >= len(raw) - n:
out.append(raw[i])
return out
EOF
git add -A
两个 bug:字符串拼接 + shell=True 的命令注入,和 range(len(raw)+1) 的越界。这两类是 AI 写代码时相当典型的疏漏——尤其第二个,看上去非常”对”。
跑审查:
bash ~/cx.sh review --uncommitted /tmp/cxdemo
我这次的真实输出(22 秒):
- [P1] Avoid executing the backup path through a shell — app.py:5-6
When `user_dir` contains shell metacharacters such as `;`, `&&`, or command
substitution, they are interpreted as commands because the value is concatenated
into a string passed with `shell=True`. Any caller-controlled directory can
therefore execute arbitrary commands; pass an argument list to `subprocess.run`
without a shell instead.
- [P1] Stop iteration before indexing past the sequence — app.py:10-12
For every nonnegative `n`, the loop reaches `i == len(raw)`, the condition is
true, and `raw[i]` raises `IndexError`. Consequently `last_n` cannot successfully
return for normal inputs, including `n == 0`; iterate only through valid indices
or use slicing.
两个预设 bug 都抓到了,带优先级、带行号、给了修法,行号准确、没有编造不存在的问题。
(诚实说明两点:一是你跑出来的措辞和耗时不会跟我一模一样——模型版本、账户、配置都会影响,抓到这两个问题就算对了;二是这只能说明”这两个被抓到”,推不出”零漏报”,我没有完整规格去证明不存在第三个问题。)
这个格式的好处是可以整段回灌给主脑:“按这份清单逐条修,修完我再审一遍。”
同机另外两条实测:ask 一次简单问答 6.7 秒;edit 让它建文件写内容,文件真的生成了。
一个重要提醒:别拿退出码当质量门禁
review 的退出码只说明命令跑没跑成功,不代表”审查通过”或”发现了问题”。想挂 CI 门禁,得自己解析或判定那份清单。
同理,那个 [P1]/[P2] 格式虽然稳定好用,但它是模型生成的文本,不是接口契约(review 没有 --json / --output-schema)。别写正则去硬解析它,当”给人和给另一个模型看的清单”用。
六、进阶:挂成 MCP 工具
如果你要在一个会话里反复多轮调它(而不是一次性审一把),可以挂成 MCP:
{ "mcpServers": { "codex": { "command": "codex", "args": ["mcp-server"] } } }
它是 stdio MCP,暴露两个工具:codex(起一次会话)、codex-reply(按 thread id 续)。我实测握手返回 codex-mcp-server 0.144.4,tools/list 正常列出这两个。
选型建议:一次性审查/驱动,用命令行封装就够了,轻、干净、好嵌流程;只有需要多轮往返对话时才上 MCP(更重)。 别一上来就挂。
七、什么时候该挂这道审查
我把它设成三个场景的默认动作,不是”想起来才做”:
- 改了关键代码之后——审未提交的 diff,清单回灌逐条修,复审到干净再提交;
- 对外发布之前——把成稿丢给它”只挑错别夸”,事实错、逻辑漏先过一道;
- 长任务的产出回来时——把它当独立验收员,把一部分”待人工验”换成”异构模型验”。
顺便说,这篇文章自己就走了第 2 条。初稿我写的是”review 走只读沙箱,这是设计”——Codex 直接指出:脚本里根本没传 -s read-only,这个说法不成立。它还挑出我把 stdin 行为、-a 参数都描述错了。上面那版脚本和坑 3、坑 4 就是照它的清单改的。这是一次真实的、生效了的异构审。
反过来,不该用的场景:琐碎小改动(审查开销比改动本身大)、纯创意类产出(没有对错标准,两个模型只会互相打架)、以及你自己都还没想清楚要什么的时候(这时候该做的是想清楚,不是加个模型)。
八、几条红线
- 别给审稿模型写权限。审查显式传
-s read-only(放在review前面),让职责分离有物理保障。 danger-full-access别当默认。要写就给workspace-write,把它限制在工作目录里。- 注意数据外传面比你想的大。不只是”别把密钥写进 prompt”——
-C <dir>指定的工作目录里的.env、配置、源码,模型都可能读到。跑一次 = 把这个目录交给了另一个厂商的模型。含敏感数据的目录别随手跑。 - 异构审查是找盲点,不是找权威。它也会说错、也会挑一些不成立的毛病(我这次 15 条里就有几条是过度批评,我自己复核后没采纳)。它的价值在于”它错的地方和你的主脑错的地方不重合”,而不是”它更对”。收到清单要自己复核,别照单全收。
最后
这套东西门槛其实很低——一个封装脚本,三条命令。但它改变的是工作方式:从”我写完检查一遍”,变成”我的产出默认要过另一个分布的模型”。
单模型闭环里,你不知道自己不知道什么。加一道异构审,至少能把”系统性盲点”降级成”两个模型都没看出来”。这已经是很大的进步了。
配套工具打包(封装脚本 + 完整说明 + 可跑 demo):drive-codex-skill.zip