DAY 54 / PHASE 6 · ENGINEERING

Oracle 盘点

Test Oracle · 独立性陷阱 · 按可验证性重排自动化边界

2026-07-06 · BigCat

能不能放手让 AI 自动跑,不取决于它多聪明,取决于有没有一个独立于它的裁判。

// WHY THIS MATTERS

你手里的 AI 工作流里,每一个「自动化环节」背后都藏着一个问题:谁来判断这一步做对了? 软件测试里这叫 test oracle problem——判断「观察到的行为是否正确」的那个裁判。当裁判是编译器、类型检查、测试、property check 时,它独立于生成代码的模型,说对就是对;当裁判是「让 AI 再看一遍」「多个 agent 辩论」「self-consistency 投票」时,它和生成者共享失败模式——是同一个大脑批改自己的卷子。这一期不讲「怎么写更好的 eval」,讲一件更底层的事:给自己的整条 AI 流水线做一次 oracle 盘点,标出哪些环节有独立裁判、哪些是 AI 看 AI,然后按可验证性、而不是按任务难度重新画自动化边界。这决定了你哪些地方能真正放手,哪些地方一放手就是在放大一个没有裁判的系统。

// 01

什么是 Oracle:以及大多数人用的是「假 oracle」

论断:一个验证环节的价值不取决于它多严格,而取决于它是否独立于被验证者的失败模式。

背景与原理

Barr 等人 2015 年那篇被反复引用的 oracle 综述把「验证」拆成了一个很干净的问题:test oracle 就是能回答「这个输出对不对」的信息源。它按独立性和可自动化程度分四类,这个分类直接决定你能不能放手:

关键洞察在这:编译器、类型检查、单元测试、property-based test 是强独立 oracle;而「让 LLM 再检查一遍」「LLM-as-judge」「self-consistency」是弱 oracle 甚至伪 oracle——因为验证者和生成者是同一个(或同族)模型,它们的盲区高度重叠。你以为加了一道验证,其实只是让同一个大脑把答案又抄了一遍。盘点的第一步,就是对流水线里每个环节问:这里的裁判,是哪一类?

实战示例

给你的工作流列一张 oracle 盘点表——每个自动化环节一行,标注裁判类型与独立性:

# 环节                裁判              类型         独立于模型?
生成函数实现        typecheck+pytest   implicit/spec  是 ✅  → 可全自动
重构等价改写        回归测试快照        derived        是 ✅  → 可全自动
SQL 生成            在只读库上 EXPLAIN  implicit       是 ✅  → 可全自动
文档摘要质量        LLM-as-judge       伪 oracle       否 ❌  → 需人抽检
「这段代码安全吗」  另一个 LLM 说安全   伪 oracle       否 ❌  → 需独立扫描/人
需求是否理解对      —(无)            no oracle      否 ❌  → 必须人

贴在项目根目录。规则很简单:「独立于模型」那一列是 ✅ 的行,才有资格全自动放手;❌ 的行要么找到一个真 oracle,要么保留人在环。

失败模式:把「通过了 LLM review」当成「验证过了」。LLM review 能抓到低级错误(有信号),但它系统性地放过生成者和它共有的那类错误——而那恰恰是最危险、最需要拦住的一类。伪 oracle 的危害不是没用,是制造了「已验证」的假象,让你误以为可以放手。
进阶资源 · Barr, Harman, McMinn, Shahbaz, Yoo The Oracle Problem in Software Testing: A Survey (IEEE TSE 2015), discovery.ucl.ac.uk (PDF) · Anthropic Building Effective Agents, anthropic.com/engineering
// 02

同源验证的独立性陷阱:AI 看 AI 为什么不算验证

论断:用同一族模型生成又验证,错误是相关的;self-preference 让它系统性放过自己的错——双重检查≈单次检查。

背景与原理

为什么「独立」是核心词?做一点粗糙的概率直觉:如果 generator 漏检率是 p,再叠一道完全独立的 verifier(漏检率 q),双漏概率降到 p·q——平方级下降,这才是「双重检查」的价值来源。但如果 verifier 和 generator 的错误高度相关(相关系数 ρ→1),双漏概率退回到 ≈ p——你什么都没多买到。同一个模型自查、同族模型互查,正是 ρ 很高的情形:它们在同样的地方自信地错。

更糟的是 self-preference bias。Panickssery 等人 2024 年(NeurIPS)证明:LLM 评判者能认出自己的生成并系统性打更高分——人类看来质量相当时也如此。加上 Day 5 讲过的 backward rationalization、confidence inflation,「让 AI 检查 AI」不仅相关,还带正向偏袒。所以 multi-agent debate、self-reflection 这些看起来像「独立验证」的东西,本质仍是同族大脑的内部争论,共享同一套盲区。

