法不净空,觉无性也。

第11章 客观验收门与结构化审查

2026.07.29

"做完了"不是 agent 说了算——是三级门串联验证后系统说了算。


模式层

11.1 三级验收门

验收门(Acceptance Gate)是里程碑从"in_progress"到"done"的必经检查。CodeCoder 定义了三级门,串联执行:

第一级:命令门(Command Gate)。

最简单的验收方式:agent 调用 milestone done 并附上自评。系统记录"agent 说做完了",然后继续到下一级门。

命令门的信任假设是"有一个人在循环里"。交互式模式下,用户可以看到 agent 的输出并判断是否真的完成。如果用户觉得不对,可以把里程碑打回 needs_fix

但命令门本身不做任何验证——它只是接收 agent 的声明。它的存在是为了给验收 pipeline 一个"起点":agent 声明完成后,系统知道"开始验收了"。

第二级:检查门(Check Gate)。

检查门引入与 agent 无关的确定性检查。CodeCoder 定义了四种 CheckSpec:

  • BuildExitZero:编译/测试命令的退出码是否为 0
  • NoTemplateContent:生成的文件不包含模板残留("TODO"、"placeholder"、"your code here")
  • FileCountMin:创建了足够数量的文件
  • MinLinesPerFile:生成的代码不是空壳

检查门的核心特征是确定性。退出码要么是 0 要么不是,文件要么存在要么不存在,模板残留要么有要么没有。没有 LLM 判断,没有"看起来合理"的模糊地带。

第三级:审查门(Review Gate)。

审查门引入一个独立的只读子 agent,用结构化 rubric 评审输出质量。子 agent 读文件、读 diff、读代码结构,然后返回一个结构化的 Verdict。

审查门处理的是检查门无法处理的问题:架构合理性、过度设计、术语一致性。这些问题没有"对/错"的二进制答案,但可以通过结构化 rubric 转化为可比较的信号。

三级门的执行顺序是串联的:

milestone done(命令门)
CheckSpec 检查(检查门)——失败 → needs_fix
审查子 agent 评审(审查门)——失败 → needs_fix
里程碑状态:done

任一扇门 fail 后,里程碑进入 needs_fix 状态,不会自动回退到 pendingneeds_fix 是一个显式的"失败 + 需要修改"状态——不是"回到起点"。

11.2 客观 vs 主观

检查门和审查门的分界线是"客观检查 vs 主观判断"。

检查门处理客观检查。 这些检查的特点是:结果无歧义。编译通过了吗?退出码是 0 还是非 0?模板有残留吗?grep "TODO" 返回结果了吗?文件够数吗?count >= threshold?

客观检查的好处是可自动化、可重复。同一个检查在 CI 和本地执行的结果完全一致。如果 agent 声称"编译通过了",检查门可以通过重新运行编译命令来验证——不需要相信 agent 的输出。

审查门处理主观判断。 这些判断的特点是:没有标准答案,但可以通过结构化评分减少随意性。四信号 rubric(foundationover_engineeringvolumeterminology)的结构体定义见第 5 章 5.5 节,本章仅展开评分标准和合并规则。

  • "这个模块的接口设计是否过度抽象了?"——不同的人可能有不同的判断
  • "新代码的术语是否与项目一致?"——需要理解项目上下文才能判断

审查门用 rubric 把主观判断转化为可比较的信号。四个信号(foundation、over_engineering、volume、terminology)各自有评分标准,LLM 根据标准打分。四个信号合并为一个 Verdict。这个过程仍然是 LLM 判断(非确定性),但它比"这段代码怎么样?"的开放式问题更可控。

11.3 漂移信号

验收门解决的是"做完时"的质量问题。但架构漂移——代码逐渐偏离原始设计——往往是累积的结果,而不是单次变更的问题。

四信号 rubric 中的 terminology 信号专门用于检测术语漂移:新代码是否使用了 CONTEXT.md 中禁止的术语?over_engineering 信号检测设计漂移:是否引入了不必要的抽象?

