跳到正文

目录

LLM-as-a-Verifier:当打分从离散分类升级成概率分布,agent 验证发生了什么

LLM-as-a-Verifier:当打分从离散分类升级成概率分布,agent 验证发生了什么

llm-as-a-verifier/llm-as-a-verifier 是 2026-04 在 GitHub 上线、2026-07-07 上 arXiv (2607.05391) 的开源框架,2026-08-14 发布 0.2.0 版本,目前 2.9k stars / 230 forks / MIT / Python。它的核心是用同一套「打分 token 的概率分布」在四个完全不同的 agent benchmark 上拿到 SOTA——同一行代码既能给 GPT-5.5 选轨迹,又能给机器人 SAC 和数学 GRPO 当奖励信号。

一句话定位:

Discrete judgment → continuous expectation. 把 LLM 当评审员的范式从「读完给一个分」升级成「读打分 token 的 logprob 并算期望」,再用概率化 pivot tournament 把 O(N²) 的两两比较砍到 O(Nk)。这套打分不需要训练、不需要 reward model,并且在四个跨域 agent benchmark 上 SOTA。

读完代码 + 论文摘要 + 全部 criteria + 三套 scripts + 论文里的四个 benchmark 数字之后,得出四条判断:

  1. 它不是「又一种 LLM-as-a-Judge」——它把 judge 的输出从离散 label 升级成连续分布,粒度本身就是 scaling 轴
  2. 它的算法选算法不是「全打一遍」——概率化 pivot tournament 把 pairwise 次数从 N² 砍到 Nk,靠的是 Bradley-Terry 软胜出 + Hamilton 环消除位置偏置
  3. 它的成本不靠模型变小——靠 prefix-cache 把 uncached input 砍到 3.4× 之一,靠 A-T 字母表让打分 token 离散化后能被 logprob 稳定读取。
  4. 它的副业比主业更值钱——同一个细粒度打分既能做 best-of-N 选最优,也能做 progress tracking 做早停,最后还能喂给 RL 当 dense reward

下面分十节展开这四条。


一、问题域——agent 选最优轨迹这道题,为什么一直没好解

Pass@1 和 Pass@k 之间隔着一道很具体的工程题:怎么从 k 次采样里挑出最好的那一条

这道题的难度在三个层面同时上升:

  • 任务域变宽了:2024 年的「写代码」已经扩散到「改 PR」(SWE-Bench Verified)、「跑终端命令」(Terminal-Bench)、「调 FHIR 服务器」(MedAgentBench)、「给机器人做奖励 shaping」(RoboRewardBench)。同一个 reward model 想跨这四个域,传统做法要么每个域训一个 RM,要么放弃。
  • 轨迹变长了:mini-swe-agent 跑 SWE-Bench 一条轨迹动辄 80k tokens,agent 越强轨迹越长,reward model 看得越贵。
  • reward hacking 越来越严重:agent 已经学会「声称自己成功了」——「All tests pass!」「Patch looks correct!」这种 agent narration 几乎一律不可信。Verifier 必须只看实际输出,不看 agent 自评。

过去三年的应对大致是两条路:

  • 学一个 reward model:训练一个 RM 在每条轨迹上判分。代价:要训、跨域差、容易被 hack。
  • LLM-as-a-Judge:让 LLM 直接读两条轨迹后输出 <score_A>15</score_A> 这种离散分数。代价:粒度太粗(1-5、1-10),位置偏置(模型偏好 B 槽),噪声大。

LLM-as-a-Verifier 走第三条路:不改 LLM,只改打分的读法——让 LLM 输出 <score_A>A</score_A> 这种 20 档字母,然后读打分那个位置的 logprob 分布,算期望分


二、从 Judge 到 Verifier,一张表说清差异

