"""
=============================================================================
第 14 讲 · 02：没有标准答案时怎样评估 —— 和专家的交付物两两比较，以及 LLM 评委的偏差
=============================================================================

对应讲义：lectures/lecture14_agent_evaluation.md 第 3、5 节（Patwardhan et al. 2025, GDPval）

-----------------------------------------------------------------------------
【GDPval 式的评估】
-----------------------------------------------------------------------------
每个任务是一项真实的工作（写一份法律备忘录、做一个销售报表……），有一份**行业专家**做的交付物。
模型也做一份。另一位专家在**不知道哪份是谁做的**情况下比较两份，判断：模型更好 / 一样好 / 专家更好。
指标：模型"胜或平"的比例（win-or-tie rate）。50% 左右意味着和专家旗鼓相当。

toy 版本：
    · 每个任务：专家交付物的质量 h ~ N(0, 1)；模型交付物的质量 m ~ N(μ, 1)，μ 是模型的平均水平。
    · 专家评委：看到的质量差 = (m − h) + N(0, 0.5²)（评委也有判断误差）；|差| < 0.15 判"一样好"。
    · 自动评委（LLM 评委）：判断误差更大（N(0, 0.8²)），而且有两种偏差（设定的）：
        位置偏差：放在前面的那份，质量差多算 +0.3；
        长度偏差：更长的那份多算 +0.4 × (长度差)。模型 C 的"啰嗦"版本和模型 C 的交付物质量完全相同，
        只是总比专家的长 1 个单位。
所有数值都是**设定**的。演示的是：置信区间有多宽、评委之间有多一致、评委的偏差怎样影响结论、怎样修正。

运行时间：约 1 秒（只用标准库）。
"""

import math
import random

N_TASKS = 220               # GDPval 公开的 gold 子集有 220 个任务
HUMAN_NOISE, JUDGE_NOISE = 0.5, 0.8
TIE_BAND = 0.15
POSITION_BIAS, LENGTH_BIAS = 0.3, 0.4
N_BOOT = 1000
MODELS = (("模型 A（较弱）", -1.0, 0.0), ("模型 B", -0.4, 0.0), ("模型 C", -0.1, 0.0),
          ("模型 C 的\"啰嗦\"版本", -0.1, 1.0))      # (名字, 平均质量 μ, 比专家交付物长多少)


def make_tasks(rng):
    """每个任务的专家交付物质量 h。"""
    return [rng.gauss(0, 1) for _ in range(N_TASKS)]


def model_outputs(mu, tasks, rng):
    """模型在每个任务上的交付物质量 m ~ N(μ, 1)。"""
    return [rng.gauss(mu, 1) for _ in tasks]


def judge(m, h, rng, noise, model_first=None, length_gap=0.0):
    """
    一次比较的结果：+1（模型更好）、0（一样好）、−1（专家更好）。

    【参数】
        noise       ：评委的判断误差（标准差）
        model_first ：None 表示评委没有位置偏差（人类专家，或者已经修正过）；
                      True / False 表示模型的交付物放在前面 / 后面，有位置偏差
        length_gap  ：模型交付物比专家的长多少（只对有长度偏差的自动评委起作用，调用者负责传 0）
    【思路】评委"看到"的质量差 = 真实的质量差 + 判断误差 + 偏差；落在 ±TIE_BAND 之内算平局。
    """
    d = (m - h) + rng.gauss(0, noise) + LENGTH_BIAS * length_gap
    if model_first is not None:
        d += POSITION_BIAS if model_first else -POSITION_BIAS
    if abs(d) < TIE_BAND:
        return 0
    return 1 if d > 0 else -1


def win_or_tie(results):
    return sum(1 for r in results if r >= 0) / len(results)


def bootstrap_ci(results, rng):
    """对任务重抽样，得到胜或平比例的 95% 区间。"""
    stats = []
    n = len(results)
    for _ in range(N_BOOT):
        stats.append(win_or_tie([results[rng.randrange(n)] for _ in range(n)]))
    stats.sort()
    return stats[int(0.025 * N_BOOT)], stats[int(0.975 * N_BOOT) - 1]


def true_win_or_tie(mu):
    """
    无限多任务时的真实胜或平比例（数值积分）。

    【思路】看到的质量差 d ~ N(μ, 1 + 1 + 0.5²)；胜或平 = P(d > −TIE_BAND)。用误差函数 erf 计算正态分布的累积概率。
    """
    sd = math.sqrt(2 + HUMAN_NOISE ** 2)
    return 0.5 * (1 + math.erf((mu + TIE_BAND) / (sd * math.sqrt(2))))


