paired-eval

paired-eval

给模型、agent、harness 做评测:能用程序验证的先验,验不过的再交给 rubric;每一次比较都做成统计上诚实的配对。

Evaluate models, agents and harnesses: verify programmatically whatever can be verified, hand the rest to a rubric — and make every comparison a statistically honest paired one.

pip install paired-eval

v0.4.0 · MIT · Python ≥ 3.9 · 零第三方依赖 · 模型调用由你注入no third-party dependencies · you inject the model call

它评什么What it evaluates

三个对象问的是三个不同的问题,各自固定不同的变量;混着评什么都得不到。The three objects ask three different questions and hold different things constant; mixing them up yields nothing.

评什么Evaluate固定什么Hold constant换什么Vary
model同一批题、同一脚手架same tasks, same scaffold模型the model
harness同一模型same model提示 / 脚手架 / 工具接线prompt / scaffold / tool wiring
agent同一模型、同一脚手架same model, same scaffold策略(单遍 vs 自检修正…)the strategy (one pass vs self-check…)
交互interaction2×2 因子设计:同一把尺子上比主效应,自动报出天花板2×2 factorial: main effects on one scale, ceilings flagged automatically

怎么验:能用程序验的先验,验不过的再判How it verifies: programmatic checks first, judges only for the rest

有唯一真值 → exact 或程序判定;只有事实来源 → retrieval / trajectory(逐条 claim grounding);只有质量标准 → rubric(自身要过 canary)。层可叠加:程序做 gate,rubric 做 score——验不过直接 0 分且不调用评委。A unique ground truth → exact or a programmatic check; only sources of fact → retrieval / trajectory (per-claim grounding); only a quality bar → rubric (which must pass a canary). Layers stack: the program gates, the rubric scores — failing the gate scores 0 and never calls the judge.

task = {"id": "sum-explained", "instruction": "12×12 等于多少, 并说明过程; 以「答案: 」开头给出结果",
        "verification": {"class": "gated",
                         "gate":  {"class": "exact", "gold": "144", "kind": "numeric"},
                         "score": {"class": "rubric", "criteria": [{"text": "说明了计算过程", "weight": 1}]}}}
r = pe.evaluate(task, response=answer, llm=judge)
print(r["score"], r["verdict"])        # 答错 -> 0.0 'gated_out'(judge 一次都没调用); 答对 -> rubric 分
task = {"id": "sum-explained", "instruction": "What is 12×12? Explain, and give the result starting with 'Answer: '",
        "verification": {"class": "gated",
                         "gate":  {"class": "exact", "gold": "144", "kind": "numeric", "marker": "Answer"},
                         "score": {"class": "rubric", "criteria": [{"text": "explains the calculation", "weight": 1}]}}}
r = pe.evaluate(task, response=answer, llm=judge)
print(r["score"], r["verdict"])        # wrong -> 0.0 'gated_out' (judge never called); right -> the rubric score

十秒钟看效果(统计层的离线演示)Ten seconds (an offline demo of the statistics layer)

这里的 strict / bare 是两个桩函数,不是模型——它们复刻一种真实观察到的失败机理(裸指令下正确答案被包进 markdown 围栏),只为在没有 API 时展示报告的形状。输出是真的(由测试与当前代码逐行比对)。strict / bare here are two stub functions, not models — they replay one real failure mechanism (a bare prompt wraps the correct JSON in markdown fences), only to show the shape of a report without any API. The output is real (a test keeps it identical to the current code).

$ python3 -m paired_eval
有效样本: 2/4 题有信息(恒过 2, 恒败 0)
拒答: {'strict': 0, 'bare': 0}
触顶(1.000): strict —— 该系统无余量, 它作为参照时效应会被压缩
bare vs strict: Δ=-0.500 CI95=[-1.000,+0.000] | 逐题p=0.506 逐轮McNemar=0.00781 Holm=0.00781 | 不一致对 0:8 集中度=0.50 | 显著: Δ=-0.500 CI95=[-1.000,+0.000] p=0.0078 (n=16)
$ python3 -m paired_eval --lang en
informative sample: 2/4 tasks informative (always-pass 2, always-fail 0)
refusals: {'strict': 0, 'bare': 0}
at ceiling (1.000): strict — no headroom; effects measured against it as the reference are compressed
bare vs strict: Δ=-0.500 CI95=[-1.000,+0.000] | per-task p=0.506 per-round McNemar=0.00781 Holm=0.00781 | discordant 0:8 concentration=0.50 | significant: Δ=-0.500 CI95=[-1.000,+0.000] p=0.0078 (n=16)