维度LLM-as-a-JudgeLLM-as-a-Verifier
打分单位1 个离散数字20 个 token 的 logprob 分布
输出形式<score_A>15</score_A><score_A>A</score_A>
取值范围整数 1-10期望分 ∈ [0, 1](连续)
位置偏置普遍存在,无法消除Hamilton 环对消(A/B 槽各出现一次)
Scaling 轴仅「多次采样」粒度 × 重复次数 × 准则数 三轴
校准度粗粒度,边界模糊细粒度,positive / negative 分离度更好
训练需求无(但要 prompt 工程)无(且 token 选择是 first-class design)

A = 20 分(最优),T = 1 分(最差)。仓库 fine_grained_reward.pySCALE["valid_tokens"] 把 A/B/C…T 都映射成 20/19/18…/1,小写 a/b/c…/t 等价。然后 extract_score 读打分位置的 top-20 logprob,归一化到 [0, 1]。

这个设计的隐性后果是:g=20 是性能曲线的拐点。论文里反复跑 g ∈ {5, 10, 20} 的消融,g=5 时 positive / negative 几乎重叠;g=20 时分离度大到足以让 pairwise 比较有 80%+ 一致性。再往上 g=50 / g=100 也试过,但边际收益骤降——token 序列需要更多 logprob 槽,推理反而变贵。


三、核心机制 1——细粒度打分:读分布而不是读标签

打分数学化在论文公式 1 里:

$$ R(x, \tau) = \frac{1}{C \cdot K} \sum_{c=1}^{C} \sum_{k=1}^{K} \sum_{g=1}^{G} p_{\theta}(v_g \mid x, c, \tau),\phi(v_g) $$

  • $C$ = 准则数(terminal_bench 用了 3 条)
  • $K$ = 同一对轨迹上重复评分次数(默认 4,0.2.0 起 terminal_bench_2.1 调到 2)
  • $G$ = 粒度(=20)
  • $v_g$ = 字母表里的第 g 个 token(A/B/…/T)
  • $\phi(v_g)$ = 把字母映射成标量值(A→20, B→19, …, T→1)
  • $p_\theta(v_g \mid \dots)$ = 模型在打分位置对字母 $v_g$ 分配的概率

直觉上是这样:模型在 <score_A> 后面给 A 的概率是 0.6、给 B 是 0.3、给 C 是 0.1——这一次打分的期望分 = (0.6×20 + 0.3×19 + 0.1×18) / 20 = 0.975(归一化后)。这里有个关键细节:离散打分只能看"最可能的那个字母",会丢掉 0.6/0.3/0.1 这层置信度信息;logprob 期望把这层信息接住了。同样是判 A 的两条轨迹,一条可能 0.9 坚挺、另一条 0.55 勉强,离散版看起来一样"都是 A",logprob 版却能把它们分开——这就是 Judge 只看见标签、Verifier 看见分布的分界线。

仓库实现细节(fine_grained_reward.py):

GRANULARITY = 20
SCALE = {
    "valid_tokens": {
        **{chr(65 + i): float(GRANULARITY - i) for i in range(GRANULARITY)},
        **{chr(97 + i): float(GRANULARITY - i) for i in range(GRANULARITY)},
    },
}

def extract_score(text, tokens, position_logprobs, tag):
    """Expected score over the verifier's token distribution at `tag`,
    normalized to [0, 1]. Falls back to parsing the literal text token."""
    # 1) 找 tag 位置:最后一个匹配(verdict 在文末)
    # 2) 读该位置 +1 的 top-20 logprob
    # 3) 对 valid_tokens 取 exp(logprob) 求期望
    # 4) 归一化到 [min(SCALE), max(SCALE)] → [0, 1]

为什么用字母而不是数字——这是论文里没说但源码里写了的设计:logprob API 一次最多返回 20 个候选 token(OpenAI 把 top_logprobs 上限设为 20)。如果用 1-10 的数字,模型大概率在「15」和「16」之间反复横跳,但 top-20 槽位会被「15」「16」「17」三个相近数字填满,信号被自己稀释。换成 A-T 字母,字母表天然离散化,top-20 槽位能装下整个 20 档粒度,分布更干净。

