跳转至

第 13 章 评估与自我验证

前面的 agent 已经会干很多活了:它能跑工具、能改文件、能压缩上下文、能拉起子 agent 协作。但有个要命的问题一直没正面回答:它怎么知道自己干对了? 更糟的是——如果让它自己判自己,它几乎总会给自己打高分。 这一章讲怎么造一个可信的"验收闭环",并用 Codex 源码里那个常被误解的 ReviewTask 把它拆到底。

引子:一个会给自己打全 A 的学生

Anthropic 在《Harness design for long-running application development》里有一句很扎心的观察:"Out of the box, Claude is a poor QA agent."(开箱即用的 Claude,是个糟糕的 QA。)他们紧接着描述了那个让人哭笑不得的失败模式——模型 "would identify legitimate issues, then talk itself into deciding they weren't a big deal and approve the work anyway"(它会发现一个真 bug,然后又说服自己"这问题不大",然后照样批准通过)。

这不是模型笨,而是一种系统性偏差:让产出者来评判自己的产出,缺乏独立性。就像让学生给自己的卷子打分,全 A 不奇怪——他既写了答案,又当了阅卷人,立场天然偏向"通过"。

这种偏差为什么这么顽固?一部分原因藏在训练目标里:模型被大量"有帮助、要肯定用户"的信号塑造过,天然倾向于给出令人满意、显得已完成的回答;当被审的"用户"就是它自己刚写的代码时,这股"想说没问题"的拉力反而成了 QA 的敌人。另一部分是上下文使然——它在同一段对话里既看见了自己生成代码时的"意图",又来评判结果,于是很容易用"我本来就是这么想的"去解释掉一个本该被标红的 bug。独立性的缺失不只是立场问题,更是上下文与目标的双重污染。 这也预告了后面的解法:要让评判可信,光换提示词不够,得换一个视野更窄、目标更纯、权限更小的角色去看。

所以"评估"这一层的第一性原理就一句话:把"做"和"判"分开。 别让写代码的 agent 自己当唯一的验收者,要有一个独立的评判角色。这个"独立"不是口号——它必须落到机制上:评判者得有自己的提示词、自己的权限、看不到也碰不到产出者的工具。本章下半场会看到,Codex 的 ReviewTask 恰恰是把"独立"这两个字焊进了配置里。

〔配图 1:自评 vs 独立评审〕——左"自评":一个 agent 看着自己的作业,笑嘻嘻盖了个"全 A / LGTM"的章,旁注"自信地夸自己——糟糕的 QA"。右"独立评审":另一个戴眼镜的 evaluator agent 拿放大镜逐项挑刺,列出一串 FAIL,旁注"独立、挑剔、只看不改"。底部:"别让学生给自己的卷子打分。"


一、最朴素的版本:让"判"成为一种独立的任务

回忆第 3 章。Codex 的一次 turn 由一个 SessionTask 驱动,trait 定义在 codex-rs/core/src/tasks/mod.rs

pub(crate) trait SessionTask: Send + Sync + 'static {
    fn kind(&self) -> TaskKind;
    fn span_name(&self) -> &'static str;
    fn run(self: Arc<Self>, /* … */) -> impl Future<Output = Option<String>> + Send;
}

它的实现有好几种:RegularTask(日常对话)、CompactTask(压缩,见第 5 章)、UserShellCommandTask,以及本章主角 ReviewTask。把"评审"做成一个平级的任务种类——而不是在普通对话里塞一句"你顺手检查一下自己写的代码"——本身就是一个工程决定:评审有自己的生命周期、自己的遥测计数器(codex.task.review)、自己的进出场仪式。这是"做判分离"的第一层落地。

为什么"独立任务"这件事本身就有价值?因为它把评审从"对话里的一句叮嘱"升格成了"有边界的执行单元"。回想第 3 章:每个 SessionTaskSession::start_task 拉起,跑在自己的 Tokio 任务里,挂自己的 tracing span(span_name() 返回 "session_task.review"),完成或被取消时统一走 on_task_finished / handle_task_abort。这意味着评审天生具备可取消、可观测、可计量三个属性——你能中途打断它、能在遥测里看到它跑了多久花了多少 token、能数它被触发了几次。如果评审只是混在普通对话里的几句提示词,这些边界全都消失:它会和写代码的动作搅在同一段历史、同一个 span、同一笔 token 账里,既调不准也撤不掉。把"判"做成一等公民的任务,是后面一切隔离的地基。

