1. 为什么"独立评审"才是 Headless Agent CLI 的正确打开方式
很多人第一次接触 Headless 模式的 Agent CLI,脑子里冒出来的第一个念头都是"自动化"——让它自己跑任务、自己改代码、自己提交。我一开始也是这么想的,折腾了小半个月,结果发现这条路又累又不稳:任务越复杂,Agent 越容易在中间某一步跑偏,而且你根本不知道它是在哪一步开始错的。后来我换了个思路,把 Headless Agent CLI 当成一个独立的评审者来用,整个体验完全不一样了。
所谓 Headless,指的是 Agent CLI 不依赖交互式终端界面,可以直接在脚本、CI 流水线、Git hook 里被调用,输入一段内容、输出一段结果就退出。它没有"对话窗口"这个概念,你给它什么它处理什么。而"独立评审"的意思是:让一个和主开发流程隔离的 Agent 实例,专门负责对产出物做审查、挑刺、给意见,而不是让它去写代码。
这个用法为什么合适?因为 Agent 在"生成"这件事上天然有幻觉风险,但在"评审"这件事上,它的价值反而被放大了。评审不需要它百分之百正确,只需要它把可疑的地方标出来,人来拍板。它像一个不知疲倦、不会顾及情面的初级审阅者,你不需要它多聪明,你需要它稳定、可重复、不遗漏。
这篇文章适合三类人看:一是已经在用 Codex 这类 Agent CLI 但总觉得"自动化不靠谱"的开发者;二是想把代码评审、文档校对、配置检查这类重复劳动外包出去的团队;三是单纯想搞明白 Headless 模式到底该怎么落地、而不是被各种教程带着瞎跑的人。下面我会从架构、实操、踩坑、进阶四个层面,把"独立评审"这套用法讲透。
2. 独立评审的架构:主 Agent 与评审子代理如何分工
2.1 为什么不能让同一个 Agent 既写又审
这是最核心的一条经验。我早期图省事,让同一个会话里的 Agent 写完代码后自己 review 一遍,结果它几乎永远说"看起来没问题"。原因很简单:同一个上下文里的 Agent 对自己的产出有确认偏误,它刚刚生成的内容还在上下文窗口里,它会倾向于认为自己的推理是连贯的、正确的。
正确的做法是把"写"和"审"拆成两个独立的进程调用。写的那次调用结束后,进程退出,上下文清空;评审时重新起一个 Headless 进程,只把产出物本身(diff、文件内容、配置文本)喂进去,不带任何"这是我刚写的"这类元信息。这样评审 Agent 面对的就是一份陌生材料,它的挑刺意愿会明显上升。
2.2 子代理(Sub-agent)模式的三种落地形态
"子代理"这个词听起来玄乎,落地其实就三种形态,我按复杂度从低到高列一下:
| 形态 | 实现方式 | 适用场景 | 隔离程度 |
|---|---|---|---|
| 进程级隔离 | 每次评审起一个新的 CLI 进程 | 个人项目、本地 pre-commit | 高 |
| 目录级隔离 | 评审 Agent 在独立工作目录运行,只读挂载待审文件 | 团队 CI、多仓库 | 高 |
| 容器级隔离 | 评审跑在一次性容器里,网络与文件系统受限 | 对外部贡献代码的评审 | 最高 |
我个人最常用的是进程级隔离,因为它最简单,一条命令就能起。目录级隔离适合 CI 场景,因为 CI 本身就是干净环境。容器级隔离一般只在评审第三方 PR 时才值得上,成本偏高。
2.3 评审 Agent 的输入应该包含什么
这一点很多人做错。评审 Agent 的输入不是"帮我看看这段代码",而是结构化的材料包。我通常会给它这几样东西:
- 变更的 diff(用
git diff或git diff --staged生成) - 相关的上下文文件(被改动文件的完整内容,而不是只有 diff 片段)
- 一份评审清单(checklist),明确告诉它关注哪些维度
- 项目约定(比如命名规范、目录结构约定)
提示:diff 单独喂给 Agent 时,它经常因为看不到上下文而误报。把改动文件的完整内容一起给它,误报率能降一大截。
评审清单是灵魂。没有清单,Agent 会自由发挥,一会儿聊性能一会儿聊风格,输出没法用。有了清单,它的输出就变成可解析的结构化结果。我常用的清单维度包括:逻辑正确性、边界条件、错误处理、命名一致性、潜在的性能陷阱、安全相关写法。
3. 把评审跑起来:从命令行到 CI 的完整链路
3.1 最小可用的本地评审命令
先给一个能直接抄的最小例子。假设你已经装好了 Codex CLI,并且配置好了模型接入,那么一次本地评审大概长这样:
# 生成待评审的 diff 和上下文 git diff --staged > /tmp/review.diff git diff --staged --name-only | xargs -I{} cat {} > /tmp/review.context # 组装评审材料 cat /tmp/review.diff /tmp/review.context > /tmp/review.input # 起一个 Headless 评审进程 codex exec --headless \ --prompt-file ./review-checklist.md \ --input /tmp/review.input \ --output /tmp/review.result这里几个参数值得说明。--headless表示不进入交互模式,处理完就退出。--prompt-file指定评审清单,把清单和材料分开管理,方便复用。--input是待审内容,--output是结果落盘,方便后续解析。
不同版本的 CLI 参数名可能略有差异,比如有的用-p代替--prompt-file,有的把 headless 模式叫exec或run。参数名以你本地--help输出为准,别照搬网上的教程,我见过太多人因为参数名对不上卡半天。
3.2 评审清单怎么写才有效
清单不是越长越好。我试过写二十条的清单,结果 Agent 每条都蜻蜓点水,输出质量反而下降。后来精简到六到八条,每条都要求它给出具体行号 + 问题描述 + 修改建议,输出立刻变得可用。
一份我常用的清单长这样:
你是独立代码评审者。请针对输入内容逐项检查,每项给出: - 是否存在问题(是/否) - 若存在,指出具体位置(文件:行号) - 问题描述与修改建议 检查项: 1. 逻辑正确性:是否存在明显的逻辑错误或死代码 2. 边界条件:空值、越界、并发等边界是否处理 3. 错误处理:异常是否被吞掉或未处理 4. 命名一致性:命名是否符合项目约定 5. 性能陷阱:是否存在明显的低效写法 6. 安全写法:是否存在注入、越权等风险模式注意最后一句"每项给出是否存在问题",这个格式约束非常关键。它逼着 Agent 对每一项都表态,而不是挑几个它感兴趣的说说。没有这个约束,它经常只报两三条就收工。
3.3 把评审接进 CI 流水线
本地跑通之后,接 CI 是水到渠成的事。核心思路是:在 CI 的某个阶段,把 diff 和上下文准备好,起一个评审进程,把结果作为构建产物或评论输出。
# 伪配置,字段名按你所用 CI 平台调整 review_job: steps: - run: git fetch origin main - run: git diff origin/main...HEAD > review.diff - run: | codex exec --headless \ --prompt-file .ci/review-checklist.md \ --input review.diff \ --output review.result - run: cat review.result这里有个坑要提前说:CI 环境里的 Agent 调用通常需要凭证,而凭证管理是安全红线。我的做法是把凭证放在 CI 平台的加密变量里,绝不写进仓库,也绝不在日志里打印。评审结果里如果包含敏感路径信息,输出前要做一次过滤。
注意:评审 Agent 的输出不要直接当成"通过/不通过"的硬门禁。它会有误报,硬门禁会误伤正常提交。我的做法是把它作为提示性评论,人来决定是否阻塞合并。
4. 踩过的坑:评审 Agent 的误报、漏报与上下文陷阱
4.1 误报的三种典型来源
误报是评审 Agent 最让人头疼的问题。我统计过自己项目里一个月的评审输出,误报大概占三成。归因下来主要是三类:
第一类是上下文缺失导致的误报。Agent 只看到 diff,看不到被调用函数的定义,就会说"这个函数不存在"。解决办法前面提过,把完整文件内容一起喂进去。
第二类是清单过于宽泛导致的误报。比如清单里写"检查性能问题",Agent 会把任何循环都标成性能隐患。把清单细化成"检查是否存在嵌套循环导致的 O(n²) 复杂度",误报立刻减少。
第三类是模型对项目约定不了解导致的误报。比如项目里snake_case和camelCase混用是有意为之(历史原因),Agent 不知道,就会一直报命名不一致。解决办法是在清单里附上一段项目约定说明。
4.2 漏报往往比误报更危险
误报烦人但无害,漏报才是真问题。我遇到过最典型的一次漏报:Agent 评审一个并发修改,没发现两个 goroutine 同时写同一个 map,结果上线后偶发崩溃。复盘时发现,清单里根本没有"并发安全"这一项。
所以清单要定期根据线上事故反推补充。每次出了 bug,问一句"如果评审清单里有这一项,能不能提前发现",能就加进去。这样清单会随着项目演进越来越贴合实际风险。
4.3 上下文窗口的边界处理
大项目里,一次 diff 可能涉及几十个文件,全塞进去会超上下文窗口。我的处理策略是分层:
- 核心改动文件:完整内容喂入
- 相关但未改动的依赖文件:只喂函数签名和注释
- 无关文件:不喂
这个分层逻辑可以写成一个脚本自动完成,根据 diff 涉及的文件路径,去仓库里捞对应的上下文。别偷懒全塞,超窗口后 Agent 会截断,截断位置之后的内容它完全看不到,漏报就是这么来的。
4.4 一个真实的排查链路
有段时间评审结果里频繁出现"无法解析输入"的报错。我按这个顺序排查:
- 先看输入文件大小,发现某个 diff 有 8MB,怀疑超限
- 手动截断到 1MB 重跑,正常了,确认是大小问题
- 进一步查,发现是某次提交误把构建产物也 diff 进去了
- 在生成 diff 的命令里加上路径过滤,排除
dist/、build/等目录 - 重跑,问题消失
这个链路说明一件事:评审 Agent 的很多"故障"其实是输入准备阶段的故障,别一上来就怀疑模型,先检查你喂给它的东西干不干净。
5. 让评审更准:清单迭代、结果解析与人工兜底
5.1 评审结果的结构化解析
Agent 的输出是自然语言,直接看很累。我一般要求它输出成固定格式,然后用脚本解析成表格。比如要求每条问题以[严重度] 文件:行号 - 描述开头,解析脚本就能按严重度排序、按文件聚合。
import re pattern = re.compile(r"\[(高|中|低)\]\s+(\S+):(\d+)\s+-\s+(.+)") issues = [] with open("review.result") as f: for line in f: m = pattern.match(line.strip()) if m: issues.append({ "severity": m.group(1), "file": m.group(2), "line": int(m.group(3)), "desc": m.group(4), }) # 按严重度排序输出 order = {"高": 0, "中": 1, "低": 2} issues.sort(key=lambda x: order[x["severity"]]) for i in issues: print(f"{i['severity']} {i['file']}:{i['line']} {i['desc']}")有了结构化结果,就能做很多事:只把"高"严重度的推到人面前,"低"的攒起来定期看;统计哪类问题出现频率最高,反过来优化清单。
5.2 清单迭代的节奏
清单不是写完就完事的。我的节奏是每两周回顾一次:把这两周的评审结果翻一遍,看哪些条目从来没报过问题(可能太宽泛或没用),哪些条目误报特别多(需要细化),哪些线上问题清单没覆盖(需要新增)。
这个迭代过程本身就是价值。它逼着团队把"我们到底在意什么"想清楚,而不是笼统地说"代码质量要好"。
5.3 人工兜底不可省
无论评审 Agent 多好用,最终拍板必须是人。我见过有团队把评审结果直接当门禁,结果因为一次误报阻塞了紧急修复,得不偿失。正确的定位是:Agent 负责"广撒网",人负责"精判断"。它把可疑点都列出来,人快速扫一遍,该忽略的忽略,该改的改。
提示:把评审 Agent 的输出当成"同事的初稿意见",而不是"权威结论"。这个心态摆正了,用起来会顺很多。
6. 进阶玩法:多评审子代理交叉验证与场景延展
6.1 两个评审子代理交叉验证
单个评审 Agent 会有盲区。我的进阶做法是起两个评审进程,用不同的清单或不同的模型,然后对比结果。两个都报的问题,基本可以确认;只有一个报的,人工重点看。
# 评审者 A:侧重逻辑与边界 codex exec --headless --prompt-file checklist-logic.md \ --input review.input --output review.a # 评审者 B:侧重安全与性能 codex exec --headless --prompt-file checklist-security.md \ --input review.input --output review.b # 取交集与差集 comm -12 <(sort review.a) <(sort review.b) > confirmed.issues comm -3 <(sort review.a) <(sort review.b) > needs.human.review这个模式成本翻倍,但换来的是更高的召回率。对于核心模块的改动,我觉得值。对于边角改动,单评审就够了。
6.2 评审场景不止于代码
"独立评审"这套思路可以延展到很多场景,我试过并且有效的有:
- 配置评审:把配置文件 diff 喂给 Agent,检查是否有拼写错误、是否有未识别的字段。热词里提到的"unrecognized configuration setting"这类问题,用评审 Agent 提前抓非常合适。
- 文档评审:检查文档里的链接是否失效、术语是否一致、示例代码是否能跑通。
- SQL 迁移评审:检查迁移脚本是否有锁表风险、是否有回滚方案。
- 依赖变更评审:检查新增依赖的许可证、体积、维护状态。
这些场景的共同点是:产出物是文本,评审标准可以写成清单,且人工评审很枯燥。只要满足这三点,就适合交给 Headless Agent。
6.3 与主开发流程的隔离原则
最后强调一条架构原则:评审 Agent 永远不要有写权限。它只读材料、只输出意见,绝不碰仓库。我见过有人图省事让评审 Agent 直接改代码,结果它把"建议"当"指令"执行了,改出一堆问题。
隔离的具体做法:评审进程的工作目录是只读挂载的,或者干脆在一个临时目录里跑,材料是拷贝进去的。这样即使 Agent 想写也写不了,安全边界清晰。
7. 我在实际使用中总结的几条硬经验
第一,评审 Agent 的价值不在"聪明",在"稳定"。它不需要比人强,它只需要每次都按同样的标准检查一遍,不遗漏、不偷懒。人做不到每次都全神贯注,它能。
第二,输入质量决定输出质量。花在准备评审材料上的时间,远比花在调 prompt 上的时间值得。diff 干净、上下文完整、清单明确,输出自然可用。
第三,别追求零误报。误报是评审 Agent 的固有特性,接受它,然后用结构化解析和人工兜底来消化它。追求零误报只会让你不断加约束,最后把有用的信号也约束没了。
第四,清单要跟着事故走。每次线上出问题,都问一句"评审清单能不能提前发现",能就加。这样清单会越来越贴合你的真实风险,而不是网上抄来的通用模板。
第五,评审结果要留档。我习惯把每次评审的输入和输出都存下来,一是方便回溯"这个问题当时为什么没报",二是积累一段时间后可以用来分析团队的高频问题类型,反过来指导开发规范。
这套用法我用了大半年,最大的感受是:Headless Agent CLI 最舒服的姿势,真的不是让它替你干活,而是让它替你盯着。写代码的活人自己干更靠谱,但盯代码的活,交给一个不知疲倦、不讲情面的独立评审者,刚刚好。