Перейти к содержимому

8.1 · Статистика evals: шум и размер выборки

Неделя 8 · Evals · ⏱ ~4 ч · Теория

На неделе 8 мы сравниваем конфигурации ассистента поддержки, и цифры отличаются на несколько процентов. Разница 72% против 75% на 50 вопросах — это 1–2 вопроса, и она легко возникает от одной только случайности модели и судьи. Без базовой статистики система «улучшается» подбрасыванием монетки, а в README попадает выдуманный прогресс. На собеседованиях вопрос «как вы поняли, что улучшение реальное?» — проверка на зрелость.

Скрипт eval/run.py из 6.4 печатает одно число на метрику, например correctness 0,72. Возникает соблазн считать его качеством ассистента. Но correctness 0,72 на 50 вопросах — оценка «настоящего» качества ассистента на всех возможных вопросах клиентов такого рода. Другие 50 вопросов того же вида дали бы, скажем, 0,66 или 0,78. Есть три источника шума:

  1. Выборка вопросов. Какие именно вопросы попали в набор.
  2. Недетерминизм системы. Модель-ответчик с temperature > 0 отвечает по-разному; даже при temperature 0 ответы API не гарантированно идентичны.
  3. Недетерминизм судьи. Тот же ответ в пограничном случае судья оценивает то PASS, то FAIL.

Аналогия из жизни магазина: промокод TEA10 неделю, TEA15 следующую неделю, по 50 заказов с каждым. Средний чек с TEA15 на 3% выше, но объявлять TEA15 лучшим никто не станет: заказов мало, покупатели разные, неделя на неделю не похожа. С evals то же самое.

Доверительный интервал для доли «на пальцах»

Заголовок раздела «Доверительный интервал для доли «на пальцах»»

Если из n вопросов прошли k, доля p = k/n. Её стандартная ошибка:

SE = sqrt(p × (1 − p) / n)

Грубый 95% доверительный интервал: p ± 2·SE (точнее 1,96). Смысл на пальцах: если повторять сбор набора много раз, такие интервалы накроют настоящее значение примерно в 95% случаев. Ширину интервала и разницу между долями измеряем в процентных пунктах (п.п.): 0,72 → 0,75 — это +3 п.п., а не +3%.

n p SE 95% интервал (±)
50 0,72 0,064 ±12 п.п.
100 0,75 0,043 ±8,5 п.п.
200 0,75 0,031 ±6 п.п.
500 0,75 0,019 ±4 п.п.

Вывод, который стоит запомнить: на 50 вопросах интервал шириной около ±12 п.п. Точность растёт как корень из n: чтобы сузить интервал вдвое, нужно вчетверо больше вопросов.

Для маленьких n и долей у краёв (0,95, 0,05) формула p ± 2·SE врёт (может дать интервал за пределами [0, 1]). Лучше интервал Уилсона:

import math
def wilson(k: int, n: int, z: float = 1.96) -> tuple[float, float]:
# Wilson score interval for a binomial proportion
if n == 0:
return (0.0, 1.0)
p = k / n
denom = 1 + z * z / n
center = (p + z * z / (2 * n)) / denom
half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
return (center - half, center + half)
print(wilson(36, 50)) # ~ (0.58, 0.83)

Сравнение двух конфигураций: парный подход

Заголовок раздела «Сравнение двух конфигураций: парный подход»

Интервал описывает одну конфигурацию. В экспериментах нас интересует другое: лучше ли B, чем A. Возьмём две конфигурации на одной dev-части eval/golden.jsonl из 60 вопросов: A прошла 43/60, B — 46/60. Наивно: у A интервал Уилсона [0,59; 0,81], у B [0,65; 0,86], интервалы перекрываются, значит разницы нет. Это слишком строго, потому что A и B гонялись на одних и тех же вопросах. Шум «какие вопросы попали в набор» у них общий, и его можно вычесть.

Парный подход: смотрим только на вопросы, где результаты разошлись.

B: PASS B: FAIL
A: PASS 39 b = 4 ← A лучше на 4 вопросах
A: FAIL c = 7 10 ← B лучше на 7 вопросах

A — 43/60 (71,7%), B — 46/60 (76,7%). Вопросы, где обе прошли или обе провалились, ничего не говорят о разнице. Важны только 11 расхождений: 7 в пользу B, 4 в пользу A. Если бы A и B были одинаковы, расхождения делились бы как броски монетки, 50/50. Насколько 7:4 необычно для монетки?