为什么是 g=20 而不是 g=100——logprob 上限卡死,加上 26 个英文字母里 21 个之后就是 U/V/W…,这些字母在自然语言里几乎不出现在词首,prompt 训练时几乎不会被分配概率,强行扩到 100 个 token 反而会让分布被噪声支配。


四、核心机制 2——概率化 pivot tournament:O(N²) → O(Nk)

N=5 的 best-of-5 全 pairwise 两两比需要 N(N−1)=20 次 directed 比较;N=8 就 56 次;N=20 就 380 次。工程上不可持续。

仓库 pivot_tournament.py 把这砍成两步:

第一步:Hamilton 环打分(消除位置偏置)

def ring_cycle(n, rng):
    """The N directed adjacent pairs of a random Hamiltonian cycle."""
    perm = list(range(n))
    rng.shuffle(perm)
    return [(perm[t], perm[(t + 1) % n]) for t in range(n)]

N 个轨迹排成一个随机环,每个轨迹在 A 槽出现一次、在 B 槽出现一次。位置偏置(模型总是偏好 B 槽或偏好 A 槽)在环上求和后被对消。这一步用掉 N 次 verifier 调用。

第二步:选 pivot + 跟所有非 pivot 比

def select_pivots(w, c, k):
    """Top-k candidates by mean preference w_i / c_i (ties broken by index)."""
    order = sorted(range(n),
                   key=lambda i: (-(w[i] / c[i] if c[i] else 0.0), i))
    return order[:k]

def pivot_round_pairs(n, pivots):
    """Every non-pivot vs pivot, plus pivot vs pivot."""
    non_pivots = [i for i in range(n) if i not in pivot_set]
    pairs = [(i, p) for i in non_pivots for p in pivots]
    pairs += list(combinations(sorted(pivots), 2))
    return pairs

环打分后取 top-k 作 pivot set,所有「非 pivot × pivot」对 + pivot 内部 pair 全部跑一遍。总次数 = N + k(N-k) + C(k,2)——N=5、k=2 时只有 5 + 2×3 + 1 = 12 次(对比全 pairwise 的 20 次省 40%);N=20、k=2 时只有 20 + 2×18 + 1 = 57 次(对比 380 次砍 85%)。**

为什么 pivot 数 2 就够——论文给的直觉是:top-2 pivot 把「真正可能赢」的候选覆盖了,剩下的候选只需要跟这两个对比就行。k 越大越准但越贵,k=2 是工程上的甜点。仓库 benchmarks.pypivots: int = 2 是全部 4 个 benchmark 的默认值。

胜负怎么算——仓库用 Bradley-Terry:

def bradley_terry(ra, rb):
    """p(a beats b) under the Bradley-Terry model on rewards in [0, 1]."""
    return 1.0 / (1.0 + math.exp(-(ra - rb)))

每次 directed 比较返回一个软胜率 p ∈ [0,1],聚合到 $w_i = \sum p(a \succ b)$ 和 $c_i = $ 比较次数。最终赢家是 $\arg\max_i w_i/c_i$。Bradley-Terry 比简单 win/lose 好的地方是它对「微弱优势」和「压倒性优势」区分得开——A 比 B 好 0.001 和好 0.5 在 BT 里给出截然不同的软胜率。


五、核心机制 3——prefix-cache 把成本砍 3.4×

每个 pairwise prompt 都长这样:

Task: {prompt}
Trajectory A: {trajectory_A}
Trajectory B: {trajectory_B}
Rating Scale: ...
Criterion: {criterion}

轨迹 A + 轨迹 B + task + scale 几乎是所有 prompt 的共享前缀——只有 criterion 在尾端变。一个 benchmark 有 C 个 criterion + K 次重复 + 每任务最多 N(N-1) 个 pair,prompt 数能到数千。

仓库的优化是双管齐下:

  1. 把 criterion 放尾部:让所有 prompt 共享最大前缀(仓库 build_prompt 里 criterion 是最后一段)。
  2. warm-up 一发再 fan-out:仓库 score_directed_pairs 先挑每个 unique prefix 跑一发完整请求(让后端把 prefix 写进 cache),剩下的同 prefix 请求并发跑。
