VKВлад КуликовОткрытые лекции
Современные LLM19 / 20

Модуль 19. Оценка современных LLM: от бенчмарка к инженерному решению

Здесь можно прочитать пояснения и код имеющихся ноутбуков или скачать файлы. Код на сайте не запускается.

Скачать файлы курса (.zip) 2588,1 КБ

Архив содержит файлы на обоих языках и вспомогательные материалы с сохранением структуры каталогов.

Ноутбук 19. Оценка LLM-систем: от одного вызова к проверяемому решению

Основной ноутбук модуля

Скачать .ipynb module_19_notebook_eval.ipynb · 62,9 КБ

Показать текст ноутбука

Ноутбук 19. Оценка LLM-систем: от одного вызова к проверяемому решению

Модуль 19 — практический notebook

Здесь мы строим небольшой, но строгий контур оценки. Его главный принцип прост: один пример вызывает модель ровно один раз. Показанный ответ, все метрики, задержка, стоимость и парное сравнение вычисляются из одного сохранённого наблюдения. Повторный вызов стохастической или зависящей от состояния модели может дать другой текст и сделать отчёт внутренне противоречивым.

Notebook проходит десять шагов:

  1. грубая оценка неопределённости по агрегированным долям;

  2. Wilson interval для одной доли;

  3. точный тестом Мак-Немара для парных исходов;

  4. несмещённый pass@k для конечной выборки;

  5. типизированная исполняющая обвязка с контрактом «один вызов»;

  6. отчёт по срезам и парное сравнение двух систем;

  7. перестановочный протокол для модельного судьи;

  8. расчёт размера частного золотого набора и контрольной строки-canary;

  9. синтетический профиль «потерянного в середине»;

  10. негативные тесты входных данных, модели и метрик.

Это учебная система, а не готовая платформа оценки. Она не обращается к сети, не требует API-ключей и выполняется на CPU в чистой сессии Colab. Все числа сценариев встроены и защищены SHA-256.

При первом проходе выполните все ячейки по порядку и разберите сохранённые ответы восьми вымышленных примеров. При втором изменяйте по одному допущению; проектный ноутбук добавляет 200 запросов, парный bootstrap и проверку состояния. API, веса моделей и GPU не нужны.

0. Встроенные численные и тестовые контракты

Эталонные числа модуля и небольшой набор примеров встроены в notebook отдельно от текста лекции. Каноническая JSON-сериализация и SHA-256 позволяют обнаружить случайное расхождение между лекцией, ответами и вычислениями.

Python
import hashlib
import json
import math
import re
import string
import unicodedata
from collections import defaultdict
from dataclasses import dataclass
from typing import Callable, Mapping, Sequence

REFERENCE_JSON = '{"golden":{"canary_guess_probability":2.938735877055719e-39,"canary_hex_characters":32,"cost":{"completion_tokens":150,"dense_flops_floor":6183100000000000.0,"effective_flops_per_second":495000000000000.0,"examples":700,"parameter_count":8030000000,"prompt_tokens":400,"seconds_floor":12.491111111111111,"total_tokens":385000},"expected_rate":0.8,"n_for_3pp":683,"n_for_5pp":246},"judge":{"averaged_a_rate":0.5,"calls_per_pair":6,"forward_a_wins":650,"judges":3,"orders":2,"pairs":1000,"position_bias":0.15,"swapped_a_wins":350},"leaderboard":{"aggregate_approximation":{"ci_high":0.03136546073938403,"ci_low":-0.021365460739384022,"delta":0.0050000000000000044,"half_width":0.026365460739384027,"se":0.013451765683359198,"z_score":0.3716984162298756},"first_rate":0.955,"paired_demo":{"both_correct":150,"both_wrong":20,"delta":0.05,"first_accuracy":0.85,"first_only":20,"mcnemar_p":0.09873714670538902,"second_accuracy":0.8,"second_only":10,"total":200},"required_tasks":{"0.005":14599,"0.01":3650,"0.02":913,"0.05":146},"sample_size":500,"second_rate":0.95},"lost_in_middle":{"dip":0.35,"edge":0.95,"mean":0.7749999999999999,"threshold":0.9,"working_share":0.2467517144288497,"working_tokens_of_1m":246752},"pass_at_k":{"correct_samples":4,"naive":{"1":0.19999999999999998,"10":0.8926258176,"5":0.67232},"total_samples":20,"unbiased":{"1":0.19999999999999998,"10":0.9566563467492261,"5":0.718266253869969}},"schema_version":1}'
REFERENCE_SHA256 = "d89b860e2938e3c662854b634cbf9bf5ded99aded5af80555a3a9f76d154f9ef"
FIXTURE_JSON = '{"examples":[{"expected":"париж","id":"geo_fr","prompt":"столица Франции","slice":"facts"},{"expected":"4","id":"math_2","prompt":"2+2","slice":"math"},{"expected":"au","id":"chem_au","prompt":"химический символ золота","slice":"facts"},{"expected":"1969","id":"hist_moon","prompt":"год высадки на Луну","slice":"facts"},{"expected":"42","id":"math_42","prompt":"сколько будет 7*6","slice":"math"},{"expected":"токио","id":"geo_jp","prompt":"столица Японии","slice":"facts"},{"expected":"9","id":"math_sqrt","prompt":"квадратный корень из 81","slice":"math"},{"expected":"холодный","id":"lang_antonym","prompt":"антоним слова горячий","slice":"language"}],"model_a":["Париж.","4","AU","1969","42","Варшава","9","холодный"],"model_b":["париж","четыре","ag","1969","42","Токио","9","холодный."]}'
FIXTURE_SHA256 = "c7528a138a167486717962112054664f8f548356a10b8c583633f49d41209535"


def canonical_json(value) -> str:
    """Serialize JSON deterministically for embedded-contract verification."""

    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


REFERENCE = json.loads(REFERENCE_JSON)
FIXTURE = json.loads(FIXTURE_JSON)
assert hashlib.sha256(canonical_json(REFERENCE).encode("utf-8")).hexdigest() == REFERENCE_SHA256
assert hashlib.sha256(canonical_json(FIXTURE).encode("utf-8")).hexdigest() == FIXTURE_SHA256
assert REFERENCE["schema_version"] == 1
print('Встроенные контракты М19 проверены')

1. Планирование и наблюдаемые исходы

Вероятности 0,955 и 0,950 при N=500 относятся к планированию. Наблюдаемые доли одного бинарного прогона меняются с шагом 0,002: 95,5% получить нельзя. Стандартная ошибка измеряет разброс оценки между выборками; множитель 1,96 задаёт нормальную 95-процентную полуширину. Число 14 599 планирует полуширину 0,5 пункта для двух независимых долей, а не мощность обнаружения эффекта.

Для наблюдаемой одной доли используем интервал Вилсона; для двух систем на одинаковых задачах сохраняем пары. Точный тест Мак-Немара использует только несовпадения. При N=200 таблицы (150,20,10,20) и (160,10,0,30) дают те же 85%/80%, но p≈0,098737 и p=0,001953. Отсутствие убедительного различия не доказывает эквивалентность.

Python
def _positive_int(name: str, value: int) -> int:
    """Validate and return a strictly positive, non-boolean integer."""
    if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
        raise ValueError(f"{name} must be a positive integer")
    return value