实战示例

破解办法只有一条主线:把裁判换成非 LLM,或换成与问题数学性质绑定的 oracle。metamorphic testing 是最典型的一招——不判单个输出对错,判多次执行之间的关系,关系来自问题本身而非模型:

# LLM 生成了一个 sort 实现,你没有「正确答案」当 oracle
# → 用 property + metamorphic 关系当独立裁判(hypothesis)
from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_idempotent(xs):
    assert my_sort(my_sort(xs)) == my_sort(xs)      # MR: 排序幂等

@given(st.lists(st.integers()))
def test_permutation(xs):
    assert sorted(my_sort(xs)) == sorted(xs)         # MR: 输出是输入的排列

@given(st.lists(st.integers()), st.integers())
def test_shift_invariant(xs, k):
    assert my_sort([x+k for x in xs]) == [x+k for x in my_sort(xs)]

这三条 property 里没有一个「标准答案」,但它们联合起来是一张独立于 LLM 的过滤网——模型编不出既满足全部关系又是错的实现。这就是把「无 oracle 任务」硬转成「有 oracle 任务」的手艺。退一步的弱化版:至少让 verifier 用不同族模型(Claude 生成 → 换一族模型 + 显式扫描器交叉),能降低 ρ,但仍不如一个真正的非 LLM oracle。

失败模式:把 multi-agent debate 或「reflection 循环」当成独立验证写进 pipeline,然后在它上面开自动放行。同族 agent 的辩论会在共有盲区上达成一致的错——一致不等于正确,反而给了你更强的「已验证」错觉。
进阶资源 · Panickssery et al. LLM Evaluators Recognize and Favor Their Own Generations (NeurIPS 2024), neurips.cc · Segura et al. Metamorphic Testing: A Review (ACM Computing Surveys), dl.acm.org/10.1145/3143561
// 03

按可验证性重排自动化边界

论断:自动化边界不该按「任务难不难」画,该按「有没有便宜的独立 oracle」画。

背景与原理

大多数人画自动化边界的直觉是「简单的交给 AI,难的自己来」。这是错的坐标轴。正确的坐标轴是可验证性——一个环节有没有便宜、独立的 oracle。把它排成一道阶梯:

可验证性阶梯(Verifiability Ladder) oracle 强度 ▲ 独立性 ▲ → 自动化决策 ┌──────────────────────────────────────────────────────┐ │ ① 强 oracle 编译 / 类型 / 单元测试 / 形式检查 │ │ 独立、近零边际成本 → 全自动 + fail-closed │ ├──────────────────────────────────────────────────────┤ │ ② 派生 oracle metamorphic / property / 回归快照 │ │ 独立于单次对错 → 全自动 + 抽样人核 │ ├──────────────────────────────────────────────────────┤ │ ③ 弱 oracle LLM-judge / 启发式规则 / 交叉模型 │ │ 与生成者相关 → 自动预筛 + 人把关 │ ├──────────────────────────────────────────────────────┤ │ ④ 无 oracle 需求理解 / 品味 / 战略判断 │ │ 只能靠人 → human-in-the-loop │ └──────────────────────────────────────────────────────┘ 放手区(可全自动):① ② 把关区(必须人在环):③ ④

规则:越靠上,越该无脑放手让 AI 高速迭代——因为 generate-and-check 循环便宜又可信,AI 跑错了 oracle 会立刻拦下。越靠下,越该保留人在环。最危险的是 generate-verify gap 大的任务:生成一秒钟、验证要半小时。METR 2025 那项随机对照实验里,资深开发者用 AI 后完成时间反而多了 19%——一个主要来源就是验证成本被系统性低估:AI 生成飞快,但把「看起来对」验证成「真的对」的活全压回了人身上,而这段没有 oracle。

实战示例

重排你的 agentic loop:只在有 oracle 的 sub-task 上开自动 retry,无 oracle 的 sub-task 一律 checkpoint 交人

def step(task):
    out = llm_generate(task)
    oracle = ORACLE_FOR[task.kind]          # 盘点表查出来的裁判
    if oracle.independent:                   # ①② 阶梯上层
        for _ in range(MAX_RETRY):
            if oracle.check(out): return out   # 通过才放行 (fail-closed)
            out = llm_fix(task, oracle.feedback)   # 独立反馈驱动自修复
        raise NeedsHuman(task)                          # 试满了仍不过 → 交人
    else:                                     # ③④ 阶梯下层:不许自动放行
        return checkpoint_to_human(task, out)