但真正关键的不是"它是一个独立任务",而是这个任务内部是怎么把评判者隔离开的。这就是下一节,也是最容易理解错、最值得讲清的地方。


二、ReviewTask 的真相:一个"只看不改"的受限 sub-agent

打开 codex-rs/core/src/tasks/review.rsReviewTask::run 的核心动作不是"在当前会话里换个提示词继续聊",而是另起一个受限的子会话(sub-codex / delegate),通过 run_codex_thread_one_shot 一次性跑完。真正承重的是 start_review_conversation 里这段配置收窄:

let mut sub_agent_config = config.as_ref().clone();
// 评审者不许联网搜索、不许用协作工具、不许再拉子 agent——堵死它"出去找别的活干"的路
sub_agent_config.web_search_mode.set(WebSearchMode::Disabled);
let _ = sub_agent_config.features.disable(Feature::SpawnCsv);
let _ = sub_agent_config.features.disable(Feature::Collab);
let _ = sub_agent_config.features.disable(Feature::MultiAgentV2);

// 换上评审准则
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
// 关键一锁:审批策略钉死成 Never
sub_agent_config.permissions.approval_policy =
    Constrained::allow_only(AskForApproval::Never);

逐行解读这几行为什么是本章最硬的证据:

  • web_search_mode = Disableddisable(Collab)disable(MultiAgentV2)disable(SpawnCsv):评审者被剥掉了联网、协作、再拉子 agent、跑 CSV 工具的能力。它能干的事被收窄到"读这次改动、对照准则、产出判语"。这呼应第 6 章的主线魂——对 agent 可见(可调用)的才存在:你想让一个角色"只评不做",最可靠的办法不是在提示词里求它,而是从能力清单里把"做"删掉。一个连"联网查资料"都被掐断的评审者,会被迫把全部注意力收回到"眼前这份 diff 到底对不对"上,而不会跑题去搜索、去开协作、去拉新 agent——约束工具面,等于强行聚焦评判焦点。
  • approval_policy = Constrained::allow_only(AskForApproval::Never):审批策略被锁成 Never,而且用的是 Constrained::allow_only——意味着这个 sub-agent 无法在运行中把权限再调回来。它问不出"要不要执行这个写操作"的审批,也就从机制上保证了它不会动手改代码。第 10 章讲过 approval policy 是安全自治的总闸;这里它被借用为"独立性"的物理保证:评审者连请求修改的渠道都没有。
  • base_instructions = REVIEW_PROMPT:这才是"换提示词"那一层——但它只是冰山上方那一角。

把这四类收窄叠加起来,"独立"就不再是修辞,而是一组可验证的约束:独立的会话、独立的模型(可配 review_model)、独立且更窄的权限、独立的工具面。评审者和产出者共享的只有"被审的那份代码"。这正是引子里那句"做判必须分开"在 Rust 里的样子。

值得停下来体会一下"用 approval_policy = Never 来保证只看不改"这步的妙处。一个偷懒的设计会怎么做?大概是在提示词里写"你是评审者,请不要修改任何文件"——然后祈祷模型听话。但提示词是软约束:模型可能误解、可能被后续输入带偏、可能"为了把 bug 演示清楚"顺手改两行。Codex 没赌这个,它把约束下沉到了权限闸门:在 Codex 的执行模型里(第 10 章),任何带副作用的动作(写文件、跑命令)都要过审批;把审批策略锁成 Never,等于让所有这类请求自动被拒——评审者哪怕一时糊涂想动手,也会在闸门处碰壁。这是"软约束 vs 硬约束"的经典分野:想让一个角色绝对不做某事,别在提示词里求它,去把那条路从机制上堵死。 同理,联网/协作/再拉子 agent 也不是靠提示词"请勿使用",而是直接 disable 掉对应 Feature——能力不存在,诱惑也就不存在。