def _non_negative_int(name: str, value: int) -> int:
    """Validate and return a non-negative, non-boolean integer."""
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        raise ValueError(f"{name} must be a non-negative integer")
    return value

def _probability(name: str, value: float) -> float:
    """Validate and return a finite probability in the closed unit interval."""
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError(f"{name} must be a real number")
    number = float(value)
    if not math.isfinite(number) or not 0.0 <= number <= 1.0:
        raise ValueError(f"{name} must lie in [0, 1]")
    return number

def _positive_finite(name: str, value: float) -> float:
    """Validate and return a finite positive real number."""
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError(f"{name} must be a real number")
    number = float(value)
    if not math.isfinite(number) or number <= 0.0:
        raise ValueError(f"{name} must be finite and positive")
    return number

def wilson_interval(successes: int, total: int, z_value: float = 1.96) -> tuple[float, float]:
    """Return the Wilson score interval for a binomial proportion."""
    n = _positive_int("total", total)
    s = _non_negative_int("successes", successes)
    if s > n:
        raise ValueError("successes cannot exceed total")
    z_value = _positive_finite("z_value", z_value)
    p = s / n
    z2 = z_value**2
    denominator = 1.0 + z2 / n
    centre = (p + z2 / (2.0 * n)) / denominator
    radius = z_value * math.sqrt(p * (1.0 - p) / n + z2 / (4.0 * n**2)) / denominator
    return max(0.0, centre - radius), min(1.0, centre + radius)

def mcnemar_exact_pvalue(first_only: int, second_only: int) -> float:
    """Return the two-sided exact McNemar p-value for discordant paired outcomes."""
    first_only = _non_negative_int("first_only", first_only)
    second_only = _non_negative_int("second_only", second_only)
    discordant = first_only + second_only
    if discordant == 0:
        return 1.0
    tail = min(first_only, second_only)
    cdf = sum(math.comb(discordant, k) for k in range(tail + 1)) / (1 << discordant)
    return min(1.0, 2.0 * cdf)

L = REFERENCE["leaderboard"]
A = L["aggregate_approximation"]
assert abs(A["z_score"] - 0.3716984162298756) < 1e-12
assert A["ci_low"] < 0.0 < A["ci_high"]
assert L["required_tasks"]["0.005"] == 14599
P = L["paired_demo"]
assert abs(mcnemar_exact_pvalue(P["first_only"], P["second_only"]) - P["mcnemar_p"]) < 1e-15
lo, hi = wilson_interval(7, 8)
assert 0.0 <= lo < 7 / 8 < hi <= 1.0
print(f"planning probabilities: delta={A['delta']*100:.1f} pp, z={A['z_score']:.2f}, CI=[{A['ci_low']*100:.1f}, {A['ci_high']*100:.1f}] pp")
print(f"paired demo: {P['first_accuracy']:.0%} vs {P['second_accuracy']:.0%}, exact McNemar p={P['mcnemar_p']:.3f}")
print('Агрегированная аппроксимация, интервал Уилсона и точный McNemar проверены')

2. pass@k: конечная выборка без подстановочной иллюзии

Если из nn сгенерированных решений cc успешны, несмещённая оценка равна

pass@⁡k=1−(n−ck)(nk).\operatorname{pass@}k=1-\frac{\binom{n-c}{k}}{\binom{n}{k}}.

Она моделирует выбор kk кандидатов без возвращения из уже полученной конечной выборки. Подстановка 1−(1−c/n)k1-(1-c/n)^k использует шумную оценку вероятности и может заметно отличаться. Реализация явно проверяет 0≤c≤n0\le c\le n, 1≤k≤n1\le k\le n и крайние случаи c=0c=0 и c=nc=n.

Несмещённость относительно 1−(1−p)^k требует независимых генераций из неизменной политики. Подстановка c/n оценивает ту же целевую вероятность, но нелинейность создаёт смещение. Формулу считают по каждой задаче, затем усредняют; отобранные заранее лучшие ответы не образуют подходящую выборку.

Python
def pass_at_k_unbiased(total_samples: int, correct_samples: int, k: int) -> float:
    """Return the finite-sample unbiased pass@k estimator of Chen et al."""
    n = _positive_int("total_samples", total_samples)
    c = _non_negative_int("correct_samples", correct_samples)
    k = _positive_int("k", k)
    if c > n:
        raise ValueError("correct_samples cannot exceed total_samples")
    if k > n:
        raise ValueError("k cannot exceed total_samples")
    if n - c < k:
        return 1.0
    # Use the shorter symmetric product and avoid cancellation near zero.
    if c == 0:
        return 0.0
    count, removed = (k, c) if k <= c else (c, k)
    if removed / n == 1.0:  # the success probability rounds to one at float precision
        return 1.0
    return -math.expm1(sum(math.log1p(-removed / (n - i)) for i in range(count)))

def pass_at_k_naive(total_samples: int, correct_samples: int, k: int) -> float:
    """Return the plug-in estimate ``1 - (1 - c/n)**k`` for comparison."""
    n = _positive_int("total_samples", total_samples)
    c = _non_negative_int("correct_samples", correct_samples)
    k = _positive_int("k", k)
    if c > n:
        raise ValueError("correct_samples cannot exceed total_samples")
    if k > n:
        raise ValueError("k cannot exceed total_samples")
    return 1.0 if c / n == 1.0 else -math.expm1(k * math.log1p(-c / n))

R = REFERENCE["pass_at_k"]
n, c = R["total_samples"], R["correct_samples"]
for k in (1, 5, 10):
    unbiased = pass_at_k_unbiased(n, c, k)
    naive = pass_at_k_naive(n, c, k)
    assert abs(unbiased - R["unbiased"][str(k)]) < 1e-15
    assert abs(naive - R["naive"][str(k)]) < 1e-15
    print(f"pass@{k:>2}: unbiased={unbiased:.4f}, plug-in={naive:.4f}, delta={(naive-unbiased)*100:+.1f} pp")
assert pass_at_k_unbiased(20, 0, 10) == 0.0
assert pass_at_k_unbiased(20, 20, 1) == 1.0
assert all(pass_at_k_unbiased(n, c, k) <= pass_at_k_unbiased(n, c, k + 1) for k in range(1, n))
print('Несмещённый pass@k и крайние случаи проверены')

3. Один сохранённый ответ для всех метрик

Вызываем каждую систему один раз на пример в текущей попытке. Восемь авторских примеров с заранее заданными ответами проверяют обвязку, а не способности LLM. Версию системы и номер попытки при полном запуске добавляют к ключу записи. Задержка и число токенов здесь неизвестны и равны None: число слов не выдаётся за токены.

Ошибка вызова получает нулевой успех по объявленному правилу; ошибка программы-метрики делает агрегат недействительным (None), сохраняя ответ для расследования. Нельзя молча исключать неудачные строки. Нормализация разрешает регистр, пробелы и конечную пунктуацию фразы, но сохраняет знак минуса и десятичную точку внутри ответа. Для кода или JSON потребуется другая метрика.

exact_match применяет только strip и casefold. Более мягкая метрика normalized_exact_match дополнительно использует NFKC, сводит внутренние пробелы и удаляет оговорённые конечные знаки. Поэтому две метрики могут дать разные доли на одних ответах.