# Warm-up wave: one job per distinct prompt prefix, then the rest.
seen = set()
warm, rest = [], []
for job in jobs:
    prefix = job[7]
    if prefix in seen:
        rest.append(job)
    else:
        seen.add(prefix)
        warm.append(job)

log(f"  {len(jobs)} scoring jobs ({len(cached)} cached); "
    f"warming {len(warm)} prefixes")

效果在论文里给得很硬:terminal_bench_2.1 上 prefix-cache 命中率从 5.2% 涨到 78.4%,uncached input 砍 3.4×

仓库把这个数字落到了 USAGE 计数器里,实测而不是估计

def format_usage(usage, title="Verifier tokens"):
    return [
        f"{title} ({s['calls']:,} verifier calls)",
        f"  input                          {s['input_tokens']:>16,d}",
        f"  cached input                   {s['cached_input_tokens']:>16,d}  "
        f"({100 * s['cache_hit_rate']:.1f}% hit rate)",
        f"  uncached input                 {s['uncached_input_tokens']:>16,d}",
        f"  output                         {s['output_tokens']:>16,d}",
    ]

跑一次 scripts/run.py terminal_bench 会在 result 表下打出:

Verifier tokens (4,320 verifier calls)
  input                          272,551,552
    cached input                 214,712,320  (78.8% hit rate)
    uncached input                57,839,232
  output                          32,441,600
    reasoning                     26,102,144

78.8% 的输入 token 是从 cache 里读出来的,真正计费的是 uncached 那一半。0.2.0 CHANGELOG 里单独把这条提出来——这是 v0.2.0 的主推升级。


六、任务流案例——一条 mini-swe-agent 的轨迹如何被打分

把上面三节串成一次真实调用。场景:SWE-Bench Verified 任务 django__django-11039,mini-swe-agent 跑了 3 条轨迹(traj_1 / traj_2 / traj_3),仓库 select 调用如下:

import llm_verifier

problem = "Fix the failing test in utils.py."
candidates = [traj_1, traj_2, traj_3]

result = llm_verifier.select(
    problem=problem,
    candidates=candidates,
    criteria="swe_bench",          # → criteria/swe_bench.md
    n_evaluations=4,                 # K
    pivots=2,                        # k
    seed=0,
    max_workers=50,
    model="gemini-2.5-flash",
)

# 假设选中了 traj_2(最优)
print(result.index)         # 1
print(result.scores)        # [0.62, 0.97, 0.41] —— traj_2 胜
print(result.n_comparisons) # 6 directed pairs = Hamilton 环 3 + pivot rounds 3
print(result.ranking)       # [1, 0, 2]

任务流转顺序

  1. select 收口 → pivot_tournament.ring_cycle:seed=0 决定环顺序是 (0,1)、(1,2)、(2,0),3 个 directed pair 进 Phase A。
  2. score_directed_pairs warm-up:3 个 unique prefix 各跑一发让后端写 cache;剩下的同 prefix 请求并发。
  3. 读打分位置 logprobextract_score 对每对 pair × 3 个准则 × 4 次重复 = 12 次 verifier 调用得到 (R_a, R_b),归一化到 [0,1]。
  4. accumulate 聚合成 (w, c):每个 pair 走 Bradley-Terry,w[i] 是 i 的总胜率、c[i] 是 i 的总出场次数。
  5. select_pivots 选 top-2:假设 w/c 排序后是 [1, 0, 2],pivot = {1, 0}。
  6. Phase B 跑 pivot roundspivot_round_pairs(3, {1, 0}) 返回 [(2,1), (2,0), (1,0)],3 个新 pair 再走 12 次 verifier 调用(cache 命中率从 Phase A 暖出)。
  7. 聚合 + 选赢家accumulate 合并两轮,(w_i / c_i) 最高的轨迹 1 胜出。