这段的灵魂是那个 if oracle.independent:它把「AI 能不能自转」这个决策,从「任务看起来简不简单」硬移到了「有没有独立裁判」。Day 3 讲过升级成 agent 的三条件之一就是「能验证最终输出对错」——这里给了它可操作的判据。

失败模式:在没有 oracle 的地方开自动循环。agent 会「跑飞还不自知」——没有独立裁判,retry 只是在放大同一个错误,还烧 token。凡是要开 MAX_RETRY 自动重试的环节,先确认它上面挂着的是①②,不是③④。
进阶资源 · METR Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity (arXiv:2507.09089), arxiv.org/abs/2507.09089 · Simon Willison on reviewing AI-generated code, simonwillison.net
// 04

个人版护栏:先做 oracle 盘点,再谈规模化

论断:把一个 AI 工作流从「手动跑」升级到「自动/规模化」之前不做 oracle 盘点,等于在放大一个没有裁判的系统。

背景与原理

规模化放大的从来不只是产出,还有没被 oracle 拦住的错误。手动跑十次,你眼睛就是那个(很贵但独立的)oracle;一旦自动化跑一万次,你的眼睛退场了,如果没有别的独立裁判补位,错误就以一万倍的速度流进下游。所以「个人版护栏」的本质是:在你自己的 harness 层,把独立 oracle 固化成自动 gate,在放手之前。一张升级前必答的检查清单:

  1. 这个环节独立 oracle 吗?(编译/类型/测试/property,还是只有 LLM?)
  2. 这个 oracle独立于生成它的模型吗?(同族自查不算)
  3. 这个 oracle 是 fail-closed 吗?(不过就 block,而不是记个 warning 放行)
  4. 没有 oracle 的环节,留了 human checkpoint 吗?

四个都 ✅ 才有资格把这条流水线接上自动触发。任何一个 ❌,就是你规模化时会被反噬的地方。

实战示例

在 Claude Code 里,护栏就是 hooks——把真 oracle 挂成 fail-closed 的门,不过就物理拦截,不靠模型自觉:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{"type":"command",
        "command":"tsc --noEmit && pytest -q || exit 2"}]  # 真 oracle,非 0 即拦
    }],
    "Stop": [{"hooks":[{"type":"command",
        "command":"pytest tests/property -q || exit 2"}]}]  # 派生 oracle
  }
}

exit 2 是关键:Claude Code 的 hook 用非零退出把结果作为错误反馈回喂给模型并阻断本次动作——这就是把独立 oracle 变成 fail-closed gate。注意别把「格式类」hook(prettier / lint --fix)误当验证正确性:它们让代码能看,不保证跑对。护栏要分清「能跑」和「跑对」。

失败模式:把 lint / format / 「代码没报错」当成正确性 oracle。格式 oracle ≠ 正确性 oracle ≠ 语义 oracle——三者独立。一段能编译、格式漂亮、lint 全绿的代码,逻辑可以是彻底错的。盘点时务必标清每个 gate 到底在验证哪一层,别用一个下层 oracle 冒充上层的覆盖。
进阶资源 · Claude Code Hooks 文档, docs.claude.com/.../hooks · Anthropic Building Effective Agents(升级成 agent 的判据), anthropic.com/engineering

// 综合实战 · 给自己的一条流水线做 oracle 盘点

挑一条你已经在用的 AI 工作流(比如「Claude Code 改 bug → 提 PR」或「RAG 问答」),花半小时走一遍:

  1. 拆环节:把流水线拆成 5–8 个原子步骤,每步一行。
  2. 标裁判:给每步填「裁判是谁」——编译 / 类型 / 测试 / property / LLM-judge / 人 / 无。
  3. 判独立:对每个裁判打 ✅/❌——它独立于生成这一步的模型吗?同族自查一律 ❌。
  4. 排阶梯:把每步归到①强 / ②派生 / ③弱 / ④无 oracle 四档。
  5. 重画边界:①②划入「放手区」接自动 gate(fail-closed);③④划入「把关区」留 human checkpoint。若某个你本想自动化的步骤落在③④,问:能不能给它造一个真 oracle?(让输出可编译 / 可执行 / 可查引用 / 带 schema——把无 oracle 转成有 oracle)
  6. 装护栏:把①②的 oracle 写成 Claude Code hook 或 CI gate,跑一遍。

做完你会发现:真正卡住你规模化的,从来不是「模型不够强」,而是那几个没有独立裁判的环节。把它们找出来、要么造 oracle 要么留人——这就是个人 AI 工作流从「能用」到「敢放手」的分水岭。