Python
@dataclass(frozen=True)
class EvalExample:
    """One immutable golden-set example with a predeclared slice."""

    example_id: str
    prompt: str
    expected: str
    slice_name: str

    def __post_init__(self) -> None:
        """Validate that every identifying field is a non-empty string."""

        for name, value in (
            ("example_id", self.example_id),
            ("prompt", self.prompt),
            ("expected", self.expected),
            ("slice_name", self.slice_name),
        ):
            if not isinstance(value, str) or not value.strip():
                raise ValueError(f"{name} must be a non-empty string")


@dataclass(frozen=True)
class ModelResponse:
    """One response with optional measured accounting; unknown values remain None."""

    text: str
    latency_ms: float | None = None
    output_tokens: int | None = None

    def __post_init__(self) -> None:
        """Validate response text and optional accounting fields."""

        if not isinstance(self.text, str):
            raise TypeError("text must be a string")
        if self.latency_ms is not None and (isinstance(self.latency_ms, bool) or not isinstance(self.latency_ms, (int, float)) or not math.isfinite(self.latency_ms) or self.latency_ms < 0):
            raise ValueError("latency_ms must be finite and non-negative")
        if self.output_tokens is not None:
            _non_negative_int("output_tokens", self.output_tokens)


@dataclass(frozen=True)
class PredictionRecord:
    """Stored response and all metric values for one example."""

    example_id: str
    slice_name: str
    expected: str
    prediction: str
    scores: Mapping[str, float | None]
    latency_ms: float | None
    output_tokens: int | None
    error: str | None = None


@dataclass(frozen=True)
class EvaluationReport:
    """Deterministic evaluation report built from call-once prediction records."""

    records: tuple[PredictionRecord, ...]
    aggregate: Mapping[str, float | None]
    by_slice: Mapping[str, Mapping[str, float | None]]
    model_calls: int
    error_count: int


def normalize_answer(text: str) -> str:
    """Normalize case, whitespace, and terminal punctuation without deleting signs."""

    if not isinstance(text, str):
        raise TypeError("answer must be a string")
    text = unicodedata.normalize("NFKC", text).casefold()
    text = text.strip().rstrip(".!?。!?")  # task-specific terminal sentence punctuation only
    return " ".join(text.split())


def exact_match(prediction: str, expected: str) -> float:
    """Return strict case-insensitive exact match after outer whitespace removal."""

    if not isinstance(prediction, str) or not isinstance(expected, str):
        raise TypeError("prediction and expected must be strings")
    return float(prediction.strip().casefold() == expected.strip().casefold())


def normalized_exact_match(prediction: str, expected: str) -> float:
    """Return exact match after the explicit normalization above."""

    return float(normalize_answer(prediction) == normalize_answer(expected))


def _validate_dataset(dataset: Sequence[EvalExample]) -> tuple[EvalExample, ...]:
    """Freeze a non-empty dataset and reject duplicate example IDs."""

    items = tuple(dataset)
    if not items:
        raise ValueError("dataset must be non-empty")
    if not all(isinstance(item, EvalExample) for item in items):
        raise TypeError("every dataset item must be EvalExample")
    ids = [item.example_id for item in items]
    if len(ids) != len(set(ids)):
        raise ValueError("example_id values must be unique")
    return items


def evaluate(
    model_fn: Callable[[str], str | ModelResponse],
    dataset: Sequence[EvalExample],
    metrics: Mapping[str, Callable[[str, str], float]],
    *,
    max_response_chars: int = 10_000,
) -> EvaluationReport:
    """Call the model exactly once per example and score the stored response."""

    items = _validate_dataset(dataset)
    if not callable(model_fn):
        raise TypeError("model_fn must be callable")
    if not metrics or any(not isinstance(name, str) or not name for name in metrics):
        raise ValueError("metrics must be a non-empty mapping with non-empty names")
    if any(not callable(metric) for metric in metrics.values()):
        raise TypeError("every metric must be callable")
    max_response_chars = _positive_int("max_response_chars", max_response_chars)

    records: list[PredictionRecord] = []
    calls = 0
    for example in items:
        calls += 1
        try:
            raw = model_fn(example.prompt)  # the only model invocation for this example
            response = raw if isinstance(raw, ModelResponse) else ModelResponse(raw)
            if len(response.text) > max_response_chars:
                raise ValueError("model response exceeds max_response_chars")
        except Exception as exc:
            records.append(PredictionRecord(
                example_id=example.example_id,
                slice_name=example.slice_name,
                expected=example.expected,
                prediction="",
                scores={name: 0.0 for name in metrics},
                latency_ms=None,
                output_tokens=None,
                error=f"model: {type(exc).__name__}: {exc}",
            ))
            continue

        try:
            values: dict[str, float] = {}
            for name, metric in metrics.items():
                value = metric(response.text, example.expected)
                if isinstance(value, bool) or not isinstance(value, (int, float)):
                    raise TypeError(f"metric {name!r} must return a real number")
                value = float(value)
                if not math.isfinite(value) or not 0.0 <= value <= 1.0:
                    raise ValueError(f"metric {name!r} must return a finite value in [0, 1]")
                values[name] = value
            error = None
        except Exception as exc:
            # Keep the single model response visible even when a metric implementation fails.
            values = {name: None for name in metrics}
            error = f"metric: {type(exc).__name__}: {exc}"

        records.append(PredictionRecord(
            example_id=example.example_id,
            slice_name=example.slice_name,
            expected=example.expected,
            prediction=response.text,
            scores=values,
            latency_ms=response.latency_ms,
            output_tokens=response.output_tokens,
            error=error,
        ))

    aggregate = {
        name: (None if any(record.scores[name] is None for record in records) else
               sum(record.scores[name] for record in records) / len(records))
        for name in metrics
    }
    groups: dict[str, list[PredictionRecord]] = defaultdict(list)
    for record in records:
        groups[record.slice_name].append(record)
    by_slice = {
        slice_name: {
            name: (None if any(record.scores[name] is None for record in group) else
                   sum(record.scores[name] for record in group) / len(group))
            for name in metrics
        }
        for slice_name, group in sorted(groups.items())
    }
    return EvaluationReport(
        records=tuple(records),
        aggregate=aggregate,
        by_slice=by_slice,
        model_calls=calls,
        error_count=sum(record.error is not None for record in records),
    )


class CountingTableModel:
    """Deterministic mock model that exposes how many times it was called."""

    def __init__(self, prompts: Sequence[str], outputs: Sequence[str]) -> None:
        """Build a one-to-one prompt table and initialize the call counter."""

        if len(prompts) != len(outputs):
            raise ValueError("prompts and outputs must have equal length")
        self._table = dict(zip(prompts, outputs))
        if len(self._table) != len(prompts):
            raise ValueError("prompts must be unique")
        self.calls = 0

    def __call__(self, prompt: str) -> ModelResponse:
        """Return one deterministic response and increment the visible counter."""

        self.calls += 1
        if prompt not in self._table:
            raise KeyError(prompt)
        text = self._table[prompt]
        return ModelResponse(text=text)