总成本:72 次 verifier API(Phase A 环 3 对 × 12 + Phase B pivot 3 对 × 12),与全 pairwise directed(3 条轨迹 × 2 个方向 = 6 directed pairs × 12 calls/pair = 72)的 72 calls 相比完全持平——N=3 时 PPT 调用量不减,它换的是「位置偏置对消 + Bradley-Terry 软胜出」;省钱要到 N≥5 才显现

PPT vs 全 pairwise directed 的成本对照(每对 directed pair × 3 criteria × n_eval=4 = 12 calls/pair;全 pairwise directed = N(N−1) 对):

Ndirected pairs 全 pairwisePPT pairs(k=2)全 pairwise callsPPT callsPPT 节省
36672720%(持平)
5201224014440%
8562167225262.5%
2038057456068485%
50245014729400176494%

PPT pairs 公式:N 条环向边 + (N−2)×2 条非 pivot×pivot + C(2,2) 条 pivot 内部 = 3N−3。N=5 就是省钱起点——它比全 pairwise 的 20 对直接省 40%;N≥8 后降到三分之一以下。论文给的 4 个 benchmark 多在 N=3-5(尤其是 best-of-3 的 SWE-Bench),这一区间 PPT 的主要收益确实是验证质量而非调用成本,但一旦 N 上去就两端通吃。


七、benchmark 解读——四个 SOTA 数字背后分别证明什么

论文和 README 列了 4 个 benchmark 的对照数字。每个数字回答的问题都不一样,不能简单地说「都 SOTA」:

BenchmarkBase Model + HarnessPass@1LLM-as-a-VerifierOracle测的是什么
Terminal-Bench 2.0GPT-5.5 (Best-of-5)83.1%86.5%92.1%终端命令类任务的 Pass@1
SWE-Bench VerifiedOpus 4.5/4.6/Gemini 3 Flash (Best-of-3)76.1%78.2%84.4%真实 GitHub issue 修复
MedAgentBenchClaude Opus 4.8 (Best-of-5)70.2%73.3%75.0%FHIR 医疗数据库查询
RoboRewardBench(论文披露)87.4%机器人轨迹 reward shaping
Terminal-Bench 2.1 (self-verification)deepseek-v4-flash (Best-of-3)79.4%86.5% ± 1.1%92.1%同模型既是 agent 又是 verifier
Terminal-Bench 2.1 (self-verification)deepseek-v4-flash (Best-of-5)78.7%88.0% ± 0.6%96.6%同模型既是 agent 又是 verifier

Terminal-Bench 2.0 那一行回答「同 agent 同轨迹池,verifier 选得比随机好多少」——86.5% vs 83.1% Pass@1 = +3.4pp 绝对提升(相对约 4%),这是 verifier 这种轻量级方案的天花板了。Oracle 92.1% 意味着如果 verifier 是完美 oracle,N=5 还能再提 5.6pp。

SWE-Bench Verified 那一行回答「真实软件工程任务能不能也用」——78.2% vs 76.1% = +2.1pp,相对收益约 2.8%。这个数字看起来比 Terminal-Bench 低,但绝对难度高一个数量级——SWE-Bench Verified 的隐藏测试是真的仓库跑测试,Terminal-Bench 是看 stdout。Oracle 84.4% 说明即使 oracle 也只能到 84%,verifier 已经吃到 78.2% 距离上限只剩 6pp

MedAgentBench 那一行回答「医疗这种高 stakes 域能不能用」——73.3% vs 70.2% = +3.1pp,Oracle 75.0% 距离上限只剩 1.7pp。这意味着 verifier 几乎吃到了 oracle 的能力——医疗 query 这种 FHIR JSON 模板化任务上 verifiers 比 verifier-judged 这种主观打分准得多。

RoboRewardBench 87.4% 在论文里是机器人 RL reward shaping 的专用 benchmark,证明同一个打分信号不仅能做 best-of-N 选优,还能直接喂 SAC/GRPO 做 dense reward——这是论文第 5 节的核心贡献。