〔配图 2:受限 sub-agent 的"减法"〕——中间一个完整 agent 的能力清单(web 搜索 / Collab / MultiAgentV2 / 写操作审批 / …)。一道"REVIEW"闸门把它复制成右侧的评审者,但闸门上挂着四把红锁,逐一划掉:联网 ❌、协作 ❌、再拉子 agent ❌、approval=Never ❌(写操作无门)。评审者手里只剩一支红笔和一份 diff。底部:"独立 = 把'做'从能力清单里删掉。"

评审输出:结构化的判语,而非闲聊

评审跑完,process_review_events 监听子会话的事件流,在 TurnComplete 时把最后一条 assistant 消息交给 parse_review_output_event 解析。这里有个鲁棒性细节值得一提:它先尝试整体反序列化成 ReviewOutputEvent,失败就截取第一个 { 到最后一个 } 的子串再试,再不行就把原文塞进 overall_explanation 兜底:

fn parse_review_output_event(text: &str) -> ReviewOutputEvent {
    if let Ok(ev) = serde_json::from_str::<ReviewOutputEvent>(text) { return ev; }
    // 退一步:从文本里抠出第一个 JSON 对象
    if let (Some(s), Some(e)) = (text.find('{'), text.rfind('}')) /* … */ { /* 再试一次 */ }
    ReviewOutputEvent { overall_explanation: text.to_string(), ..Default::default() }
}

这反映了一个工程现实:模型不一定严格吐 JSON,harness 得自己扛起容错。评审的产出是结构化的 findings(每条带标题、正文、置信度、优先级、代码位置)外加一句 overall_correctness 判决——这套 schema 不是凭空来的,正是评审准则里逐字规定的。

"受限子会话"是怎么起来又怎么收的

start_review_conversation 最后那一步——run_codex_thread_one_shot(定义在 core/src/codex_delegate.rs)——值得单独说,因为它是"独立"二字在运行时的载体。它的签名透露了三件事:

pub(crate) async fn run_codex_thread_one_shot(
    config: Config,                      // 已被收窄过的 sub_agent_config
    /* … */
    parent_session: Arc<Session>,        // 共享父会话(用于事件回传)
    parent_ctx: Arc<TurnContext>,
    cancel_token: CancellationToken,
    subagent_source: SubAgentSource,     // 这里传 SubAgentSource::Review
    final_output_json_schema: Option<Value>,
    initial_history: Option<InitialHistory>,
) -> Result<Codex, CodexErr>

几个落点:第一,它内部用了 cancel_token.child_token()——子会话挂在一个子取消令牌上,父任务一旦被打断(第 3 章讲过的 abort 链路),评审会话能被连带干净地停掉,而不会变成游离的僵尸 turn。第二,subagent_source = SubAgentSource::Review 把这次子会话在遥测和 UI 里标记成"评审来源",与日常对话区分开——又一次"对 agent / 对人可见的才存在"。第三,注意评审用的模型可以单独配start_review_conversationlet model = config.review_model.clone().unwrap_or_else(|| ctx.model_info.slug.clone());——你可以让一个更强或更挑剔的模型专门当评审者,产出者用另一个模型。这就把第六节"评估者价值随模型能力移动"的洞见,变成了一个可调的旋钮:评审者和产出者不必是同一个模型

收场同样讲究。评审跑完,exit_review_mode 负责把判语重新缝回父会话:它把评审结论渲染成一条 user 消息(render_review_exit_success,带格式化的 findings 块)和一条 assistant 消息记进对话历史,再发一个 ExitedReviewMode 事件,最后 ensure_rollout_materialized() 确保这次评审被持久化。这里藏着一个细节——评审 turn 可能在任何一次常规用户 turn 之前就跑(比如一进来就 /review),所以它必须显式落盘,不能依赖常规 turn 的持久化路径。一句话:评审是个独立子会话,但它的判语要回流主线,否则 agent 后续根本"看不见"评审说了什么——而看不见,就等于不存在。

〔配图补充思路:可并入配图 2〕在评审者脚下画一条细回流箭头,把红笔写下的 finding 清单送回主会话的对话历史里,标注"判语回流主线——看不见就等于不存在"。