验收门不是漂移检测的唯一工具,但它是最后一道防线。如果前期的代码审查、方案讨论都没有发现问题,验收门至少能在"做完时"捕获架构漂移。


案例层

11.4 三级验收 pipeline

三级验收门的完整执行流程如下:

graph LR
    subgraph "里程碑状态:in_progress"
        AGENT[Agent 执行任务]
    end

    subgraph "第一级:命令门"
        REPORT[agent 调用<br/>milestone done]
        NOTE[附自评]
    end

    subgraph "第二级:检查门"
        BUILD[BuildExitZero<br/>编译退出码=0]
        TEMPL[NoTemplateContent<br/>无模板残留]
        FILE[FileCountMin<br/>文件数达标]
        LINES[MinLinesPerFile<br/>代码行数达标]
    end

    subgraph "第三级:审查门"
        REVIEW[只读子 agent<br/>架构评审]
        SIGNAL[四信号 rubric<br/>foundation<br/>over_engineering<br/>volume<br/>terminology]
    end

    subgraph "验收结果"
        PASS[Verdict::Pass<br/>→ done]
        NFIX[Verdict::NeedsFix<br/>→ 修复]
        RB[Verdict::Rebuild<br/>→ 重建]
    end

    AGENT --> REPORT
    REPORT --> BUILD
    BUILD --> TEMPL
    TEMPL --> FILE
    FILE --> LINES

    LINES -->|检查全部通过| REVIEW
    LINES -->|任一检查失败| NFIX

    REVIEW --> SIGNAL
    SIGNAL -->|全部正常| PASS
    SIGNAL -->|foundation < 0.3| RB
    SIGNAL -->|其他信号异常| NFIX

    PASS -->|状态:done| FINAL([里程碑完成])
    NFIX -->|自恢复循环| AGENT
    RB -->|人工介入| FINAL

    style PASS fill:#e8f5e9
    style NFIX fill:#fff3e0
    style RB fill:#fce4ec

验收 pipeline 的完整伪代码实现:

fn verify_milestone(milestone: &Milestone, context: &Context) -> Result<Verdict> {
    // 1. 命令门——agent 自报完成
    // milestone.done() 已经在外部调用,这里不做额外处理

    // 2. 检查门——确定性检查
    if let Some(specs) = &milestone.check_specs {
        for spec in specs {
            let result = match spec {
                CheckSpec::BuildExitZero { command } => {
                    let output = context.run_command(command)?;
                    if !output.status.success() {
                        return Ok(Verdict::NeedsFix {
                            reason: format!("build failed: {}", output.stderr),
                        });
                    }
                }
                CheckSpec::NoTemplateContent { patterns } => {
                    for pattern in patterns {
                        let matches = context.grep(pattern)?;
                        if !matches.is_empty() {
                            return Ok(Verdict::NeedsFix {
                                reason: format!("template content found: {:?}", matches),
                            });
                        }
                    }
                }
                CheckSpec::FileCountMin { min } => {
                    let files = context.list_created_files()?;
                    if files.len() < *min {
                        return Ok(Verdict::NeedsFix {
                            reason: format!("expected {} files, got {}", min, files.len()),
                        });
                    }
                }
                CheckSpec::MinLinesPerFile { min } => {
                    for file in context.list_created_files()? {
                        let lines = context.count_lines(&file)?;
                        if lines < *min {
                            return Ok(Verdict::NeedsFix {
                                reason: format!("{}: {} lines, expected {}", file, lines, min),
                            });
                        }
                    }
                }
            };
        }
    }

    // 3. 审查门——架构级评审
    if let Some(review_config) = &milestone.review_config {
        let verdict = context.run_review_agent(review_config)?;
        if !matches!(verdict, Verdict::Pass) {
            return Ok(verdict);
        }
    }

    Ok(Verdict::Pass)
}