DATASET = tuple(EvalExample(
    example_id=item["id"], prompt=item["prompt"], expected=item["expected"], slice_name=item["slice"]
) for item in FIXTURE["examples"])
METRICS = {"exact": exact_match, "normalized_exact": normalized_exact_match}
MODEL_A = CountingTableModel([e.prompt for e in DATASET], FIXTURE["model_a"])
REPORT_A = evaluate(MODEL_A, DATASET, METRICS)
assert MODEL_A.calls == len(DATASET) == REPORT_A.model_calls
assert REPORT_A.error_count == 0
assert REPORT_A.aggregate == {"exact": 0.75, "normalized_exact": 0.875}
assert [r.prediction for r in REPORT_A.records] == FIXTURE["model_a"]
print(REPORT_A.aggregate)
print(REPORT_A.by_slice)
print('Отчёт с одним вызовом на пример построен')

4. Почему двойной вызов является дефектом измерения

Модель AlternatingModel намеренно меняет ответ при каждом обращении. Старый шаблон — показать результат первого вызова, а метрику посчитать по второму — создаёт отчёт, в котором текст и оценка относятся к разным событиям. Исправленная обвязка доказывает инвариант: число вызовов равно числу примеров.

Python
class AlternatingModel:
    """Return a correct answer on odd calls and a wrong answer on even calls."""

    def __init__(self, correct: str) -> None:
        """Store the correct answer and reset the stateful call counter."""

        self.correct = correct
        self.calls = 0

    def __call__(self, prompt: str) -> str:
        """Alternate between the correct and deliberately wrong response."""

        self.calls += 1
        return self.correct if self.calls % 2 else "__wrong__"


example = DATASET[0]
broken = AlternatingModel(example.expected)
shown_prediction = broken(example.prompt)
score_from_second_call = normalized_exact_match(broken(example.prompt), example.expected)
assert normalize_answer(shown_prediction) == normalize_answer(example.expected)
assert score_from_second_call == 0.0 and broken.calls == 2

fixed = AlternatingModel(example.expected)
fixed_report = evaluate(fixed, [example], {"normalized_exact": normalized_exact_match})
assert fixed.calls == 1
assert fixed_report.records[0].prediction == shown_prediction
assert fixed_report.aggregate["normalized_exact"] == 1.0
print("broken displayed:", shown_prediction, "scored:", score_from_second_call, "calls:", broken.calls)
print("fixed displayed:", fixed_report.records[0].prediction, "score:", fixed_report.aggregate["normalized_exact"], "calls:", fixed.calls)
print('Дефект двойного вызова воспроизведён и устранён')

5. Срезы, доверительные интервалы и парное сравнение

Две демонстрационные системы оцениваются на одних и тех же восьми примерах. Общая точность дополняется результатами по срезам, интервалом Вилсона и таблицей парных исходов. Малый набор намеренно даёт широкую неопределённость: notebook показывает правильную форму отчёта, а не пытается создать убедительность из восьми наблюдений.

Python
def binary_scores(report: EvaluationReport, metric_name: str) -> dict[str, int]:
    """Return one 0/1 score per example ID for a binary metric."""

    if not report.records:
        raise ValueError("paired report must be non-empty")
    result: dict[str, int] = {}
    for record in report.records:
        if record.example_id in result:
            raise ValueError("duplicate report ID")
        value = record.scores[metric_name]
        if value not in {0.0, 1.0}:
            raise ValueError("paired binary comparison requires 0/1 metric values")
        result[record.example_id] = int(value)
    return result


def paired_contingency(first: EvaluationReport, second: EvaluationReport, metric_name: str) -> dict[str, int]:
    """Build the four paired correctness cells for two reports on the same IDs."""

    a = binary_scores(first, metric_name)
    b = binary_scores(second, metric_name)
    if set(a) != set(b):
        raise ValueError("reports must contain the same example IDs")
    metadata = lambda report: {r.example_id: (r.expected, r.slice_name) for r in report.records}
    if metadata(first) != metadata(second):
        raise ValueError("paired reports must use the same targets and slices")
    cells = {"both_correct": 0, "first_only": 0, "second_only": 0, "both_wrong": 0}
    for example_id in sorted(a):
        pair = a[example_id], b[example_id]
        key = {
            (1, 1): "both_correct",
            (1, 0): "first_only",
            (0, 1): "second_only",
            (0, 0): "both_wrong",
        }[pair]
        cells[key] += 1
    return cells


MODEL_B = CountingTableModel([e.prompt for e in DATASET], FIXTURE["model_b"])
REPORT_B = evaluate(MODEL_B, DATASET, METRICS)
assert MODEL_B.calls == len(DATASET)
assert REPORT_B.aggregate == {"exact": 0.625, "normalized_exact": 0.75}

cells = paired_contingency(REPORT_A, REPORT_B, "normalized_exact")
p_value = mcnemar_exact_pvalue(cells["first_only"], cells["second_only"])
assert cells == {"both_correct": 5, "first_only": 2, "second_only": 1, "both_wrong": 0}
assert p_value == 1.0
successes_a = sum(binary_scores(REPORT_A, "normalized_exact").values())
ci_a = wilson_interval(successes_a, len(DATASET))
print("A:", REPORT_A.aggregate, "Wilson normalized_exact:", tuple(round(v, 3) for v in ci_a))
print("B:", REPORT_B.aggregate)
print("paired cells:", cells, "exact McNemar p=", p_value)
print('Срезы и парное сравнение построены')

5.1. Уверенность и право воздержаться

Используем отдельные 20 авторских записей: ответ, эталон и оценка вероятности правильности всего ответа. Это не вероятности токенов и не результаты запуска LLM. Уверенность задана в самих записях; мы не подбираем её по тестовым меткам. Сначала сохраняем JSON и читаем его обратно, затем все проверки работают с теми же ответами.

Сопоставьте среднюю уверенность и точность трёх групп. Затем проследите пороги 0,8, 0,9 и 1,0: сколько ответов осталось и сколько среди них ошибок? Ошибка Брайера усредняет квадрат разности уверенности и бинарного исхода по всем 20 строкам.

Python
"""CPU calibration experiment over authored, saved response records (no LLM)."""
import json
import math
from pathlib import Path
from tempfile import TemporaryDirectory


def validate_records(records):
    if not records:
        raise ValueError("records must be nonempty")
    ids = set()
    for row in records:
        ident = row["id"]
        if not isinstance(ident, str) or not ident or ident in ids:
            raise ValueError("unique nonempty string ids required")
        ids.add(ident)
        if not all(isinstance(row[key], str) for key in ("prediction", "expected")):
            raise ValueError("responses and targets must be strings")
        q = row["confidence"]
        if isinstance(q, bool) or not isinstance(q, (float, int)) or not math.isfinite(q) or not 0 <= q <= 1:
            raise ValueError("confidence must be finite and in [0, 1]")


def correctness(row):
    # Same explicitly declared policy as the lecture's exact_match.
    return int(row["prediction"].strip().casefold() == row["expected"].strip().casefold())


def brier_score(records):
    validate_records(records)
    return sum((row["confidence"] - correctness(row)) ** 2 for row in records) / len(records)


def reliability_groups(records):
    """Group identical authored scores; general data require declared bin edges."""
    validate_records(records)
    result = []
    for q in sorted({row["confidence"] for row in records}):
        group = [row for row in records if row["confidence"] == q]
        result.append(dict(confidence=q, count=len(group),
                           accuracy=sum(map(correctness, group)) / len(group)))
    return result