三、准则即品味:rubric.md 里写了什么

REVIEW_PROMPT 的来源链路是:core/src/lib.rspub use codex_prompts::REVIEW_PROMPT;codex_prompts crate 的 review_request.rspub const REVIEW_PROMPT: &str = include_str!("../templates/review/rubric.md");。所以真正的"评审品味"是一份 Markdown:codex-rs/prompts/templates/review/rubric.md。读它的真实条款,比任何方法论转述都有说服力。

开头第一句就给评审者定了立场:"You are acting as a reviewer for a proposed code change made by another engineer."(你在替另一位工程师评审一处拟议改动。)——"another engineer",刻意把产出者描述成"别人",从语言层面再强化一次独立性。

它最值得抄进自己 harness 的,是那几条"什么才算 bug"的判据(原文八条,摘其要):

  1. 必须真有影响——"meaningfully impacts the accuracy, performance, security, or maintainability"。
  2. 必须具体可操作——"discrete and actionable",不是"这代码库整体不行"这种空话。
  3. 不得超出本仓库的严谨度——"does not demand a level of rigor that is not present in the rest of the codebase"(在一堆一次性脚本里别强求详尽注释和输入校验)。
  4. 只挑这次改动引入的 bug——"The bug was introduced in the commit (pre-existing bugs should not be flagged)."
  5. 作者知道了真的会去修——"The author of the original PR would likely fix the issue."
  6. 不许靠脑补——"does not rely on unstated assumptions";不能光"猜"这改动可能影响别处,必须指出被证明受影响的具体代码

然后是对评语口吻的硬约束,这部分尤其见功力:

  • 评语简短,正文至多一段;代码片段不超过 3 行("should not include any chunks of code longer than 3 lines")。
  • 口吻就事论事、不指责、也不过度正面——并明确禁奉承"avoid phrasing like 'Great job …', 'Thanks for …'."

最狠的是那条"宁缺毋滥"的产出策略:"If there is no finding that a person would definitely love to see and fix, prefer outputting no findings."——没有值得修的就别硬挑,但凡挑就要"挑剔"到把每一条够格的都列全("Do not stop at the first qualifying finding")。最后每条 finding 还要打 P0–P3 优先级(P0 = Drop everything),并给出一个 overall_correctness 二元判决。

把这些条款连起来看,你会发现 rubric 干的事,正是引子里那个"全 A 学生"缺的东西:它用规则把模型的"自我说服"逼回墙角——不许夸、不许脑补、不许翻旧账、不许凑数、必须给位置、必须给优先级、必须给最终判决。评判的品味,被写成了可执行的约束。

还有一处设计很容易被忽略,但极能说明 harness 工程的心思。rubric.md 在正文里专门留了一句"逃生口":它说这些只是默认通则"In many cases, you will encounter other, more specific guidelines…Those guidelines should be considered to override these general instructions."(很多时候你会在 developer 消息、user 消息、甚至某个文件里遇到更具体的准则,那些应当覆盖这套通则。)这等于在最底层的评审准则里,先天留好了"被项目特定规则覆写"的接口——既给了 Codex 一个稳定的评审地基,又允许具体仓库(比如靠第四节那组 code-review 技能)往上叠更严的标准。这正是第 4 章上下文工程那条魂的回声:准则不是一块铁板,而是分层、可覆写的拼装件。 通用底座写在 crate 里,仓库特例写在 skill 里,临场要求写在 developer / user 消息里,优先级层层递增。

主线魂回声:"模型是商品、harness 是杠杆。"同一个模型,套上 ReviewTask 的受限配置 + rubric.md 的硬条款,就从"糟糕的 QA"变成一个像样的评审者。变的不是模型,是它周围的 harness。


四、把评审标准再上一层:打包成 skill