pipeline 的关键设计点:

  • 检查门和审查门都是可选的(通过 milestone 的 check_specsreview_config 字段控制)。不是每个里程碑都需要三级门全部通过——大部分里程碑只需要命令门 + 检查门
  • 检查门的四种检查是并联的(所有检查都要通过),不是串联的(前一个通过才执行下一个)
  • 审查门只在里程碑配置了 review_config 时才执行——默认不开启

11.5 CheckSpec 四种检查的评分标准

BuildExitZero:

fn check_build_exit_zero(command: &str) -> CheckResult {
    let output = run_command(command);
    if output.status.success() {
        CheckResult::Pass
    } else {
        CheckResult::Fail(format!(
            "exit code: {}, stderr: {}",
            output.status.code().unwrap_or(-1),
            output.stderr
        ))
    }
}

BuildExitZero 是最常用的检查——几乎每个里程碑都应该配置。它验证 agent 的输出至少是"可编译的"。

NoTemplateContent:

fn check_no_template_content(patterns: &[&str]) -> CheckResult {
    let files = list_created_files();
    for file in files {
        for pattern in patterns {
            if grep_file(&file, pattern).is_some() {
                return CheckResult::Fail(format!("{} contains '{}'", file, pattern));
            }
        }
    }
    CheckResult::Pass
}

默认的 patterns 是 ["TODO", "FIXME", "placeholder", "your code here", "implement me"]。这些模式是 agent 生成代码时最容易留下的模板残留。

FileCountMin / MinLinesPerFile:

这两个检查用于防止 agent 生成空壳代码(文件存在但内容为空,或只有几行)。FileCountMin 确保 agent 创建了足够数量的文件,MinLinesPerFile 确保每个文件不是空壳。

fn check_min_lines_per_file(min_lines: usize) -> CheckResult {
    let files = list_created_files();
    for file in files {
        let count = count_lines(&file);
        if count < min_lines {
            return CheckResult::Fail(format!(
                "{} has {} lines, minimum {}",
                file, count, min_lines
            ));
        }
    }
    CheckResult::Pass
}

11.6 Review Verdict 四信号 rubric

审查门的评分标准(以信号值 0.0-1.0 评分)。ReviewSignals 结构体(foundationover_engineeringvolumeterminology 四个字段)在第 5 章 5.5 节已定义,本章给出各信号的详细评分标准:

foundation(基础结构完整性):

  • 1.0:模块结构完整,类型声明齐全,公共接口清晰
  • 0.7:结构基本完整,有少量缺失但可修复
  • 0.3:结构缺失关键模块或类型
  • 0.0:几乎不可用

over_engineering(过度设计):

  • 1.0:完全不必要的抽象,或实现了未要求的通用性
  • 0.7:抽象级别略高,但尚可接受
  • 0.3:设计合理,没有明显过度
  • 0.0:设计恰到好处

volume(变更规模):

  • 1.0:单次变更范围远超任务描述(可能做了未要求的事)
  • 0.7:范围略大,但大部分相关
  • 0.3:范围合理
  • 0.0:范围过小,未完成任务

terminology(术语一致性):

  • 1.0:使用了 CONTEXT.md 中禁止的术语,或创造了与现有术语冲突的新术语
  • 0.7:术语使用有少量不一致
  • 0.3:术语使用一致
  • 0.0:术语使用完美

Verdict 合并规则:

fn merge_signals(signals: &ReviewSignals) -> Verdict {
    if signals.foundation < 0.3 {
        return Verdict::Rebuild;
    }
    if signals.foundation < 0.5 || signals.over_engineering > 0.7
        || signals.volume > 0.7 || signals.terminology > 0.7 {
        return Verdict::NeedsFix;
    }
    Verdict::Pass
}

RebuildNeedsFix 更严重——它表示"基础结构有问题,修复成本接近重写"。

11.7 needs_fix → 自恢复循环