两题两系统全对、不携带信息(有效样本 2/4);8 个不一致对全部偏向 strict,分布在 2 题上(集中度 0.50,非单题异常);逐题置换 p=0.506 是因为只有 2 道有信息题,它的最小可能 p 就是 0.5——而逐轮 McNemar 用上全部 16 个配对单元。末段是可直接写进报告的结论。Two tasks were solved by both systems every time and carry no information (informative sample 2/4); all 8 discordant pairs favour strict, spread over 2 tasks (concentration 0.50); the per-task permutation p is 0.506 because with 2 informative tasks its minimum attainable p is 0.5 — per-round McNemar uses all 16 paired units. The last clause is a verdict you can paste into a report.

特性Features

三个对象各固定各的变量Three objects, each holding its own variable constant

评 model 固定脚手架、评 harness 固定模型、评 agent 固定两者只换策略;2×2 因子看交互。Model with the scaffold fixed, harness with the model fixed, agent with both fixed; a 2×2 for the interaction.

程序做 gate,rubric 做 scoreThe program gates, the rubric scores

gated 类:验不过直接 0 分且不调用评委;验过的才由 rubric 分质量。gated class: failing the gate scores 0 and never calls the judge; only passing answers are ranked by the rubric.

同题交错重复、逐题配对Interleaved repeats, paired per task

不是两个独立均值的差;报效应量与 bootstrap 区间。Not a difference of independent means; effect size with a bootstrap interval.

配对精确检验Exact paired tests

McNemar(二值)、符号翻转置换(连续分),Holm 校正;不假设正态。McNemar (binary), sign-flip permutation (continuous), Holm correction; no normality assumption.

四种结论,不是一个 p 值Four verdicts, not one p-value

显著 / 有界的 null / 无信息 / 检验无力——缺的是不一致对还是单元,处方不同。Significant / bounded null / uninformative / powerless — and whether what's missing is discordant pairs or units.

有效样本与触顶诊断Informative-sample and ceiling diagnostics

哪些题没在贡献信息、哪个系统满分导致效应被压缩。Which tasks contribute nothing; which system's perfect score compresses the effect.

事前样本量规划Sample-size planning

required_tasks / required_pairs 内部真跑将要使用的检验,不用闭式近似。required_tasks / required_pairs run the very test that will be used, not a closed-form approximation.

rubric 自身要过 canaryThe rubric itself must pass a canary

一份会被糊弄回答骗过的 rubric 不能用来评分;rubric_canary 先测分离度。A rubric that a bluffing answer can game is not fit to score; rubric_canary measures the separation first.

四种结论,而不是一个 p 值Four verdicts, not one p-value

“p > 0.05” 有三种完全不同的含义,处方各异。interpret() 把每次比较翻译成其中一种:“p > 0.05” means three very different things with different remedies. interpret() translates every comparison into one of these:

显著significant效应量与区间,可直接写进报告。Effect size and interval, ready for a report.
有界的 nullbounded null未检出差异,但说清能排除多大效应(MDE)。No difference detected — with the effect size this design rules out (MDE).
无信息uninformative样本太小,任何效应都检不出;不能当作『无差异』的证据。Too small to detect anything; not evidence of 'no difference'.
检验无力powerlessp 有地板:不一致对/非零差值对不够,效应再大也到不了显著,并说明缺多少。The p-value has a floor: too few discordant or nonzero-difference pairs; no effect could reach significance. Says how many are missing.

用真实模型:三个对象各做一次 A/BWith real models: one A/B per object

任务按“有什么可验”混搭验证类;bench_tasks 把 judge 评分的题按显式阈值二值化,进同一套配对流水线;三个维度各固定各的变量。Mix verification classes by what can be verified; bench_tasks binarises judge-scored tasks at an explicit threshold so they flow through the same paired pipeline; each axis holds its own variable constant.

import paired_eval as pe
from examples.adapter_openai_compat import make_call, make_llm     # 只用标准库的 OpenAI 兼容适配器