rubric.md 是 Codex 内置 review 的总准则。但仓库里还有另一套更细、更可组合的评审知识——一组 code-review 技能(第 8 章的 skill 机制),住在 /.codex/skills/ 下:

  • code-review(编排者):它的 SKILL.md 直接说——"Use subagents to review code using all code-review-* skills…One subagent per skill"。一个技能配一个子 agent,又是一次"做判分离"的细分。
  • code-review-breaking-changes:盯外部集成面的破坏性变更(app-server API、CLI 参数、配置加载、从旧 rollout 恢复会话),并强调"别找到一个就收手"。
  • code-review-change-size:改动规模红线——"非机械改动总行数不应超过 800 行",复杂逻辑应控制在 500 行内,超了就要给出可拆分的最小可落地阶段。
  • code-review-testing:测试取向——"agent 改动优先集成测试"(放在 core/suite、用 test_codex),改 agent 逻辑的特性必须加集成测试。
  • code-review-context:模型可见上下文的纪律——不许重写历史、避免频繁改动导致缓存失效、注入项必须有硬上限(>10K token 不行、跨 1K token 的新项标 P0 需人工复核)。

把"评审标准"做成技能而非散落在提示词里的好处,是第 8 章那套魂的延续:标准被写下来、可复用、可组合、可演进,且按需加载。人的 review 经验——"改动别超 800 行""注入上下文要有上限"——就这样沉淀成了 agent 可读、可执行的工件。注意它和内置 review 的分工:rubric.md 是"什么算 bug"的通用底座,code-review 技能是"这个仓库特别在意什么"的专项清单,二者叠加。

这套分工还呼应了第 12 章的多代理编排。code-review 编排者"一技能一子 agent"的打法,和内置 ReviewTask 起受限 sub-agent 是同一种思路在不同粒度上的体现:内置 review 是一个受限评审者审全局,code-review 技能则把评审横切成多个专题,每个专题派一个子 agent 各管一摊(破坏性变更归一个、测试归一个、上下文归一个),最后由编排者汇总。粒度更细带来两个好处:每个子 agent 的注意力不被稀释(只盯一个维度更不容易"漏看"),且各维度的标准能独立演进——哪天发现"800 行"这条红线太松,只改 code-review-change-size 一个技能即可,不动其余。这正是把评估能力模块化的价值:评判这件事本身,也可以被拆成可组合、可独立迭代的小块。

〔配图 3:code-review 技能货架〕——一个货架上摆着几个"评审标准"技能盒:破坏性变更 / 改动规模(800 行红线) / 测试覆盖 / 上下文上限。一个 code-review 编排 agent 从货架取下每个盒子,各派一个子 agent 逐项对照 diff 打勾打叉,最后汇总所有 finding。底部:"把评审品味,沉淀成可复用、可组合的技能。"


五、从"一次性审"到"持续对抗":generator ↔ evaluator

到此为止,Codex 的内置 review 是一种一次性的独立审:起一个受限 sub-agent,审一遍,出判语,退场。Anthropic 在前端设计实验里把它推到了极致,做成一个持续对抗的循环——明确受 GAN(生成对抗网络)启发:"Taking inspiration from Generative Adversarial Networks (GANs), I designed a multi-agent structure with a generator and evaluator agent."(这正是第 12 章多代理编排埋下的伏笔。)这种"一个 LLM 生成、另一个 LLM 评估并反馈、循环往复"的模式,Anthropic 在更早的《Building Effective AI Agents》里就归纳为 evaluator-optimizer 工作流——"One LLM call generates a response while another provides evaluation and feedback in a loop."

机制很直白:

  • generator 做出东西。注意它不是一口气把整个 app 写完,而是工作在 sprint 里:每一轮从规格里挑一个 feature 实现(这正是第 11 章长程作业那套"分块推进、别一次吃太多"的思路在生成侧的体现);
  • evaluator 拿着一套评分准则真去用一用、挑毛病、逐项打分、写详细批语;
  • 任何一项不达标,这一轮就判失败、连同具体毛病一起打回给 generator;
  • generator 照着反馈改,再交,再判……一轮轮逼近高质量。

这条循环和上半章 Codex 的一次性 review 是同一招的两种火力档位:Codex 的 ReviewTask 是"审一遍、出判语、退场"的单发;前端实验是"审—打回—再审"的连发。但内核完全一致——独立的评判者 + 一套把判断标准写死的准则 + 让评判者真去用产物。区别只在于"打回去再来一轮"这个回环是否闭合,而这取决于第六节那个问题:任务难度有没有超出 generator 单发能可靠搞定的范围。