Тест знаков (точный тест Макнемара): вероятность получить расклад 7:4 или ещё более перекошенный при честной монетке:

from math import comb
def sign_test(b: int, c: int) -> float:
# two-sided exact binomial test on discordant pairs (exact McNemar)
n, k = b + c, min(b, c)
if n == 0:
return 1.0
tail = sum(comb(n, i) for i in range(k + 1)) / 2 ** n
return min(1.0, 2 * tail)
print(sign_test(4, 7)) # ~0.55 -> easily noise
print(sign_test(1, 9)) # ~0.02 -> unlikely to be noise

7:4 — p ≈ 0,55, абсолютно обычный расклад для монетки. +5 п.п. здесь ничего не доказывает. А 9:1 при том же размере набора (это +13 п.п.) — уже p ≈ 0,02.

Порог 0,05 — условность, а не закон природы. Важнее логика: чем меньше расхождений и чем ближе они к 50/50, тем меньше оснований верить в улучшение.

Для метрик, которые не являются простой долей (MRR, стоимость ответа, латентность, разница MRR между A и B), удобен бутстрэп: много раз выбираем вопросы из набора случайно с возвращением, пересчитываем метрику и смотрим, как она гуляет.

import random
def paired_bootstrap(a: list[float], b: list[float], iters: int = 5000, seed: int = 0):
# a[i], b[i] are per-question scores of configs A and B on the same question i
rnd = random.Random(seed)
n = len(a)
diffs = []
for _ in range(iters):
idx = [rnd.randrange(n) for _ in range(n)]
diffs.append(sum(b[i] - a[i] for i in idx) / n)
diffs.sort()
return diffs[int(0.025 * iters)], diffs[int(0.975 * iters)] # 95% interval of B - A
# if the interval contains 0, the difference is not established

Пересэмплируются вопросы, а не отдельные ответы, и одни и те же индексы для A и B: так сохраняется парность.

Интервал выше учитывает только «какие вопросы попали». Шум модели и судьи меряется повторными прогонами одного и того же конфига:

Прогон correctness faithfulness hit@5
1 0,70 0,86 0,82
2 0,74 0,84 0,82
3 0,71 0,88 0,82
разброс 0,70–0,74 0,84–0,88 0

(Цифры иллюстративные.) Важная деталь: hit rate при детерминированном поиске не гуляет вообще, а метрики с судьёй гуляют. Отсюда практическое правило: разница между конфигами меньше разброса повторных прогонов — не разница.

Полезная метрика-диагностика: доля «мигающих» вопросов — тех, что в одном прогоне PASS, в другом FAIL. Если мигает 15% вопросов, ответчик или судья нестабилен на пограничных случаях; их стоит посмотреть глазами. Часто там неоднозначный вопрос («сколько стоит доставка?» без веса и города — то, что ловит eval/ambiguity_check.py) или нечёткий критерий судьи.

Как уменьшить шум:

  • temperature 0 у судьи (и у ответчика на время экспериментов, если продукт это допускает и модель поддерживает параметр);
  • кеш вердиктов судьи по хешу входа: одинаковый ответ — одинаковый вердикт;
  • для решающих сравнений — 3 прогона и среднее по вопросу;
  • исправить неоднозначные вопросы в наборе;
  • больше вопросов.

Есть ещё одно допущение, которое мы до сих пор молча принимали. Все формулы выше предполагают, что вопросы независимы. Если 10 синтетических вопросов из eval/generate.py сгенерированы по одному чанку или касаются одной страницы (скажем, kb/delivery.md с таблицей тарифов), они ведут себя как один «большой» вопрос: падают и проходят вместе. Эффективный размер набора меньше, чем число строк, и интервалы на самом деле шире. Статья Anthropic про статистический подход к evals рекомендует в таких случаях кластеризованные стандартные ошибки; естественный кластер в нашем наборе — страница из source_urls. Достаточно помнить идею: 10 вариаций одного вопроса — не 10 свидетельств, и разнообразие набора важнее числа строк.