self-verification 那两行是 0.2.0 新加的(terminal_bench_2.1),最有反直觉价值:让 deepseek-v4-flash 既是 agent 又是 verifier——同模型审自己,结果 86.5% / 88.0% 仍然显著高于 Pass@1 的 79.4% / 78.7%。这意味着 verifier 的能力不依赖于「比 agent 更强」——同模型就能当好 verifier。

能推出什么

  • 跨 4 个域(终端、代码、医疗、机器人)都成立 = 不依赖域内 RMs;
  • Pass@1 → LLM-as-a-Verifier 的 gap ≈ Pass@1 → Oracle 的 50-70% = 已经吃掉了大半 oracle 红利;
  • self-verification 跑通 = verifier 不需要比 agent 强 = 部署成本砍掉一半(不需要额外维护一个更强的模型)。

不能推出什么

  • 4 个 benchmark 不等于「所有 agent benchmark」——OCR/视觉问答/长文档检索这些任务没测过;
  • +2-3pp 的绝对收益在大规模生产里不一定值回 verifier 调用成本(见 §10);
  • 87.4% 是 verified reward 的精度,不等于策略最终回报——RL 用 verifier 当 reward 的实际增益论文里给了数字,但泛化性仍是开放问题。

八、副业——细粒度打分的两个高价值外延

打分一旦有了连续期望,它能做的远不止 best-of-N 选优。仓库另外暴露了两个一等公民 API:

8.1 Progress tracking(在线早停)

ProgressTracker 让 verifier 边看 agent 跑边打分:

tracker = llm_verifier.ProgressTracker(problem, n_evaluations=4)

score = tracker.update('Read the problem statement')            # 0.00002
score = tracker.update('Wrote def rev(s): return s')            # 0.00013
score = tracker.update('Changed to def rev(s): return s[::-1]') # 0.73938
score = tracker.update('Tested: rev("abc") returned "cba"')     # 0.98604

if score < 0.05:      # after any step: abandon a hopeless rollout early
    break

关键工程细节:tracker 每次 update 只喂 verifier 「前缀」,绝不喂未来的步骤。如果 verifier 看到未来,它会按未来结果给当前步打分(早步骤看着就高),曲线失去信息。仓库 ProgressTracker.update 实现里严格只传 self._step_texts[:k]

应用场景:

  • 早停没救的 rollout:agent 跑了 10 步还在 A 区间([0, 0.05]),就别等它跑完了;
  • 决定何时 resample:第 8 步还停在 J([0.5, 0.6]),就重新生成一条;
  • 拟合 RL 早期 reward:agent 自己也能看到自己的进度曲线。

8.2 多模态扩展

所有入口(select / compare / track)都接受 images

result = llm_verifier.select(problem, candidates, criteria=criteria,
                             images=["before.png", "after.png"])

tracker = llm_verifier.ProgressTracker(problem)
score = tracker.update(step, images="camera_frame.png")  # per-step frame

Per-step frame 会被纳入 trajectory history,所以 verifier 永远看到完整的视觉历史——这正好对应机器人/具身场景里「相机每帧拍一张」的需求。仓库 _score_tags_by_prefill 这一段用 add_generation_prompt=False, continue_final_message=True 强制模型在 prefilled tag 后只输出打分字母,让打分位置稳定

注意:多模态需要 verifier 模型本身就是多模态——仓库 README 推荐 Gemini 2.5 Flash 或本地 vLLM Qwen/Qwen3.5-9B(带 vision)。


九、社区与生态——Stanford 系背书的工程化深度

论文 9 位作者里,4 位是 Stanford 终身教授

  • Chelsea Finn(CRFM 主任,meta-learning / robotics)
  • Marco Pavone(自动驾驶 + 机器人,NVIDIA 兼职)
  • Ion Stoica(Databricks 创始人 + Berkeley RISELab)
  • Azalia Mirhoseini(Google Brain / Apple 出身,AI for systems)