它的评分准则很有讲究。Anthropic 在前端设计上用了四条"Design quality…Originality…Craft…Functionality",并刻意给"设计质量"和"原创性"更高权重"I emphasized design quality and originality over craft and functionality")——因为模型默认就能把"工艺/可用性"做得不错,却容易产出平庸。一个反复被引用的发现:准则里写一句 "the best designs are museum quality",真的能把产出推向某种更高级的视觉收敛——作者直言 "the prompting associated with the criteria directly shaped the character of the output." 评分准则的措辞,本身就在塑造产出。 这和上一节 rubric.md 那条"禁说 Great job"的精神是一致的:你写进准则里的每个字,都是在给模型的产出"调味"。

更狠的是 evaluator 不是看截图打分,而是 "I gave the evaluator the Playwright MCP, which let it interact with the live page directly"——真的把页面点一遍:像真用户一样点按钮、走流程、查状态,再打分。它挑出的 bug 具体到能直接动手修,比如那条 "Rectangle fill tool…FAIL — Tool only places tiles at drag start/end points instead of filling the region."(矩形填充工具只在拖拽起止点放了瓦片,没真的填满区域。)真去用,才挑得出真 bug——这点对所有做 agent 验收的人都成立。

〔配图 4:generator↔evaluator + 阈值闸〕——左 generator 做出产物 → 中间 evaluator 拿四条准则(设计质量/原创性/工艺/可用性,前两条更粗的权重箭头)逐项打分,每条一道"阈值闸";任一条低于阈值 → 整体 FAIL,带着具体 bug 列表打回 generator(一条回环箭头);全过 → 放行。evaluator 旁画一个 Playwright 浏览器图标"真去点一遍"。底部:"真去用,才挑得出真 bug。"

Codex 里的近似形态:guardian 复核回路

那 Codex 仓库里有没有这种"持续对抗"的影子?有——guardian 复核回路,可作为 generator-evaluator 在本仓库的近似对照(guardian 安全机制见第 10 章)。它住在 core/src/guardian/review_session.rs,是一条会反复跑、且把历次评审累计起来的复核会话:每完成一轮,它会 state.prior_review_count += 1、推进 last_reviewed_transcript_cursor——也就是说它记得自己审过几轮、审到哪儿了,这正是"对抗循环"才需要的状态。

更耐人寻味的是它注入给复核者的那段 reminder(core/src/context/guardian_followup_review_reminder.rs):

Use prior reviews as context, not binding precedent.
Follow the Workspace Policy.
If the user explicitly approves a previously rejected action after being
informed of the concrete risks, set outcome to "allow" …

"把历次评审当上下文、而非铁律"——这句话精准刻画了 generator-evaluator 循环里 evaluator 的微妙处境:它要在多轮之间保持一致的尺度,又不能被自己上一轮的结论绑死。Codex 把这条认知直接写进了复核者的上下文。它和前端实验那套不完全一样(guardian 复核的是"动作是否安全合规",不是"设计好不好看"),但骨架同源:独立的复核会话 + 跨轮累计的状态 + 一套约束复核者判断的准则。

而且 guardian 这条复核链路比普通 ReviewTask 还再往前走了一步:build_guardian_review_session_config 里不仅把 approval_policy 锁成 Never、权限画像切到 PermissionProfile::read_only(),还显式关掉 skill 注入include_skill_instructions = false)、清空 MCP server,并额外禁掉 AppsPluginsWebSearchRequestWebSearchCachedCodexHooks。这说明 Codex 对"复核者该看到什么、能调用什么"的态度非常严格:为了让判断更纯,宁可把可用能力一路削到只剩最必要的那层。 从 harness 工程角度看,这和 ReviewTask 那套"删能力保独立"是同一条设计哲学,只是 guardian 走得更彻底。

把本仓库的三种"评判"摆在一起看,能看清一条谱系:

形态 评什么 回合 跨轮记忆 评判者隔离手段
ReviewTask(内置 review) 这次改动有没有引入 bug 单发 受限 sub-agent(approval=Never + 禁工具)
guardian 复核回路 动作是否安全合规 多发、可累计 prior_review_count / transcript cursor 独立复核会话 + reminder 准则
前端 generator↔evaluator(Anthropic 实验) 设计/功能质量 连发、打回重做 隐含在 sprint 历史里 独立 evaluator agent + Playwright + 四条准则

三者评的对象不同、火力档位不同,但都恪守同一条铁律——让一个独立的、被准则约束的角色,去判另一个角色的产出。这就是本章想反复砸实的那颗钉子。


六、评估者很难调教——而且不总是值得

把 evaluator 调好,是个苦活。Anthropic 坦言早期它就是那个"全 A 学生":发现真问题,又自我说服没事,照样放行,还倾向浅尝辄止、不戳边界用例。调教办法很朴素:读 evaluator 的日志,找出它判断和你不一致的地方,改它的提示词,反复几轮,直到它的尺度和你对得上。(这条又把第 14 章的可观测性引出来了:要调评判者,先得能"看见"它的判断过程。)

这里有一个特别重要的"反框架"洞见(再次呼应全书基调):evaluator 不是永远都值得加。 它的价值取决于任务相对模型能力的位置——Anthropic 用 4.5 和 4.6 两代模型做了对照:在 4.5 上 "that boundary was close…the evaluator caught meaningful issues across the build"(任务正好卡在 generator 单独能力的边缘,evaluator 挑出大量真问题,很值);到 4.6 "the model's raw capability increased, so the boundary moved outward"(能力上移,很多原本要 evaluator 兜的任务 generator 自己就做好了,evaluator 反而成了多余的开销)。

这正是第 12 章那条结论在评估场景的复刻:harness 的每个组件都编码了一条"模型当下做不到什么"的假设。 evaluator 也不例外——它值不值得加,要看任务有没有超出模型单独能可靠完成的范围。模型变强,先去掉不再承重的组件。 这也解释了为什么 Codex 把 review 做成可选的、按需触发的 task,而不是每轮都强制塞一个评审者:harness 随模型演化,评估这一层尤其要随之增减。

〔配图 5:评估者值不值得(随模型能力移动)〕——一根横轴"模型能力"从弱(4.5)到强(4.6);一条"任务难度线"。弱模型端:任务落在 generator 能力边缘之外,evaluator 站在缺口处补位,标"很值"。强模型端:generator 能力线上移盖过任务,evaluator 闲站一旁,标"成了多余开销"。底部:"模型变强,先撤掉不再承重的组件。"


七、可验证的 vs 主观的

最后一个分野,决定了你该花多大力气造评估器。评估任务大体分两种:

  • 可验证的(如代码):有近乎二元的判据——测试过没过、API 通没通、页面点得动不动。评估相对好做,rubric.md 那套"是不是引入了 bug / patch is correct?"就属这类。
  • 主观的(如设计、文案):"好不好看"是判断,没有二元判据。Anthropic 直言 "Whether a layout feels polished or generic is a judgment call." 这也正是 evaluator-optimizer 那条"适用条件"的另一面——它真正发力,得在 "clear evaluation criteria""iterative refinement provides measurable value" 的场景;主观任务缺的恰是前者,于是只能靠人为它造出一套可打分的准则。

但别把这条线画太死——同一篇文章还补了一句很清醒的话:"even on tasks that do have verifiable outcomes, agents still sometimes exhibit poor judgment."哪怕任务有可验证的结果,agent 有时照样判断失准。)所以可验证 ≠ 可以放心交给模型自评,那个"全 A 学生"的毛病在两类任务里都会冒头。

主观任务的破解之道,正是第五节那招:把"这设计好吗"这种没法稳定回答的问题,换成"它符不符合我们这套设计准则"这种有具体抓手的问题——四条带权重的准则、一句"博物馆级"、Playwright 真去点一遍。主观的东西,靠一套写清楚、且能实际去"用"的准则,也能变得可打分。而 Codex 的 rubric.md 则示范了可验证那一侧怎么写准则:把"对不对"拆成八条"什么才算 bug"的硬判据。两侧用的是同一招——用准则给模糊判断装上抓手。