До сих пор шум был случайным. Подгонка — систематическая ошибка, которую вносим мы сами. Каждый раз, когда мы смотрим на результат и решаем, что менять дальше, система немного подгоняется под эти конкретные 60 вопросов. После 20 экспериментов на одном наборе:

  • одна из 20 конфигураций окажется «лучше» на несколько п.п. просто по везению (если сделать 20 проверок с порогом 0,05, в среднем одна «выстрелит» случайно);
  • промпт ответчика обрастает правилами под конкретные вопросы («если спрашивают про LAMP-LIGHT-MINI, напомни, что гарантия 6 месяцев»).

Защита:

  1. Holdout из 6.3 (split: holdout): финальную конфигурацию проверяем на нём один раз. Если на holdout выигрыш исчез, он был подгонкой.
  2. Гипотеза до эксперимента, записанная письменно (8.2): «ожидаю +X по hit rate, потому что…». Это не даёт задним числом объявить успехом любую случайную разницу.
  3. Мало экспериментов, каждый осмысленный, а не перебор всех комбинаций.
  4. Правки промпта общие, а не под конкретный вопрос.

Обратная задача: какой набор нужен, чтобы заметить улучшение на d п.п.? Для независимых выборок при p около 0,75 и разнице 5 п.п. нужно больше тысячи вопросов на каждую конфигурацию. Парное сравнение на одном наборе требует заметно меньше (зависит от числа расхождений), но и с ним 60 вопросов надёжно ловят только крупные эффекты (порядка 10+ п.п. или очень однобокие расхождения). Реалистичный вывод для Проекта 3: на нашем наборе видны большие улучшения (сменили чанкинг: +15 п.п. hit rate), а мелкие (+3 п.п. от перефразировки промпта) честно записываются как «в пределах шума».

Всё сказанное сводится к одному правилу, по которому мы будем принимать или отклонять изменения на неделе 8. Правило записываем заранее, до экспериментов. Пример:

Изменение считается улучшением, если выполнены все три условия:

  1. Разница больше разброса трёх повторных прогонов базового конфига.
  2. Парное сравнение: тест знаков p < 0,1 или 95% бутстрэп-интервал разницы не содержит 0.
  3. Ни одна другая ключевая метрика (faithfulness, доля правильных отказов refusal_ok, false_refusals) не ухудшилась больше чем на разброс, а archived_in_context остался нулём.

Для финальной конфигурации — подтверждение на holdout. Если условие 2 не выполнено, но эффект согласуется с гипотезой, пишем «тенденция, не доказано».

Вместо correctness 0.74 пишем:

Эксперимент correctness Δ к baseline расхождения (+/−) p
baseline (3 прогона) 0,72 (0,70–0,74) — — —
headings chunking (3 прогона) 0,84 (0,82–0,85) +12 п.п. 9 / 2 0,07

Колонка в скобках — разброс повторных прогонов; «расхождения» — число вопросов (из 60), где новый конфиг лучше / хуже. Цифры иллюстративные. Даже +12 п.п. на 60 вопросах дают p ≈ 0,07, то есть «скорее настоящее, но не железно».

Статистика от провайдера не зависит: интервал Уилсона, тест знаков и бутстрэп одинаковы. Зависят от провайдера источники шума и то, чем их можно прижать. Сверено в ноябре 2026 по api-docs.deepseek.com и docs.ollama.com.

Повторные прогоны не меняют правило приватности: облачный судья видит только вопросы набора, эталоны и куски публичной kb/. Личные данные клиентов, рабочий код и всё, что нельзя показывать посторонним, в облако не уходят — ни в DeepSeek (серверы в Китае), ни к другим провайдерам; настоящие сообщения клиентов обрабатывает только локальная модель.

Источник шума Claude OpenAI DeepSeek Ollama
Сэмплирование ответа temperature, с ограничениями у части новых моделей temperature; у reasoning-моделей её нет temperature работает только с выключенным thinking; thinking включён по умолчанию temperature + seed
Фиксированный seed нет есть у части моделей, без гарантии в справочнике API не описан есть
Модель под тем же именем меняется снапшоты с датой в id снапшоты с датой в id да: deepseek-flash сейчас обслуживает V4.1-Flash-1017, старые имена перенаправлены на новые модели нет, пока мы сами не сделаем ollama pull того же тега
Нагрузка на сервис влияет на латентность влияет на латентность влияет на латентность; при перегрузе 429 по лимиту одновременных запросов своя машина: соседние процессы, загрузка модели после простоя
Обрезка контекста нет (ошибка при превышении) нет (ошибка при превышении) нет при наших объёмах да, молча, если контекст сервера меньше промпта