// ENGLISH GLOSSARY

Test Oracle
判断「观察到的行为是否正确」的信息源/裁判。本期核心概念。
Oracle Problem
为一个程序自动地获得正确性裁判的难题;无 oracle 时只能靠人,是测试自动化的瓶颈。
Implicit Oracle
来自「普遍不该发生」的隐式裁判:crash / assert / 编译不过。最便宜的独立 oracle。
Derived Oracle
从其他来源派生的裁判:回归基线、metamorphic 关系、property 不变量。
Metamorphic Testing
不判单个输出对错,而验证多次执行输入/输出之间的关系(MR),因而无需单点 oracle。
Property-Based Testing
用输入的普遍性质(不变量)当裁判,随机生成大量输入验证之(QuickCheck / Hypothesis)。
Self-Preference Bias
LLM 评判者认出并系统性偏袒自己的生成(Panickssery 2024)——AI 看 AI 不独立的根因之一。
Generate-Verify Gap
生成成本远低于验证成本的落差;gap 越大,无 oracle 的自动化越危险。
Fail-Closed
验证不通过就阻断(而非记警告放行)的门策略。护栏的默认姿态。
Verifiability Ladder
按 oracle 强度与独立性把环节排成的阶梯,用来决定自动化边界画在哪。

// 深入思考

LLM-as-judge 到处都在用,如果它不是独立 oracle,为什么很多 eval 里它「有用」?
因为它对粗粒度偏好有真信号,对生成者自身盲区无信号——两件事别混。judge 与被判者不同族、判「A 明显比 B 好」这种大差距时,它和人类判断相关度不低,够用;judge 与生成者同族、判细粒度「这段逻辑到底对不对」时,self-preference + 共享盲区让它失效。所以 LLM-judge 适合做排序/预筛,不适合做放手前的最后一道正确性门:用它筛掉一眼烂的,再用真 oracle 或人守细节。
编译器是强 oracle,但只覆盖「类型对」不覆盖「逻辑对」。强 oracle 覆盖窄、弱 oracle 覆盖宽,怎么取舍?
oracle 有两个正交维度:强度(说对就真对)和覆盖(能管多少种错)。别指望单个 oracle 兼得。正解是叠多个窄而强的 oracle去逼近宽覆盖:类型管接口、单测管样例、property/metamorphic 管不变量、fuzz 管崩溃。每加一层强 oracle,覆盖并上一块,且都独立。反模式是用一个「宽而弱」的 oracle(LLM-judge)假装覆盖全部——覆盖是宽了,但每一格都不可信。宁可要一堆可信的窄,不要一个可疑的宽。
metamorphic testing 号称「不需要 oracle」,这和「要独立 oracle」矛盾吗?
不矛盾——MR 本身就是一种 derived oracle。它不需要「单个输入的标准答案」这种 oracle,但它需要一个来自问题数学性质的关系(sort(shift(x))==shift(sort(x)))。这个关系独立于模型:它来自排序的定义,不来自任何一次生成。所以「不需要 oracle」是指不需要点对点的参考答案,不是说不需要裁判——裁判换成了「关系必须成立」。这恰恰是最实用的一招:当你造不出标准答案时,去找问题里天然成立的关系
如果我这个环节手上只有 LLM、别的 oracle 都没有,怎么办?
把任务改造成「能被非 LLM 验证」的形态——这就是 grounding 的工程本质。让 LLM 别直接输出结论,而是输出可执行代码(拿编译器/测试当裁判)、可查证的引用(拿检索/URL 存在性当裁判)、结构化 schema(拿 validator 当裁判)、可复算的中间量(拿计算器/SQL 当裁判)。核心动作:把「判断一段自然语言对不对」(无 oracle)转成「判断一个可机器检验的产物对不对」(有 oracle)。转不动、真的只能靠人品味的,就老实留 human checkpoint,别自欺。
规模化时 oracle 的成本结构会怎么变?这对「先盘点再放大」意味着什么?
强 oracle(编译/测试)边际成本近零——跑一次和跑一万次差不多,规模化几乎免费。弱 oracle(人核、LLM-judge)成本随量线性甚至超线性涨:人会累会走神,LLM-judge 要 token。于是规模化会把成本压到「还留着人/弱 oracle 的环节」上,它们变成瓶颈——这正是 METR 里资深开发者变慢的结构性原因。含义很直接:放大之前尽量把环节往阶梯上层迁(给它造强 oracle),迁不动的要预算好人力。没 oracle 的环节,自动化只是把验证债转移,不是消除。

// 延伸阅读