第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 状态,不会自动回退到 pending。needs_fix 是一个显式的"失败 + 需要修改"状态——不是"回到起点"。
11.2 客观 vs 主观
检查门和审查门的分界线是"客观检查 vs 主观判断"。
检查门处理客观检查。 这些检查的特点是:结果无歧义。编译通过了吗?退出码是 0 还是非 0?模板有残留吗?grep "TODO" 返回结果了吗?文件够数吗?count >= threshold?
客观检查的好处是可自动化、可重复。同一个检查在 CI 和本地执行的结果完全一致。如果 agent 声称"编译通过了",检查门可以通过重新运行编译命令来验证——不需要相信 agent 的输出。
审查门处理主观判断。 这些判断的特点是:没有标准答案,但可以通过结构化评分减少随意性。四信号 rubric(foundation、over_engineering、volume、terminology)的结构体定义见第 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_specs和review_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 结构体(foundation、over_engineering、volume、terminology 四个字段)在第 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
}
Rebuild 比 NeedsFix 更严重——它表示"基础结构有问题,修复成本接近重写"。
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_auto:
max_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 自主运行——无用户模式下的完整工程实践。