最后留一个值得记住的递归:评估器自己也是被评估的对象。 第六节讲的"读日志、对齐尺度、改提示词",本质就是你在给评估器做评估——把它的判断和你的判断比对,不一致就调。这条链没有尽头,但它有一个稳定的支点:人定义"什么算对",harness 把这个定义编码成准则与受限角色,模型在约束内执行判断,运行信号再回流给人去校准准则。 评估这一层之所以是 harness 工程的高地,正因为它是这条校准回路的中枢——它既要判产出,自己又被人判。你写下的每一条 rubric、每一个被 disable 的 Feature、每一把 Never 锁,都是在这条回路上多拧紧一圈——让那个会给自己打全 A 的学生,慢慢学会了诚实地挑自己的错。


本章小结

  • 做判必须分开:模型给自己打分总偏高("开箱即用是个糟糕的 QA"——会发现真 bug 又说服自己放行);评估这一层的根本,是引入独立的评判者。
  • ReviewTask 的真相:它不是"同工具、同循环、换个提示词",而是用 run_codex_thread_one_shot 起一个受限的独立 sub-agent——web_search=Disabled、禁 Collab/MultiAgentV2/SpawnCsvapproval_policy 锁成 Never。"独立""只看不改"被焊进了配置,而非靠提示词哀求。
  • 准则即品味:评审准则真正住在 prompts/templates/review/rubric.md(经 REVIEW_PROMPT 重导出)。它逐条规定:只挑本次引入的 bug、不翻旧账、不脑补、宁缺毋滥、禁奉承("别说 Great job")、代码片段 ≤3 行、每条打 P0–P3、给二元判决。
  • 标准可打包成 skillcode-review 编排者按"一技能一子 agent"派活,分项检查破坏性变更 / 800 行规模红线 / 集成测试 / 上下文上限。
  • GAN 式 generator↔evaluator:受 GAN 启发的对抗循环——四条带权重的准则(设计质量/原创性最重)、用 Playwright 真去点一遍、不达标就打回;准则措辞本身在塑造产出("博物馆级")。Codex 的 guardian 复核回路(跨轮累计 prior_review_count + "历次评审当上下文非铁律"的 reminder)是它在本仓库的近似形态。
  • 评估者难调教、且不总值得:靠读日志对齐尺度;它的价值取决于任务相对模型能力的位置——4.5 上很值、4.6 上常成多余开销。模型变强,先撤掉不再承重的 evaluator。
  • 可验证 vs 主观:把"好不好"换成"合不合准则",主观质量就有了抓手;但即便可验证任务,agent 也会判断失准——独立评判这层不能省。

下一章,我们看评估的"近亲"——可观测性:怎么让日志、指标、追踪这些运行时信号,不只给人看,还能回喂给 agent,让它自己看见问题、自己修(也才能像本章第六节那样,去"读 evaluator 的日志"把它调准)。


参考来源

解剖标本(codex-rs 源码)

  • core/src/tasks/review.rsReviewTask 起受限 sub-agent
  • core/src/tasks/mod.rsSessionTaskcodex.task.review 计数
  • prompts/templates/review/rubric.md — 评审准则原文
  • prompts/src/review_request.rsREVIEW_PROMPT 重导出
  • core/src/lib.rsreview_prompts / REVIEW_PROMPT 再导出
  • core/src/guardian/review_session.rs — guardian 复核回路
  • core/src/context/guardian_followup_review_reminder.rs — "非铁律" reminder
  • .codex/skills/code-review(含 -breaking-changes/-change-size/-testing/-context)— 评审技能货架

方法论

存疑/边界说明:(1) guardian 复核回路与前端 generator-evaluator 实验目的不同(前者复核动作安全合规、后者评设计质量),本章仅作"独立复核 + 跨轮状态 + 准则约束"的骨架类比,非等价实现。(2) Anthropic 该文的部分实践(四条准则权重、成本对照等)来自其前端设计实验,不是 Codex 仓库内的代码事实;凡属源码者已标 Codex 路径,凡属 Anthropic 实验者已注明出处。(3) review 子系统的事件流(ReviewOutputEvent 字段、退出仪式 XML 等)细节随版本演进,以你 clone 到的版本为准。

留言