def selective_report(records, threshold):
    validate_records(records)
    if isinstance(threshold, bool) or not isinstance(threshold, (int, float)) or not math.isfinite(threshold) or not 0 <= threshold <= 1:
        raise ValueError("threshold must be finite and in [0, 1]")
    accepted = [row for row in records if row["confidence"] >= threshold]
    errors = sum(1 - correctness(row) for row in accepted)
    return dict(threshold=threshold, accepted=len(accepted), errors=errors,
                coverage=len(accepted) / len(records),
                risk=errors / len(accepted) if accepted else None)


def authored_records():
    # These counts define a teaching fixture, not sampled model behavior.
    # Confidence is part of each authored response; it is not fitted here.
    records = []
    for q, count, successes in [(0.95, 4, 4), (0.8, 10, 6), (0.6, 6, 3)]:
        for j in range(count):
            i = len(records)
            records.append(dict(id=f"cal-{i:02}", prompt=f"{i} + 1 = ?",
                                prediction=str(i + 1 if j < successes else i + 2),
                                expected=str(i + 1), confidence=q))
    return records


def run_calibration_lab():
    with TemporaryDirectory(dir=Path.cwd()) as folder:
        path = Path(folder) / "calibration_responses.json"
        path.write_text(json.dumps(authored_records()), encoding="utf-8")
        saved = json.loads(path.read_text(encoding="utf-8"))
    # Every score below comes from the same saved records, without new calls.
    score = brier_score(saved)
    groups = reliability_groups(saved)
    curve = [selective_report(saved, t) for t in (0.0, 0.6, 0.8, 0.9, 1.0)]
    assert sum(map(correctness, saved)) == 13
    assert math.isclose(score, 0.2185, abs_tol=1e-14)
    assert groups[1] == dict(confidence=0.8, count=10, accuracy=0.6)
    assert curve[2]["accepted"] == 14 and curve[2]["errors"] == 4
    assert curve[3]["accepted"] == 4 and curve[3]["risk"] == 0
    assert curve[4]["coverage"] == 0 and curve[4]["risk"] is None
    print("Brier:", score, "accuracy:", 13 / 20)
    print("Confidence groups:", groups)
    print("Coverage and selective risk:", curve)
    return saved, groups, curve


if __name__ == "__main__":
    run_calibration_lab()

Получили 13 правильных ответов из 20 и BS=0,2185. При пороге 0,8 остаются 14 ответов с четырьмя ошибками; при 0,9 — четыре без ошибок. Последнее число описывает всего четыре наблюдения, а не гарантию надёжности. Порог 1,0 не оставляет ответов: покрытие равно нулю, риск не определён. Передача остальных запросов человеку тоже имеет цену. Порог и калибратор настраивают на отдельной выборке, а затем проверяют без подгонки к тесту.

6. Модель-судья: отделяем идентичность ответа от позиции

Судья может предпочитать первый или второй ответ независимо от содержания. Поэтому одна и та же пара проверяется в порядках A/B и B/A, а позиционные метки переводятся обратно к идентичности кандидатов. Протокол также считает число вызовов: несколько судей и две перестановки повышают устойчивость, но имеют измеримую цену.

Согласованные ничьи получают tie, противоречие или одна ничья — abstain. Судья по маркеру VERIFIED проверяет только маршрутизацию вердиктов: сам маркер не доказывает истинность ответа.

Python
@dataclass(frozen=True)
class Candidate:
    """Candidate identity and response text for pairwise judging."""

    candidate_id: str
    text: str

    def __post_init__(self) -> None:
        """Reject candidates without a stable identity or response text."""

        if any(not isinstance(v, str) or not v.strip() for v in (self.candidate_id, self.text)):
            raise ValueError("candidate_id and text must be non-empty")


@dataclass(frozen=True)
class SwapDecision:
    """Both order-specific labels, mapped identities, and consensus."""

    first_order_label: str
    swapped_order_label: str
    first_order_winner: str | None
    swapped_order_winner: str | None
    consensus: str | None
    status: str


def map_positional_label(label: str, first: Candidate, second: Candidate) -> str | None:
    """Map first/second/tie back to a stable candidate identity."""

    if label == "first":
        return first.candidate_id
    if label == "second":
        return second.candidate_id
    if label == "tie":
        return None
    raise ValueError("judge label must be 'first', 'second', or 'tie'")


def run_swap_protocol(
    judge_fn: Callable[[str, Candidate, Candidate], str],
    prompt: str,
    candidate_a: Candidate,
    candidate_b: Candidate,
) -> SwapDecision:
    """Judge A/B and B/A exactly once each, then require identity consensus."""

    if candidate_a.candidate_id == candidate_b.candidate_id:
        raise ValueError("candidate IDs must differ")
    label_ab = judge_fn(prompt, candidate_a, candidate_b)
    label_ba = judge_fn(prompt, candidate_b, candidate_a)
    winner_ab = map_positional_label(label_ab, candidate_a, candidate_b)
    winner_ba = map_positional_label(label_ba, candidate_b, candidate_a)
    consensus = winner_ab if winner_ab is not None and winner_ab == winner_ba else None
    status = "winner" if consensus is not None else ("tie" if label_ab == label_ba == "tie" else "abstain")
    return SwapDecision(label_ab, label_ba, winner_ab, winner_ba, consensus, status)


class FirstPositionJudge:
    """A maximally position-biased judge used as a negative control."""

    def __init__(self) -> None:
        """Initialize the visible number of judge invocations."""

        self.calls = 0

    def __call__(self, prompt: str, first: Candidate, second: Candidate) -> str:
        """Always select the first position, ignoring candidate content."""

        self.calls += 1
        return "first"


class ContentJudge:
    """A deterministic judge that selects the candidate containing a marker."""

    def __init__(self, marker: str) -> None:
        """Store the content marker and initialize the call counter."""

        self.marker = marker
        self.calls = 0

    def __call__(self, prompt: str, first: Candidate, second: Candidate) -> str:
        """Select the unique candidate containing the marker, otherwise tie."""

        self.calls += 1
        in_first = self.marker in first.text
        in_second = self.marker in second.text
        if in_first == in_second:
            return "tie"
        return "first" if in_first else "second"


candidate_a = Candidate("A", "answer with VERIFIED fact")
candidate_b = Candidate("B", "unsupported answer")
position_judge = FirstPositionJudge()
position_decision = run_swap_protocol(position_judge, "prompt", candidate_a, candidate_b)
assert position_judge.calls == 2
assert position_decision.first_order_winner == "A"
assert position_decision.swapped_order_winner == "B"
assert position_decision.consensus is None and position_decision.status == "abstain"

content_judge = ContentJudge("VERIFIED")
content_decision = run_swap_protocol(content_judge, "prompt", candidate_a, candidate_b)
assert content_judge.calls == 2 and content_decision.consensus == "A"

J = REFERENCE["judge"]
assert J["forward_a_wins"] == 650 and J["swapped_a_wins"] == 350
assert J["averaged_a_rate"] == 0.5 and J["calls_per_pair"] == 6
print("position control:", position_decision)
print("content consensus:", content_decision)
print("analytic pure-position aggregate: 65% first order -> 50% after two-order averaging")
print('Перестановочный протокол отделяет идентичность от позиции')

7. Частный золотой набор, canary и прозрачная стоимость

Мы рассчитываем размер набора для заданной погрешности доли, создаём детерминированную контрольную строку и оцениваем арифметическую стоимость одного прогона. Совпадение canary с внешним текстом является сигналом возможного раскрытия через некоторый канал; оно само по себе не доказывает, на каком этапе и каким способом произошло раскрытие.