当验收门 fail 后,里程碑进入 needs_fix 状态。如果是在 headless 模式(无用户在场),系统启动自恢复循环(该循环的设计演变历史见第 12 章 ADR 深度阅读):

fn needs_fix_recovery(milestone: &mut Milestone, context: &Context) -> Result<()> {
    let max_attempts = context.config.bg_max_fix_attempts;  // 默认 3
    let mut attempts = 0;

    while attempts < max_attempts {
        // 1. 将失败原因注入修复 prompt
        let fix_prompt = format!(
            "里程碑 '{}' 验收失败,原因:{}。请修复这些问题。",
            milestone.name,
            milestone.fix_reason
        );

        // 2. 重新执行里程碑
        context.execute_fix(fix_prompt)?;

        // 3. 重新验收
        let verdict = verify_milestone(milestone, context)?;
        if matches!(verdict, Verdict::Pass) {
            milestone.status = MilestoneStatus::Done;
            return Ok(());
        }

        // 4. 更新失败原因
        milestone.fix_reason = format!("{}. Retry {} failed: {}", 
            milestone.fix_reason, attempts + 1, verdict.reason());
        attempts += 1;
    }

    // 5. 预算耗尽,报告 stuck
    Err(Error::StuckNeedsFix {
        milestone: milestone.name.clone(),
        reason: milestone.fix_reason.clone(),
        attempts,
    })
}

自恢复循环的关键设计点:

  • 有界重试:默认 3 次(bg_max_fix_attempts 可配置,0 = 禁用自动重试)。超过预算仍然 fail 的里程碑,报告 StuckNeedsFix,等待人工介入
  • 失败原因累积:每次重试的失败原因都会追加到 fix_reason 中——agent 在修复时能读到前一次失败的完整原因
  • 重试不计入 max_automax_auto 是"agent 自主完成的里程碑数",自恢复循环中的重试不消耗这个预算

11.8 Self-report 被 objective gate 覆盖的演进

CodeCoder 早期版本中,里程碑的完成状态完全依赖 agent 的自报(命令门)。Agent 说"做完了"就是做完了。

问题在 headless 模式中暴露:agent 自报完成,但编译失败了。agent 没有运行编译命令就直接报告了完成——因为它的验收标准是"文件已修改",不是"文件已修改且编译通过"。

解决方案是引入检查门(BuildExitZero 检查)。检查门在命令门之后执行,覆盖 agent 的自报。如果 agent 说"做完了"但编译失败,检查门会将里程碑打回 needs_fix

这是 CodeCoder 验收体系演进的一个缩影:从信任 agent 的自报到用确定性检查覆盖自报,再到用结构化审查覆盖检查。 每一步都是因为前一级门在某次实际执行中暴露了盲区。


ADR 深度阅读

审查门与自恢复的引入(ADR 0039)

ADR 0039 记录了审查门和自恢复循环的同时引入。

引入前,headless 模式的验收完全依赖命令门 + 检查门。Agent 自报完成 → 检查门验证编译和文件数 → 通过则 milestone done。问题在于:Agent 可以通过检查门但架构有问题。编译通过但模块依赖方向反了、文件数够了但全是空壳、术语一致但逻辑不对。

审查门引入了一个独立的 LLM 调用评审架构质量。但评审 agent 也是 LLM 驱动的,它的判断同样可能出错。为了防止评审 agent 的误判影响工作流,审查门的 Verdict 被设计为 可被检查门覆盖 的——如果审查 agent 说"needs_fix"但检查门说"全部通过",系统优先相信检查门(因为检查门是确定性的)。

自恢复循环的引入是因为另一个问题:Agent 在 needs_fix 状态下不知道下一步该做什么。早期版本中,里程碑进入 needs_fix 后,agent 需要用户手动将其设回 pending 或 in_progress。在 headless 模式下,没有用户来做这个操作。自恢复循环通过将失败原因注入修复 prompt,让 agent 自动重新执行。


下一章进入 headless 自主运行——无用户模式下的完整工程实践。