def main():
    rng = random.Random(1410)
    tasks = make_tasks(rng)
    outputs = {name: model_outputs(mu, tasks, rng) for name, mu, _ in MODELS[:3]}
    outputs[MODELS[3][0]] = outputs[MODELS[2][0]]           # "啰嗦"版本：质量和模型 C 完全一样，只是更长

    print("=" * 78)
    print(f"演示 1：专家盲评的\"胜或平\"比例（{N_TASKS} 个任务，自助法 95% 区间）")
    print("=" * 78)
    ci_rng = random.Random(1411)
    for name, mu, _ in MODELS[:3]:
        res = [judge(m, h, rng, HUMAN_NOISE) for m, h in zip(outputs[name], tasks)]
        lo, hi = bootstrap_ci(res, ci_rng)
        print(f"  {name:<10} 胜或平 {win_or_tie(res):.3f}  [{lo:.3f}, {hi:.3f}]   （无限多任务时的真实值 {true_win_or_tie(mu):.3f}）")

    print("\n" + "=" * 78)
    print("演示 2：评委之间有多一致？（同一批比较，模型 C；两位评委的结论相同的比例）")
    print("=" * 78)
    name = MODELS[2][0]
    pairs = list(zip(outputs[name], tasks))
    expert1 = [judge(m, h, rng, HUMAN_NOISE) for m, h in pairs]
    expert2 = [judge(m, h, rng, HUMAN_NOISE) for m, h in pairs]
    auto = [judge(m, h, rng, JUDGE_NOISE, model_first=rng.random() < 0.5) for m, h in pairs]
    agree = lambda x, y: sum(a == b for a, b in zip(x, y)) / len(x)          # noqa: E731
    print(f"  专家 1 vs 专家 2：{agree(expert1, expert2):.3f}")
    print(f"  自动评委 vs 专家 1：{agree(auto, expert1):.3f}")

    print("\n" + "=" * 78)
    print("演示 3：自动评委的偏差（胜或平比例；专家盲评作为参照）")
    print("=" * 78)
    print(f"{'':24}{'专家盲评':>10}{'模型总在前':>12}{'模型总在后':>12}{'两种顺序都评':>14}")
    for name, mu, gap in MODELS[1:]:
        pairs = list(zip(outputs[name], tasks))
        expert = win_or_tie([judge(m, h, rng, HUMAN_NOISE) for m, h in pairs])
        first = win_or_tie([judge(m, h, rng, JUDGE_NOISE, True, gap) for m, h in pairs])
        last = win_or_tie([judge(m, h, rng, JUDGE_NOISE, False, gap) for m, h in pairs])
        both = []
        for m, h in pairs:                                       # 每个任务两种顺序各评一次，取平均
            a = judge(m, h, rng, JUDGE_NOISE, True, gap) >= 0
            b = judge(m, h, rng, JUDGE_NOISE, False, gap) >= 0
            both.append((a + b) / 2)
        width = sum(2 if ord(ch) > 127 else 1 for ch in name)
        print(name + " " * max(1, 24 - width) + f"{expert:10.3f}{first:12.3f}{last:12.3f}{sum(both) / len(both):14.3f}")

    print("\n观察：")
    print("  · 220 个任务的胜或平比例，95% 区间宽约 ±6 个百分点：两个模型差 5 个点，单靠这个 benchmark 很难分清。")
    print("  · 专家之间本身就不完全一致；自动评委和专家的一致率比专家之间低一些（GDPval：自动评委 66%，专家之间 71%）。")
    print("    评委的噪声给\"真实的胜率\"设了一个天花板：再好的模型，也不会被评委一致地判为更好。")
    print("  · 位置偏差：同一批交付物，模型总放在前面和总放在后面，胜或平比例差 10 多个百分点。")
    print("    修正很简单：每个比较两种顺序各评一次（或随机顺序），结果回到接近专家盲评的水平。")
    print("  · 长度偏差修正不了：\"啰嗦\"版本和模型 C 的质量完全一样，自动评委却给它更高的分，")
    print("    两种顺序都评也消不掉。要靠控制长度、让评委按细则打分、定期和专家评分对照来发现和纠正。\n")


if __name__ == "__main__":
    main()