Отсюда практические правила:

  1. Судья DeepSeek — с выключенным thinking и temperature 0. Иначе temperature не действует, и «мигающих» вопросов будет больше. Если всё же выбран судья с рассуждениями (по согласию с разметкой в главе 6.4 · Скрипт оценки: retrieval + faithfulness), разброс трёх прогонов обязателен.
  2. В каждый прогон записываем resp.model и дату. Если между прогонами baseline и варианта DeepSeek обновил модель под тем же именем, парное сравнение сравнивает уже две разные модели. Если resp.model в двух прогонах различается — перепрогоняем baseline.
  3. Локальная модель даёт самый воспроизводимый вывод: temperature 0, фиксированный seed, фиксированный тег, достаточный контекст. Бит-в-бит одинаковых ответов это не гарантирует (такого обещания в документации Ollama нет) — проверяем двумя прогонами. Но воспроизводимо не значит верно: по памяти это модель класса 4B при 8 ГБ RAM, 7–8B при 16 ГБ, 14B при 32 ГБ и больше, и даже 14B слабее судьи deepseek-v4-pro, а маленькая слабее и оцениваемой системы. Это свойство полезно для локального ответчика в сравнениях, а судья остаётся deepseek-v4-pro.
  4. Кеш промптов DeepSeek не влияет на ответы, только на цену. А наш кеш вердиктов влияет: при измерении разброса судьи его выключаем.

Три прогона baseline на 60 вопросах при ответе deepseek-flash и судье deepseek-v4-pro без thinking — около $1,8 в часы пик (3 × ≈ $0,60) и вдвое меньше вне пика (оценка из главы 6.4; пик — будни 01:00–04:00 и 06:00–10:00 UTC). На Ollama повторные прогоны бесплатны, но занимают время. Поэтому совет «3 прогона на конфиг» на нашем стеке почти ничего не стоит — экономить на нём не нужно. Для сравнения те же прогоны на сильных моделях Claude или OpenAI считаем по их страницам цен.

Эксперимент: thinking против выключенного thinking у судьи

Заголовок раздела «Эксперимент: thinking против выключенного thinking у судьи»

Хорошее упражнение на эту главу: три прогона одного и того же набора ответов (ответы зафиксированы, меняется только судья) в двух режимах судьи. Сравниваем долю мигающих вердиктов и согласие с нашей разметкой из eval/judge_labels.jsonl. Обычно получается компромисс «точнее, но шумнее и дороже» против «стабильнее и дешевле», и решение принимается по цифрам, а не по ощущению.

  1. Открываем Anthropic: A statistical approach to model evaluations → читаем целиком (≈25 мин). Пять рекомендаций: стандартная ошибка, кластеризация, уменьшение дисперсии через повторные ответы, парные сравнения, анализ мощности. Это ровно план этой главы.
  2. (По желанию, глубже) Статья целиком на arXiv: Adding Error Bars to Evals → разделы про парные различия и мощность (≈40 мин). Формулы можно пропускать, главное — выводы.
  3. Открываем Wikipedia: Binomial proportion confidence interval → разделы про нормальное приближение и интервал Уилсона (≈20 мин). Обращаем внимание, почему нормальное приближение плохо работает при малых n.
  4. Открываем Wikipedia: McNemar’s test → определение и пример (≈15 мин). Цель — понять, почему значение имеют только расходящиеся пары.
  5. В Hamel: AI Evals FAQ ищем вопросы про размер набора и статистическую значимость (≈15 мин): позиция практиков — «смотреть на данные», статистика не заменяет разбор ошибок.
  6. Открываем DeepSeek: Thinking Mode и Create Chat Completion (≈15 мин): какие параметры не действуют в режиме thinking и какие параметры вообще есть (убеждаемся, что seed в справочнике нет).
  7. Открываем Ollama: FAQ и Context length (≈15 мин): контекст по умолчанию, keep_alive, OLLAMA_NUM_PARALLEL. Это источники «шума» локальной модели, которые на самом деле не шум, а настройки.
  • «Улучшение на 2 п.п.» на 50 вопросах — один вопрос. Проценты всегда переводим обратно в число вопросов.
  • Сравнивать интервалы двух конфигов «на глаз» неправильно, когда вопросы общие: нужно парное сравнение.
  • Один прогон на конфиг при недетерминированном судье — лотерея. Хотя бы для baseline нужны три прогона, чтобы узнать разброс.
  • Смена версии набора ломает сравнимость: парное сравнение возможно только на одинаковых вопросах (dataset_sha в config.json прогона должен совпадать).
  • p-value не размер эффекта. p = 0,03 при +1 вопросе на огромном наборе может быть неважным практически, а +15 п.п. при p = 0,12 на 40 вопросах — интересной тенденцией, которую стоит проверить на большем наборе.
  • Срезы малы. Correctness по comparative или по category: warranty на 10 вопросах имеет интервал около ±30 п.п. Выводы по срезам — только как подсказки для разбора ошибок.
  • Поиск лучшего из многих. Лучший из 10 конфигов по dev имеет оптимистичную dev-цифру. Честная цифра — на holdout.
  • «temperature 0» у судьи DeepSeek без отключения thinking — параметр молча игнорируется, разброс судьи больше, чем кажется.
  • Тихая смена модели у DeepSeek. Baseline месячной давности и новый прогон могут быть сделаны разными моделями под одним именем. Сравниваем resp.model в results.jsonl.
  • Обрезка контекста в Ollama выглядит как нестабильность. Если локальный ответчик или судья мигает на длинных вопросах (сравнение «Свет» и «Свет мини», таблица тарифов) и стабилен на коротких, сначала проверяем контекст (ollama ps, prompt_eval_count), потом считаем статистику.