Публичная детерминированная строка не является секретной канарейкой. Вероятность 2^−128 относится к одному угадыванию случайного секретного маркера; вставленное совпадение — положительный контроль детектора. 12,49 с — условный эквивалент 2PT при заданной производительности, не нижняя граница задержки.

Python
def golden_sample_size(margin: float, expected_rate: float = 0.8, z_value: float = 1.96) -> int:
    """Return a normal-approximation planning size for a target half-width."""
    margin = _positive_finite("margin", margin)
    p = _probability("expected_rate", expected_rate)
    z_value = _positive_finite("z_value", z_value)
    if p in {0.0, 1.0}:
        raise ValueError("expected_rate must be strictly between 0 and 1")
    return math.ceil(p * (1.0 - p) * (z_value / margin) ** 2)

def deterministic_demo_canary(namespace: str, hex_characters: int = 32) -> str:
    """Create a reproducible demo canary; use ``secrets.token_hex`` for a real private set."""

    if not isinstance(namespace, str) or not namespace:
        raise ValueError("namespace must be non-empty")
    length = _positive_int("hex_characters", hex_characters)
    if length > 64:
        raise ValueError("this SHA-256 demo supports at most 64 hex characters")
    return hashlib.sha256(namespace.encode("utf-8")).hexdigest()[:length]


def contains_canary(canary: str, texts: Sequence[str]) -> bool:
    """Return whether an exact canary occurs in any supplied text."""

    if not canary:
        raise ValueError("canary must be non-empty")
    if any(not isinstance(text, str) for text in texts):
        raise TypeError("texts must contain strings")
    return any(canary in text for text in texts)


G = REFERENCE["golden"]
assert golden_sample_size(0.03) == G["n_for_3pp"] == 683
assert golden_sample_size(0.05) == G["n_for_5pp"] == 246
canary = deterministic_demo_canary("modern-llms-m19-demo", G["canary_hex_characters"])
assert len(canary) == 32 and all(ch in string.hexdigits.lower() for ch in canary)
assert not contains_canary(canary, [record.prediction for record in REPORT_A.records])
assert abs(16.0**-32 - G["canary_guess_probability"]) < 1e-50
cost = G["cost"]
assert cost["total_tokens"] == 385_000
assert abs(cost["seconds_floor"] - 12.491111111111111) < 1e-12
print(f"planning size: ±3 pp -> {G['n_for_3pp']}; ±5 pp -> {G['n_for_5pp']}")
assert contains_canary(canary, ["planted control " + canary])
print("PUBLIC demo canary:", canary, "random-secret reference probability:", f"{G['canary_guess_probability']:.2e}")
print(f"conditional arithmetic equivalent: {cost['total_tokens']:,} tokens, {cost['dense_flops_floor']:.3e} FLOP, {cost['seconds_floor']:.2f} s")
print('Размер золотого набора, canary и вычислительная оценка проверены')

8. Синтетический рабочий контекст

Номинальная длина окна не равна длине, на которой модель одинаково хорошо использует свидетельство. В учебной U-образной функции точность высока у краёв и падает в середине. Мы вычисляем среднюю точность и долю позиций, превышающих заданный порог. Это иллюстрация определения рабочего контекста, а не измерение конкретной модели.

Доля 0,246752 складывается из двух краевых интервалов, а не образует непрерывное окно. Проектный ноутбук отдельно измеряет реальный поиск строки полным и ограниченным читателями; длина там выражена в строках, не в токенах LLM.

Python
def working_context_share(edge: float, dip: float, threshold: float) -> float:
    """Return the fraction of positions above threshold for a synthetic U profile."""
    edge = _probability("edge", edge)
    threshold = _probability("threshold", threshold)
    dip = _probability("dip", dip)
    if edge - dip < 0.0:
        raise ValueError("edge - dip must remain a valid probability")
    if threshold > edge:
        return 0.0
    if threshold <= edge - dip:
        return 1.0
    return 2.0 * math.asin(math.sqrt((edge - threshold) / dip)) / math.pi

U = REFERENCE["lost_in_middle"]
share = working_context_share(U["edge"], U["dip"], U["threshold"])
assert abs(share - U["working_share"]) < 1e-15
assert U["working_tokens_of_1m"] == round(share * 1_000_000)
assert abs(U["mean"] - (U["edge"] - U["dip"] / 2)) < 1e-15
print(f"synthetic mean={U['mean']:.3f}; positions above {U['threshold']:.2f}: {share:.1%} ({U['working_tokens_of_1m']:,} of 1M)")
print('Синтетический профиль рабочего контекста проверен')

9. Негативные тесты как часть оценки

Проверяем пустой набор, повторяющиеся идентификаторы, неверные границы pass@k, несовместимые парные отчёты, неизвестные метки судьи и невозможные параметры сценариев. Хорошая система оценки должна отказывать явно: тихая перенормировка или пропуск ошибки превращает инфраструктурный дефект в ложный результат модели.

Python
def expect_error(error_type: type[BaseException], fn: Callable[[], object]) -> str:
    """Run a negative test and return its expected error message."""

    try:
        fn()
    except error_type as exc:
        return str(exc)
    except Exception as exc:
        raise AssertionError(f"expected {error_type.__name__}, got {type(exc).__name__}: {exc}") from exc
    raise AssertionError(f"expected {error_type.__name__}")


errors = []
errors.append(expect_error(ValueError, lambda: evaluate(lambda _: "x", [], METRICS)))
duplicate = [DATASET[0], DATASET[0]]
errors.append(expect_error(ValueError, lambda: evaluate(lambda _: "x", duplicate, METRICS)))
errors.append(expect_error(ValueError, lambda: pass_at_k_unbiased(4, 5, 1)))
errors.append(expect_error(ValueError, lambda: pass_at_k_unbiased(4, 1, 5)))
errors.append(expect_error(ValueError, lambda: wilson_interval(9, 8)))
errors.append(expect_error(ValueError, lambda: map_positional_label("left", candidate_a, candidate_b)))

bad_type_report = evaluate(lambda _: 123, [DATASET[0]], METRICS)
assert bad_type_report.error_count == 1 and "TypeError" in bad_type_report.records[0].error
oversized_report = evaluate(lambda _: "x" * 20, [DATASET[0]], METRICS, max_response_chars=10)
assert oversized_report.error_count == 1 and "max_response_chars" in oversized_report.records[0].error
bad_metric_report = evaluate(lambda _: "ok", [DATASET[0]], {"bad": lambda p, e: math.nan})
assert bad_metric_report.error_count == 1 and "finite" in bad_metric_report.records[0].error
assert bad_metric_report.records[0].prediction == "ok"  # metric failure must not erase the model output
assert bad_metric_report.aggregate["bad"] is None
assert normalized_exact_match("-1", "1") == 0.0
assert normalized_exact_match("1.2", "12") == 0.0
assert len(errors) == 6
print("expected errors:", errors)
print('Негативные тесты контрактов пройдены')

Упражнения

  1. Добавьте стратифицированный bootstrap для разности двух парных систем и сравните его с точным тестом Мак-Немара.

  2. Постройте кривую pass@k для нескольких значений c/nc/n и покажите, где наивная оценка отклоняется сильнее всего.

  3. Расширьте отчёт стоимостью и хвостовой задержкой; сформулируйте правило выбора системы при ограничении по качеству и бюджету.

  4. Сравните исходный протокол с условием, в котором названия систем скрыты. В обоих условиях сохраните два порядка ответов и измерьте согласованность судей.

  5. Создайте собственный приватный срез из 30–50 примеров, версионируйте критерии и проверьте, какие ошибки скрывает общая точность.