这个阵容跟论文 2026-04 在 GitHub 上线、2026-07-07 上 arXiv、2026-08-14 发 0.2.0 的速度对得上——背后有完整 RL 团队 + robotics 团队 + systems 团队在并行跑实验。仓库 CHANGELOG 里 0.2.0 同时上线的还有「deepseek-v4-flash verifier backend」和「self-verification benchmark」,深度依赖 deepseek 的 reasoning API + logprob——这不是一个学生作业能 hold 住的工程节奏。

生态布局

  • TurboAgentllm-as-a-verifier/TurboAgent)——同一个组织下的姊妹项目,把 LLM-as-a-Verifier 接到 Claude Code 上当 LLM API 代理,用户无感地拿到 best-of-Npip install git+...turbo-agent 起在 :8888 → ANTHROPIC_BASE_URL=http://localhost:8888 claude
  • python -m llm_verifier <file.md> —— 看 criteria 文件会被 verifier 看到成什么样,无 API key。
  • Slack 社区 + Twitter/X (@jackyk02) —— 项目方主动运营。
  • 加新 benchmark 协议(add_new_benchmark.md——写一份 criteria.md + 一个 adapt_run.py 就能把 verifier 接到新任务上,Claude Code / Codex 都能直接照着跑

十、采用顺序与边界——谁该先上,谁可以等等

10.1 该先上

做 best-of-N 选优但还没上 RM 的团队

  • 已经在跑 Pass@1 → 想升级到 Pass@k 但懒得训 RM;
  • 跨多个 agent benchmark(终端 / 代码 / 医疗 / 机器人),不想每个域训一个 RM;
  • 接受「+2-3pp 绝对收益」+「verifier 调用成本」这笔账算得过来。

已经上 LLM-as-a-Judge 但对准确度不满意的团队

  • 当前 LLM-as-a-Judge 在 1-5 离散分上摇摆大;
  • 需要在线 progress tracking 做早停;
  • 想让同一个打分信号同时进 RL reward shaping。

做 RL(GRPO / SAC)需要 dense reward 的团队

  • 已经有 environment + agent,但 reward sparse;
  • RoboRewardBench 87.4% 这个数字值得严肃对待——它是真在 RL 训练里跑出来的,不是离线打分。

10.2 可以等等

任务域没有「实际输出可读」的

  • 比如纯开放式创意写作——verifier 没法只看实际输出就判断好坏,必须靠 LLM subjective judgment,g=20 letter 的精细度在这里没优势;
  • 比如长文档检索——verifier 看到「retrieved 4 docs」这种输出判断不了 relevance,必须看下游 LLM 用了什么。

生产环境对 verifier 调用延迟敏感

  • 一次 select(problem, 5 trajs, 3 criteria, n_eval=4) 要 144 次 verifier 调用(12 directed pairs × 12 calls/pair),并发 50 也得等几秒——见 §6 的成本表;
  • progress tracking 是 1 次/step,N 步 agent 跑完得多花 N 次 verifier;
  • 如果产品场景是「用户输入 → 30 秒内返回」,verifier 这块成本可能接不住。

已经有成熟 RM 的团队

  • RM 已经训到 Pareto frontier;
  • RM 的训练数据足够覆盖你的 agent 输出分布;
  • LLM-as-a-Verifier 对你的定位属于「能力替代」而非「能力提升」——RM 已经训到 Pareto frontier 的情况下,迁移到 verifier 拿不到什么新东西,除非 RM 的训练成本实在高得离谱。

10.3 上手顺序

  1. 先跑一次官方 benchmarkpip install llm-verifierpython scripts/run.py terminal_bench 确认它在你的数据上能跑通;
  2. 再写自己的 criteria:照 criteria/TEMPLATE.md 写 2-3 条窄准则(不要写「整体正确性」这种宽口径);
  3. select 做 best-of-N:用 cache="cache/your_task.json" 让重复 run 不花钱;
  4. ProgressTracker 做早停:在已有 agent loop 里加 5 行代码就能拿到 step-level progress;
  5. 最后接 TurboAgent:如果你用 Claude Code 或类似 CLI agent,TurboAgent 是 0 成本升级路径——只是换一个 ANTHROPIC_BASE_URL

10.4 必看边界

  • verifier 必须能返回 token-level logprob:仓库 README 写明「Vertex AI only — logprob extraction needs the Vertex API」, plain Gemini API 不返回 logprob——这点 vLLM 自托管能解,本地 OSS 替代方案是 OPENAI_BASE_URL=http://localhost:8000/v1 + vLLM serve
  • 必须用 letter scale 而不是 1-20 数字:论文消融里 1-20 数字版的 g=5 性能接近 baseline,字母版 g=20 性能显著高——这是 token 分布的离散化设计,不是 prompt engineering
  • prefix-cache 是默认假设:如果你的后端不缓存 prefix(比如裸 HTTP API),warm-up 优化无效,回到全 uncached 成本

十一、回到系统层——这个工作真正贡献的是什么

它真正的贡献不在 +3pp Pass@1 这种数字上——这种收益早就被各种 RM 论文刷过。它的不可替代处在于把验证这件事从「给一个离散标签」升级成「读打分 token 的概率分布」——同一个 LLM,只换打分读取方式。

这套机制一次性解锁了五件事:

  • 三轴 scaling(粒度 × 重复 × 准则)——以前 LLM-as-Judge 时代只有「多次采样」一轴;
  • prefix-cache 友好——读分布让 prompt 高度结构化、缓存粒度细到准则级,5.2% → 78.4% 命中率就是这条线上的红利;
  • 跨域通用——同一套打分机制在终端/代码/医疗/机器人四个完全不同的域都跑通,不需要每个域训 RM;
  • dense reward——同一个分数既能做 best-of-N 选优,又能做 RL reward shaping(SAC/GRPO),reward 形状不需要为不同任务重训;
  • self-verification——同模型当 verifier 的可能性被打开(terminal_bench_2.1 上 deepseek-v4-flash 既是 agent 又是 verifier 还拿到 86.5%),部署时不需要额外维护一个更强的模型。

更底层的视角:token-level logprob 这个 OpenAI 2023 年就暴露在 API 里的接口,在 2026 年终于有人找到了它的「distribution-aware verification」这个对应用例——之前大家要么忽略(直接要离散 label)、要么滥用(拿它做 jailbreak 检测)。LLM-as-a-Verifier 是第一个把这个原语用到正事上的工程化框架

如果你正在做 agent / RL / LLM 系统,这个仓库值得花一个下午把它吃透——核心 943 行的 fine_grained_reward.py + 92 行的 pivot_tournament.py 已经全部,剩下的脚本和 criteria 都是声明式的。这个框架的工程价值在于**「LLM 输出 = 概率分布」这个事实一旦被严肃对待,整条 verification pipeline 就从启发式工程变成了有数学支撑的工程**。


参考资料

  • 论文:Kwok et al. “LLM-as-a-Verifier: A General-Purpose Verification Framework” arXiv 2607.05391v2 (2026-07-07) https://arxiv.org/abs/2607.05391
  • 代码:https://github.com/llm-as-a-verifier/llm-as-a-verifier (MIT, 2.9k stars, 0.2.0 / 2026-08-14)
  • 文档:https://llm-as-a-verifier.com/docs/
  • 姊妹项目 TurboAgent(Claude Code 插件):https://github.com/llm-as-a-verifier/TurboAgent
  • 作者主页:Jacky Kwok, Shulu Li, Pranav Atreya, Yuejiang Liu, Yixing Jiang, Chelsea Finn, Marco Pavone, Ion Stoica, Azalia Mirhoseini(Stanford / Berkeley 系)
  • 相关工作:Pan et al. “LLM-as-a-Judge: A Survey” 2024;Self-Refine;STaR;Process Reward Models;Bradley-Terry 模型
  • 可对比的工程实现:Anthropic Claude API(无 logprob)、vLLM(开源、可 logprob)、SGLang(开源、可 logprob)、DeepSeek API(thinking + logprob)

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。