Код: projects/02-support-kb-rag/eval/.

  • Создаём eval/stats.py с функциями wilson(k, n), sign_test(b, c), paired_bootstrap(a, b). Проверяем на примерах из теории: wilson(36, 50) ≈ (0,58; 0,83), sign_test(4, 7) ≈ 0,55, sign_test(1, 9) ≈ 0,02.
  • Прогоняем eval/run.py --config eval/configs/baseline.json на dev 3 раза (кеш судьи на это время выключаем, иначе разброс судьи не виден).
  • Скрипт eval/compare.py RUN_A RUN_B: принимает две папки из eval/runs/, читает их results.jsonl, сопоставляет по id, печатает для каждой метрики: значения, Δ, расхождения b/c, p теста знаков, бутстрэп-интервал разницы.
  • Считаем для трёх прогонов baseline: разброс correctness/faithfulness, долю «мигающих» вопросов. Выписываем мигающие вопросы и смотрим на 5 из них: в чём причина нестабильности (вопрос, эталон, судья)?
  • Прогоняем compare.py на двух прогонах одного и того же baseline. Ожидаемый результат: p большое, интервал содержит 0. Это проверка инструмента: если он «находит» разницу между одинаковыми конфигами, что-то не так.
  • Записываем правило значимости в notes/evals.md и в шапку будущей таблицы экспериментов.
  • Считаем интервал Уилсона для correctness по каждому type и по category. Наглядно видно, насколько широки интервалы на срезах.
  • В results.jsonl каждого прогона сохраняем resp.model ответчика и судьи и время вызова; compare.py предупреждает, если модели или dataset_sha в двух прогонах различаются.
  • Меряем долю мигающих вердиктов для судьи deepseek-v4-pro в двух режимах (thinking выключен / включён) на трёх прогонах одних и тех же ответов. Записываем в notes/evals.md, какой режим стабильнее и какой ближе к нашей разметке.
  • Для локального ответчика (qwen3:4b или крупнее, если позволяет память; temperature 0, seed) делаем два прогона и считаем, сколько ответов различаются. Сравниваем с долей различий у deepseek-flash без thinking.
  • (Опционально, для сравнения) То же для судьи Claude или OpenAI на подвыборке из 20 ответов.
  • В таблице экспериментов у baseline указан разброс трёх прогонов.
  • Есть eval/stats.py и eval/compare.py, проверенные на примерах и на сравнении baseline с самим собой.
  • Правило «какая разница значима» записано до начала экспериментов.

Вопрос 1

Correctness 0,70 на 50 вопросах. Примерно какой ширины 95% интервал?

  • a) ±1 п.п.
  • b) ±4 п.п.
  • c) ±13 п.п.
  • d) ±30 п.п.