Ноутбук воспроизводит статистические и инженерные контракты модуля, но не заменяет аудит датасета, реальный benchmark harness и продуктовую оценку рисков.

Демо проектных упражнений модуля 19 — Парное сравнение, критические срезы и проверка действий

Проектная демонстрация

Скачать .ipynb module_19_project_demo.ipynb · 21,0 КБ

Показать текст ноутбука

Демо проектных упражнений модуля 19 — Парное сравнение, критические срезы и проверка действий

Демо-реализация, не готовое решение полномасштабного проекта. На 200 вымышленных запросах два детерминированных правила проверяют политику возврата. Сохраняем ответы в JSON, перечитываем их и строим парный bootstrap без новых вызовов. Во второй части обычные запросы, критические исключения и реальные изменения локального журнала дают разные выводы. Дополнительный опыт действительно ищет строку в документах; ограничение чтения задано программой, поэтому это проверка протокола, а не оценка LLM. Все входы малы, детерминированы и работают без сети на CPU. Цель — проверить интерфейсы, метрики, инварианты и структуру отчёта до расходования GPU/API-бюджета.

Python
from __future__ import annotations
import importlib.util, subprocess, sys
for package in ("numpy", "matplotlib"):
    if importlib.util.find_spec(package) is None:
        subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package])


import math
import random
import time
from collections import Counter, defaultdict

import matplotlib.pyplot as plt
import numpy as np

SEED = 20260801
random.seed(SEED)
np.random.seed(SEED)
print(f"воспроизводимый тестовый запуск; seed={SEED}")
DEMO_LANGUAGE = 'ru'

Перед запуском проследите границу days=30. Правило A допускает её, но не проверяет целость упаковки; B проверяет упаковку, однако исключает последний разрешённый день. На обычном срезе упаковка всегда цела. После сохранения ответов вычисления используют только JSON: повторного обращения к правилам нет.

Python
import math
from dataclasses import dataclass
from typing import Callable, Mapping, Sequence


@dataclass(frozen=True)
class Example:
    example_id: str
    prompt: str
    expected: str
    slice_name: str

    def __post_init__(self):
        if any(not isinstance(v, str) or not v.strip()
               for v in (self.example_id, self.prompt, self.expected, self.slice_name)):
            raise ValueError("example fields must be non-empty strings")


def exact_match(prediction: str, expected: str) -> float:
    return float(prediction.strip().casefold() == expected.strip().casefold())


def evaluate(
    model_fn: Callable[[str], str],
    dataset: Sequence[Example],
    metrics: Mapping[str, Callable[[str, str], float]],
) -> dict:
    dataset = tuple(dataset)
    if not callable(model_fn) or not metrics or any(not isinstance(k, str) or not k or not callable(v) for k, v in metrics.items()):
        raise ValueError("supply a callable model and named callable metrics")
    if not dataset:
        raise ValueError("dataset must be non-empty")
    if any(not isinstance(ex, Example) for ex in dataset):
        raise TypeError("expected Example records")
    if len({ex.example_id for ex in dataset}) != len(dataset):
        raise ValueError("example_id values must be unique")

    records = []
    for ex in dataset:
        prediction = model_fn(ex.prompt)  # the only model call for this item
        if not isinstance(prediction, str):
            raise TypeError("model response must be text")
        scores = {
            name: metric(prediction, ex.expected)
            for name, metric in metrics.items()
        }
        if any(isinstance(v, bool) or not isinstance(v, (float, int)) or not math.isfinite(v) or not 0 <= v <= 1 for v in scores.values()):
            raise ValueError("metric scores must be finite numbers in [0, 1]")
        records.append({
            "id": ex.example_id,
            "slice": ex.slice_name,
            "expected": ex.expected,
            "prediction": prediction,
            "scores": scores,
        })

    aggregate = {
        name: sum(row["scores"][name] for row in records) / len(records)
        for name in metrics
    }
    return {
        "records": records,
        "aggregate": aggregate,
        "model_calls": len(records),
    }

def _positive_int(name: str, value: int) -> int:
    """Validate and return a strictly positive, non-boolean integer."""
    if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
        raise ValueError(f"{name} must be a positive integer")
    return value

def _non_negative_int(name: str, value: int) -> int:
    """Validate and return a non-negative, non-boolean integer."""
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        raise ValueError(f"{name} must be a non-negative integer")
    return value

def _positive_finite(name: str, value: float) -> float:
    """Validate and return a finite positive real number."""
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError(f"{name} must be a real number")
    number = float(value)
    if not math.isfinite(number) or number <= 0.0:
        raise ValueError(f"{name} must be finite and positive")
    return number

def wilson_interval(successes: int, total: int, z_value: float = 1.96) -> tuple[float, float]:
    """Return the Wilson score interval for a binomial proportion."""
    n = _positive_int("total", total)
    s = _non_negative_int("successes", successes)
    if s > n:
        raise ValueError("successes cannot exceed total")
    z_value = _positive_finite("z_value", z_value)
    p = s / n
    z2 = z_value**2
    denominator = 1.0 + z2 / n
    centre = (p + z2 / (2.0 * n)) / denominator
    radius = z_value * math.sqrt(p * (1.0 - p) / n + z2 / (4.0 * n**2)) / denominator
    return max(0.0, centre - radius), min(1.0, centre + radius)

def mcnemar_exact_pvalue(first_only: int, second_only: int) -> float:
    """Return the two-sided exact McNemar p-value for discordant paired outcomes."""
    first_only = _non_negative_int("first_only", first_only)
    second_only = _non_negative_int("second_only", second_only)
    discordant = first_only + second_only
    if discordant == 0:
        return 1.0
    tail = min(first_only, second_only)
    cdf = sum(math.comb(discordant, k) for k in range(tail + 1)) / (1 << discordant)
    return min(1.0, 2.0 * cdf)
import json, tempfile
from pathlib import Path
# Authored fictional policy: sealed items may be returned through day 30 inclusive.
# Unique requests, but generated fixtures are NOT a sample of real customers.
dataset = tuple(Example(
    f"ordinary-{i:03d}", json.dumps({"days": i % 50, "sealed": True}),
    "refund" if i % 50 <= 30 else "deny", "ordinary"
) for i in range(200))
def policy_a(prompt):
    item = json.loads(prompt)
    return "refund" if item["days"] <= 30 else "deny"  # ignores sealed flag
def policy_b(prompt):
    item = json.loads(prompt)
    return "refund" if item["sealed"] and item["days"] < 30 else "deny"  # boundary bug

reports = {name: evaluate(fn, dataset, {"exact": exact_match})
           for name, fn in [("A", policy_a), ("B", policy_b)]}
passport = {"fixture": "fictional-refunds-v1", "systems": ["A-v1", "B-v1"],
            "trial": 0, "metric": "casefold-exact-v1", "seed": 1901}
payload = {"passport": passport, "reports": reports}
with tempfile.TemporaryDirectory(dir=Path.cwd()) as folder:
    file = Path(folder)/"saved_responses.json"
    file.write_text(json.dumps(payload), encoding="utf-8")
    saved = json.loads(file.read_text(encoding="utf-8"))