my_tasks = [   # 按"有什么可验"混搭: 有真值 -> exact; 有真值又要看质量 -> gated; 只有资料 -> trajectory
    {"id": "date", "instruction": "把 2024年3月5日 写成 ISO 8601 日期; 以「答案: 」开头给出",
     "verification": {"class": "exact", "gold": "2024-03-05"}},
    {"id": "sum-explained", "instruction": "12×12 等于多少, 并说明过程; 以「答案: 」开头给出结果",
     "verification": {"class": "gated",
                      "gate":  {"class": "exact", "gold": "144", "kind": "numeric"},
                      "score": {"class": "rubric", "criteria": [{"text": "说明了计算过程", "weight": 1}]}}},
]
call_a, call_b, judge = make_call(model="model-a"), make_call(model="model-b"), make_llm(model="judge-model")
tasks = pe.bench_tasks(my_tasks, threshold=1.0, llm=judge)        # judge 评分的题也进同一套配对流水线

STRICT = "严格按要求输出, 不要任何多余内容。要求: "
runs = {
    "model":   pe.run_interleaved({"A": pe.make_model(call_a), "B": pe.make_model(call_b)}, tasks=tasks, n=6, prompt_prefix=STRICT),
    "harness": pe.run_interleaved({"strict": pe.make_model(lambda p: call_a(STRICT + p)), "bare": pe.make_model(call_a)}, tasks=tasks, n=6, prompt_prefix=""),
}
for axis, run in runs.items():
    print(axis, pe.report(run["reports"], refusals=run["refusals"])["text"], sep="\n")
import paired_eval as pe
from examples.adapter_openai_compat import make_call, make_llm     # standard-library-only OpenAI-compatible adapter

my_tasks = [   # mix by what can be verified: ground truth -> exact; truth + quality bar -> gated; only sources -> trajectory
    {"id": "date", "instruction": "Write 5 March 2024 as an ISO 8601 date, starting with 'Answer: '",
     "verification": {"class": "exact", "gold": "2024-03-05", "marker": "Answer"}},
    {"id": "sum-explained", "instruction": "What is 12×12? Explain, and give the result starting with 'Answer: '",
     "verification": {"class": "gated",
                      "gate":  {"class": "exact", "gold": "144", "kind": "numeric", "marker": "Answer"},
                      "score": {"class": "rubric", "criteria": [{"text": "explains the calculation", "weight": 1}]}}},
]
call_a, call_b, judge = make_call(model="model-a"), make_call(model="model-b"), make_llm(model="judge-model")
tasks = pe.bench_tasks(my_tasks, threshold=1.0, llm=judge)        # judge-scored tasks flow through the same paired pipeline

STRICT = "Follow the format exactly, no extra text. Task: "
runs = {
    "model":   pe.run_interleaved({"A": pe.make_model(call_a), "B": pe.make_model(call_b)}, tasks=tasks, n=6, prompt_prefix=STRICT),
    "harness": pe.run_interleaved({"strict": pe.make_model(lambda p: call_a(STRICT + p)), "bare": pe.make_model(call_a)}, tasks=tasks, n=6, prompt_prefix=""),
}
for axis, run in runs.items():
    print(axis, pe.report(run["reports"], refusals=run["refusals"], lang="en")["text"], sep="\n")

agent 维度同理:同一模型同一脚手架,比“单遍”与“出草稿后自检修正”。还能做:事前样本量规划(required_tasks / required_pairs)、筛出有区分力的题(screen_tasks / screen_graded)。完整示例见 README。The agent axis works the same way: same model, same scaffold, one pass vs draft-then-self-correct. Also: sample-size planning (required_tasks / required_pairs) and screening for tasks that discriminate (screen_tasks / screen_graded). Full example in the README.

文档Documentation

方法学教训Methodology lessons

每个统计原语防的是什么错:p 地板、地板的基数、MDE、null 必须附界、天花板、筛选可靠性……What each primitive guards against: the p floor and its basis, MDE, nulls with bounds, ceilings, screening reliability …

案例研究Case study

用本工具对一对模型、三族任务做的实测;数字是实例特定的,展示的是报告该怎么写。One model pair, three task families; the numbers are instance-specific and show how a conclusion should be written.

纠正记录Corrections

被推翻或修正过的结论,原文保留、指向新证据。Conclusions that were overturned or corrected, kept in place and pointing at the new evidence.

参与贡献Contributing

钩子、变异测试基线、统计代码的额外纪律。Hooks, the mutation-testing baseline, extra discipline for statistical code.

更新日志Changelog

Keep a Changelog 格式;Keep a Changelog format; 发布页releases

OpenAI 兼容适配器OpenAI-compatible adapter

只用标准库;把任意兼容端点接成 call / judge。Standard library only; turns any compatible endpoint into call / judge.