c. SE = sqrt(0,7·0,3/50) ≈ 0,065, ±2·SE ≈ ±13 п.п.

Вопрос 2

Во сколько раз нужно увеличить набор, чтобы сузить интервал вдвое?

В 4 раза: ширина интервала пропорциональна 1/√n.

Вопрос 3

Почему при сравнении двух конфигураций на одном наборе важны только вопросы, где результаты разошлись?

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

Вопрос 4

A и B на 60 вопросах: B лучше на 6 вопросах, хуже на 5. Что можно сказать?

6:5 — расклад, обычный для монетки (p ≈ 1); разницы не установлено.

Вопрос 5

Три прогона одного конфига дали correctness 0,70, 0,74, 0,71. Новый конфиг дал 0,73 за один прогон. Улучшение?

Нет: 0,73 внутри разброса baseline (0,70–0,74), это неотличимо от шума.

Вопрос 6

Почему hit rate при детерминированном поиске не меняется между прогонами, а correctness меняется?

Поиск по фиксированному индексу детерминирован, а ответ модели и вердикт судьи содержат случайность.

Вопрос 7

Что не так в этом выводе: «Мы попробовали 15 вариантов промпта, лучший дал 0,81 против 0,74 у baseline на dev-наборе, это +7 п.п., берём в продакшен»?

Выбор лучшего из 15 на том же наборе — подгонка: одна из многих попыток почти наверняка окажется «лучше» по везению. Нет разброса, нет парного теста, нет проверки на holdout.

Вопрос 8

Почему 10 синтетических вопросов, сгенерированных из одного чанка, дают меньше информации, чем 10 вопросов из разных мест?

Они коррелированы: успех или провал зависит от одного куска, поэтому они ведут себя почти как один вопрос; эффективный размер набора меньше.

Вопрос 9

Что делает бутстрэп и почему при сравнении A и B индексы вопросов для них должны быть одинаковыми?

Многократно пересэмплирует вопросы с возвращением и пересчитывает метрику, получая её распределение. Одинаковые индексы сохраняют парность: сравниваются A и B на одних и тех же вопросах, и общий шум вычитается.

Вопрос 10

compare.py показывает p = 0,01 при сравнении двух прогонов одного и того же конфига. Что это значит?

Скорее всего, ошибка в инструменте (не сопоставлены id, перепутаны папки прогонов) или очень нестабильный судья/ответчик, дающий систематически разные результаты; надо разбираться, прежде чем верить любым сравнениям.

Вопрос 11

Зачем записывать правило значимости до экспериментов?

Чтобы задним числом не подгонять критерий под желаемый результат и не объявлять успехом случайную разницу.

Вопрос 12

Три прогона baseline с судьёй deepseek-v4-pro и temperature=0 дали разброс correctness 0,66–0,78. Что проверить первым?

Выключен ли у судьи режим thinking: он включён по умолчанию, и в нём temperature не действует. Затем — нет ли ошибок ERROR, посчитанных как FAIL, и мигающих вопросов с неоднозначными формулировками.

Вопрос 13

Baseline прогнан 2 недели назад, вариант сегодня, у обоих answer_model: deepseek-flash. Почему парное сравнение может быть некорректным и как это обнаружить?

DeepSeek мог за это время переназначить имя на новую версию модели, и сравниваются две разные модели. Обнаружить по resp.model в results.jsonl; при расхождении перепрогнать baseline.

Вопрос 14

Какой ответчик при прочих равных даст самый воспроизводимый результат и почему? Годится ли он поэтому в судьи?

  • a) DeepSeek с thinking
  • b) DeepSeek без thinking, temperature 0
  • c) Ollama, temperature 0, фиксированный seed и тег модели
  • d) все одинаково.

c. У локальной модели фиксированы и тег, и seed, и temperature, и никто не обновит модель без нашего ollama pull; у DeepSeek нет описанного seed, а модель под именем может меняться. Бит-в-бит повторяемость всё равно стоит проверить двумя прогонами. В судьи она не годится: в доступной памяти помещается модель 4–14B, слабее судьи deepseek-v4-pro (а маленькая — и оцениваемой системы), и стабильная ошибка остаётся ошибкой.

Теперь можно ставить первый настоящий эксперимент: 8.2 · Эксперимент: chunking A vs B — гипотеза, два прогона, парное сравнение и строка в таблице экспериментов.