# All rescoring below uses saved responses, making no new policy calls.
for name in ("A", "B"):
    rows = saved["reports"][name]["records"]
    assert all(exact_match(r["prediction"], r["expected"]) == r["scores"]["exact"] for r in rows)
a = np.array([r["scores"]["exact"] for r in saved["reports"]["A"]["records"]])
b = np.array([r["scores"]["exact"] for r in saved["reports"]["B"]["records"]])
assert len(a) == 200 and a.sum() == 200 and b.sum() == 196
rng = np.random.default_rng(1901)
differences = a - b
bootstrap = differences[rng.integers(0, len(a), size=(5000, len(a)))].mean(axis=1)
interval = np.quantile(bootstrap, [.025, .975])
first_only, second_only = int(((a==1)&(b==0)).sum()), int(((a==0)&(b==1)).sum())
p_value = mcnemar_exact_pvalue(first_only, second_only)
assert (first_only, second_only) == (4, 0) and p_value == .125
print("A/B:", a.mean(), b.mean(), "paired delta:", differences.mean(),
      "bootstrap:", interval, "exact McNemar:", p_value)
print("Calls:", {n: r["model_calls"] for n, r in reports.items()},
      "latency/cost: unmeasured; deterministic rules, not LLMs")
assert np.isclose(interval[0], .005) and np.isclose(interval[1], .04)

A даёт 200/200, B — 196/200. Все четыре различия направлены в одну сторону, поэтому точный тест Мак-Немара даёт p=0,125. Интервал эмпирического bootstrap здесь не включает ноль: малая выборка у границы показывает ограничение этой процедуры, а не доказанную победу A на реальных запросах.

Теперь меняем проверяемое свойство. Критические запросы требуют отказать при вскрытой упаковке, а журнал действий — не выполнить неразрешённый или повторный возврат. Затем опыт с документами проверяет, куда может попасть нужная строка относительно границ чтения. Его результат определяется видимым кодом поиска.

Python
# Predeclared independent gates: ordinary >= .99, critical = 1, trajectory = 1.
# They illustrate a decision rule; they are not a justified production risk policy.
critical = tuple(Example(
    f"critical-{i}", json.dumps({"days": i, "sealed": False}), "deny", "critical"
) for i in range(20))
quality = {name: {
    "ordinary": reports[name]["aggregate"]["exact"],
    "critical": evaluate(fn, critical, {"exact":exact_match})["aggregate"]["exact"]
} for name, fn in [("A",policy_a),("B",policy_b)]}

def run_trajectory(authorized, version):
    """Execute two delivery attempts; inspect effects rather than final words."""
    ledger, seen, events = [], set(), []
    for _ in range(2):
        operation_id = "refund-17"
        if version == "B":
            if not authorized:
                events.append("denied")
                continue
            if operation_id in seen:
                events.append("replay")
                continue
        ledger.append({"operation":operation_id, "amount":7})
        seen.add(operation_id)
        events.append("effect")
    expected = [{"operation":"refund-17", "amount":7}] if authorized else []
    return {"ok":ledger == expected, "ledger":ledger, "events":events}

traces = {name: [run_trajectory(allowed,name) for allowed in [True,False]]
          for name in ("A","B")}
for name in ("A","B"):
    quality[name]["trajectory"] = sum(t["ok"] for t in traces[name])/2
gates = {"ordinary":.99, "critical":1., "trajectory":1.}
decision = {name: all(scores[k] >= gates[k] for k in gates) for name,scores in quality.items()}
assert quality == {"A":{"ordinary":1., "critical":0., "trajectory":0.},
                   "B":{"ordinary":.98, "critical":1., "trajectory":1.}}
assert decision == {"A":False,"B":False}
print("suites:", quality, "release gates:", decision)
print("actual traces:", traces)
# There is no justified universal average over these deliberately different suites.
# Fix B's boundary and rerun on development data; keep a fresh final audit set.

# A public reproducible canary verifies the detector, not privacy or training data.
import hashlib
canary = hashlib.sha256(b"public-m19-positive-control").hexdigest()[:32]
assert canary not in "ordinary answer"
assert canary in ("planted positive control " + canary)
print("canary detector: negative and planted positive controls pass")

# Actual search, with a deliberately limited reader as the negative control.
# Length counts lines, NOT model tokens. No model is involved.
positions = np.linspace(0, 1, 9)
measured = {}
for length in (128, 512, 1024):
    full, edges = [], []
    for fraction in positions:
        pos = round(fraction * (length-1))
        lines = [f"irrelevant line {j}" for j in range(length)]
        needle = "TARGET: blue-orchid"
        lines[pos] = needle
        extract = lambda selected: next((s.split(": ",1)[1] for s in selected if s.startswith("TARGET: ")), "")
        full.append(extract(lines) == "blue-orchid")
        edges.append(extract(lines[:32]+lines[-32:]) == "blue-orchid")
    assert all(full)
    measured[length] = edges
    plt.plot(positions, edges, "o-", label=f"{length} "+("строк" if DEMO_LANGUAGE=="ru" else "lines"))
plt.plot(positions, full, "--", color="black", label="full scan")
plt.xlabel("позиция иголки" if DEMO_LANGUAGE=="ru" else "needle position")
plt.ylabel("успех извлечения (0/1)" if DEMO_LANGUAGE=="ru" else "retrieval success (0/1)")
plt.title("Проверка ограниченного чтения" if DEMO_LANGUAGE=="ru" else "Testing a limited reader")
plt.legend(); plt.tight_layout(); plt.show()
assert measured[128] == [True,True,False,False,False,False,False,True,True]
assert measured[512] == measured[1024] == [True,False,False,False,False,False,False,False,True]

На критическом срезе преимущество меняет сторону, и ни одна версия не проходит все заданные критерии. Проверка состояния раскрывает ошибки, которых не видно из последней реплики. Полный поиск находит строку везде, ограниченный — только у краёв. Это результаты двух программ чтения, не оценка длинного контекста языковой модели.

Переход к полному проекту

Замените правила версионированными системами и соберите независимую выборку реальных задач с разрешённым использованием данных. Для нескольких строк одного сеанса делайте bootstrap по сеансам. Доли на демонстрационных данных и интервал характеризуют только заданную процедуру; повторяющиеся шаблоны не доказывают обобщение на клиентов. На этих данных percentile-bootstrap исключает ноль, а точный тест Мак-Немара даёт p=0,125. Причина расхождения — малая дискретная выборка несовпадений у границы: эмпирический bootstrap никогда не создаёт успех B при ошибке A, поскольку такого исхода не наблюдалось. Это ограничение метода, а не доказанная победа. Сохраните реальные задержки и стоимость, а неизвестные величины обозначьте как отсутствующие. После исправления границы day=30 используйте новый контрольный набор: постоянно просматриваемый регрессионный набор становится частью разработки. Проверка журналов здесь однопроцессная и не моделирует сетевые сбои или транзакции М18.

Минимальный отчёт

  1. Версии данных, модели, кода и seed.

  2. Основная метрика, а также качество, стоимость, задержка и память там, где это применимо.

  3. Базовая линия, одинаковый бюджет и хотя бы один отрицательный контроль.

  4. Неудачные прогоны и границы обоснованных выводов.