ПРАКТИКА · ОЦЕНКА МОДЕЛЕЙ
LLM-судья под проверкой: прямые оценки против попарного сравнения
Автоматический судья способен выбрать первый, более длинный или созданный похожей моделью ответ даже тогда, когда тот не решает задачу лучше. Поэтому недостаточно один раз попросить модель назвать победителя. В этом эксперименте мы сравним прямую оценку по критериям с попарным выбором, сверим обе стратегии с человеком и отдельно поменяем ответы местами.
Почему правдоподобный вердикт ещё не является измерением
LLM-судья — это модель, которой показывают задачу, один или несколько ответов и правила оценки. Такой судья быстрее ручной проверки и умеет работать с открытыми ответами, где простого сравнения строк недостаточно. Но его вердикт остаётся ещё одним сгенерированным ответом, а не объективным свойством данных.
На решение могут влиять признаки, не связанные с качеством:
- позиция: модель чаще выбирает первый или второй показанный вариант;
- длина: подробный ответ выглядит более основательным, хотя содержит те же факты или больше ошибок;
- стиль: уверенный тон, заголовки и списки маскируют пропущенное требование;
- самопредпочтение: судье ближе формулировки, характерные для той же модели или семейства моделей;
- знание происхождения: название модели, цена или репутация становятся подсказкой;
- нестабильность: повторный запуск при тех же данных возвращает другой вердикт.
LLM-судью нужно оценивать тем же способом, которым мы оцениваем любую измерительную систему: по эталону, повторяемости и чувствительности к нерелевантным изменениям.
Один высокий средний балл ничего не доказывает. Нужно знать, совпадает ли решение с человеком, сохраняется ли победитель после перестановки и не вознаграждает ли судья многословие само по себе.
Две стратегии, которые мы сравниваем
Стратегия 1. Прямая оценка
Судья получает один ответ и выставляет ему баллы по заранее описанным критериям. Ответ A и ответ B оцениваются отдельными запросами. В нашем минимальном протоколе используются четыре шкалы от 1 до 5:
| Критерий | Что означает высокий балл | Что не должно повышать балл |
|---|---|---|
| Корректность | Утверждения верны относительно условия и предоставленных данных | Уверенный тон и количество деталей |
| Полнота | Выполнены все существенные требования задачи | Повторение одной и той же мысли |
| Релевантность | Ответ сосредоточен на вопросе и не уходит в сторону | Полезные, но не запрошенные советы |
| Ясность | Результат можно понять и применить без догадок | Оформление, которое скрывает фактическую ошибку |
Преимущество метода — диагностичность. Если один вариант проигрывает, видно, по какому критерию это произошло. Недостаток — сложность калибровки шкалы. Для одного запуска балл 4 может означать «хорошо», а для другого — «почти идеально».
Стратегия 2. Попарное сравнение
Судья одновременно видит два ответа и выбирает A, B или ничью. Относительный выбор обычно проще, чем независимое назначение абсолютных баллов: модель может заметить, что один ответ пропустил ограничение, даже если ей трудно решить, заслуживает ли второй 4 или 5.
Однако именно здесь особенно опасна позиция. Если судья предпочитает первый вариант, обычный одиночный запуск не позволяет отличить реальное превосходство A от эффекта порядка. Поэтому каждую пару нужно оценить дважды:
Запуск AB: первый = ответ A, второй = ответ B
Запуск BA: первый = ответ B, второй = ответ A
После второго запуска мы переводим позиционный вердикт обратно к смысловым меткам A и B. Если в обоих порядках победил один и тот же содержательный ответ, решение устойчиво. Если победитель изменился вместе с позицией, пара помечается как нестабильная.
Зафиксируйте гипотезы до запуска
Создайте небольшой бенчмарк и заранее запишите, какие наблюдения будут считаться приемлемыми. Это защищает от удобной интерпретации метрик после получения результата.
Минимальный протокол проверяет четыре гипотезы:
- Прямая оценка и попарное сравнение могут по-разному согласовываться с человеком.
- Попарный победитель должен сохраняться после перестановки ответов.
- Выбор первой позиции не должен систематически преобладать над выбором второй.
- Разница в длине не должна объяснять решение лучше, чем выполнение критериев.
Не устанавливайте универсальный проходной порог по чужому примеру. Допустимая ошибка зависит от применения. Судья для сортировки черновиков и судья, блокирующий выпуск новой версии, несут разный риск.
До эксперимента сохраните файл protocol.txt:
judge_model: ____________________
judge_version: __________________
run_date: _______________________
temperature_or_sampling: ________
direct_tie_margin: ______________
allowed_labels: A, B, TIE
human_reviewers: _________________
minimum_acceptable_agreement: ____
maximum_position_bias: ___________
policy_for_unstable_pairs: _______
policy_for_parse_errors: _________
Если сервис не позволяет закрепить версию модели, запишите точное имя, дату запуска и все доступные параметры. Воспроизводимость будет ограничена, но изменение хотя бы станет заметным.
Шаг 1. Подготовьте набор задач
Для первого запуска достаточно 30–50 пар. Меньший набор годится для отладки, но отдельная ошибка будет слишком сильно менять итоговую долю. В рабочей проверке нужны примеры разных типов и сложности.
Каждая запись должна содержать:
- стабильный идентификатор;
- исходную задачу;
- все данные, необходимые для ответа;
- ответ A и ответ B;
- человеческое предпочтение A, B или TIE;
- краткое объяснение человеческой метки;
- длины обоих ответов в символах или словах;
- при необходимости — категорию и уровень сложности.
Не показывайте судье названия моделей и происхождение кандидатов. Нейтральные обозначения «Ответ 1» и «Ответ 2» уменьшают риск самопредпочтения и репутационных подсказок. Смысловой идентификатор A или B должен существовать только в управляющем коде.
Конкретный учебный случай
Предположим, оператор просит подготовить короткий ответ пользователю. Это синтетический пример структуры данных, а не результат реального тестирования или факт о клиенте.
{
"case_id": "support-001",
"task": "Пользователь дважды получил уведомление об одной операции. Объясни причину и предложи следующий шаг. Не утверждай, что операция была выполнена дважды.",
"answer_a": "Мы видим два уведомления об одном событии. Это не доказывает повторное выполнение операции. Сначала сверим идентификаторы уведомлений и статус исходной операции.",
"answer_b": "Операция, вероятно, была выполнена дважды из-за технического сбоя. Обратитесь в поддержку, отмените вторую операцию и смените пароль.",
"human_preference": "A",
"human_reason": "A соблюдает ограничение и предлагает проверяемый шаг; B делает неподтверждённый вывод.",
"category": "constraint-following"
}
Здесь человеческая метка является частью учебного условия. В вашем наборе её должен поставить человек, который понимает предметную область, а не автор оцениваемого ответа.
Добавьте диагностические пары
Обычные реальные примеры полезны, но не всегда выявляют причину смещения. Добавьте несколько контролируемых пар:
- эквивалентная длина: ответы сопоставимы по объёму, но один содержит конкретную ошибку;
- раздутая версия: краткий правильный ответ сравнивается с его многословной копией без новой пользы;
- пропущенное ограничение: красивый ответ нарушает одно явно указанное требование;
- почти ничья: оба ответа корректны, различия несущественны;
- разный стиль: содержание одинаково, но один вариант оформлен заголовками и списками;
- скрытое происхождение: один ответ создан той же моделью, что и судья, но эта информация удалена из входа.
Последний тест показывает только связь с происхождением после последующего раскрытия метаданных. Он не доказывает механизм самопредпочтения. Для более сильного вывода понадобятся несколько моделей-генераторов, несколько судей и перемешивание авторства.
Шаг 2. Получите человеческий эталон
Человеческая оценка тоже может быть ошибочной. Поэтому не просите эксперта просто выбрать «что нравится». Дайте ему те же критерии, запретите учитывать предполагаемое авторство и потребуйте короткое основание.
Рекомендуемый порядок:
- Случайно перемешайте A и B для человеческой проверки.
- Скройте модель, стоимость, время генерации и названия систем.
- Попросите выбрать A, B или TIE.
- Попросите назвать решающий критерий.
- Для спорных или рискованных примеров привлеките второго человека.
- Разногласия разрешайте до просмотра решений автоматического судьи.
Не превращайте ничью в случайный выбор. TIE — полноценная метка для ситуаций, где различие несущественно. Если человек обязан всегда выбирать победителя, судья будет обучаться и проверяться на искусственных различиях.
Сохраните эталон отдельно от файлов автоматической оценки:
case_id,human_pref,human_reason
support-001,A,"A соблюдает запрет на неподтверждённый вывод"
case-002,TIE,"Оба ответа корректны и равно применимы"
case-003,B,"B выполняет обязательное требование формата"
Шаг 3. Запустите прямую оценку
Промпт должен заставлять судью опираться на наблюдаемые свойства ответа, а не на общее впечатление. Оценивайте кандидатов отдельными запросами, иначе прямой режим незаметно превратится в попарный.
Системная инструкция:
Ты оцениваешь один ответ на задачу.
Используй только условие задачи, предоставленные данные и рубрику.
Не предполагай, какая модель создала ответ.
Не повышай оценку за длину, уверенный тон или красивое оформление.
Если утверждение нельзя подтвердить из условия, считай его неподтверждённым.
Оцени каждый критерий независимо по шкале от 1 до 5.
Верни только JSON:
{
"correctness": 1,
"completeness": 1,
"relevance": 1,
"clarity": 1,
"critical_error": false,
"reason": "одно короткое проверяемое объяснение"
}
Пользовательская часть запроса:
ЗАДАЧА
{{task}}
ОТВЕТ
{{candidate}}
ШКАЛА
1 — критерий практически не выполнен
2 — есть серьёзные ошибки или пропуски
3 — выполнено частично, нужна существенная правка
4 — выполнено хорошо, есть небольшой недостаток
5 — выполнено полностью, существенных недостатков нет
Выполните один запрос для A и отдельный запрос для B. Используйте одинаковую инструкцию и одинаковые параметры. Не передавайте оценку первого ответа во второй запрос: иначе второй кандидат получит другой контекст.
Как получить итог прямого режима
Для базового сравнения сложите четыре балла. Ответ с большей суммой становится победителем. Равные суммы дают TIE. Если вы хотите считать маленькую разницу ничьей, заранее задайте direct_tie_margin.
direct_total_A = correctness_A + completeness_A
+ relevance_A + clarity_A
direct_total_B = correctness_B + completeness_B
+ relevance_B + clarity_B
если abs(direct_total_A - direct_total_B) <= direct_tie_margin:
direct_preference = TIE
иначе:
direct_preference = ответ с большей суммой
Не меняйте границу ничьей после просмотра ошибок. Сначала зафиксируйте её в протоколе, затем запускайте анализ. Если критерии имеют разный риск, используйте веса, но также объявите их заранее.
Поле critical_error полезно для задач, где одна выдуманная сумма или опасная команда должна перевешивать хороший стиль. Возможное правило: ответ с критической ошибкой не может победить ответ без неё независимо от суммы. Это правило тоже должно быть частью протокола.
Шаг 4. Запустите попарное сравнение в обоих порядках
Для первого запуска покажите A первым, B вторым. Для второго — B первым, A вторым. Создавайте независимые запросы: решение AB не должно попадать в запрос BA.
Системная инструкция:
Ты сравниваешь два ответа на одну задачу.
Выбери ответ, который лучше выполняет условие.
Сначала проверь корректность и обязательные ограничения,
затем полноту, релевантность и ясность.
Не предпочитай ответ за длину, позицию, уверенный тон или оформление.
Не угадывай происхождение ответов.
Если существенной разницы нет, выбери TIE.
Верни только JSON:
{
"winner": "FIRST",
"decisive_criterion": "correctness",
"reason": "одно короткое проверяемое объяснение"
}
Допустимые значения winner: FIRST, SECOND, TIE.
Допустимые decisive_criterion:
correctness, completeness, relevance, clarity, none.
Шаблон запроса AB:
ЗАДАЧА
{{task}}
ПЕРВЫЙ ОТВЕТ
{{answer_a}}
ВТОРОЙ ОТВЕТ
{{answer_b}}
Шаблон запроса BA:
ЗАДАЧА
{{task}}
ПЕРВЫЙ ОТВЕТ
{{answer_b}}
ВТОРОЙ ОТВЕТ
{{answer_a}}
Храните исходный позиционный вердикт. Не записывайте сразу «A» или «B»: для теста смещения важно знать, выбрал судья FIRST или SECOND.
| Запуск | Сырой вердикт | Смысловая метка |
|---|---|---|
| AB | FIRST | A |
| AB | SECOND | B |
| BA | FIRST | B |
| BA | SECOND | A |
| Любой | TIE | TIE |
Шаг 5. Соберите результаты в одну таблицу
Создайте файл results.csv. В числовых полях прямой оценки должны находиться значения от 1 до 5. В pair_ab и pair_ba сохраняются FIRST, SECOND или TIE.
case_id,human_pref,a_correct,a_complete,a_relevant,a_clear,b_correct,b_complete,b_relevant,b_clear,pair_ab,pair_ba,len_a,len_b
support-001,A,,,,,,,,,,,164,179
case-002,TIE,,,,,,,,,,,240,236
case-003,B,,,,,,,,,,,91,312
Пустые ячейки в примере нужно заменить фактическими оценками. Не подставляйте демонстрационные числа ради завершения таблицы. Ошибка разбора должна оставаться отдельным состоянием, а не автоматически превращаться в ноль или TIE.
Рекомендуемая структура каталога:
llm-judge-test/
├── cases.jsonl
├── protocol.txt
├── human_labels.csv
├── prompts/
│ ├── direct.txt
│ └── pairwise.txt
├── raw/
│ ├── direct_a.jsonl
│ ├── direct_b.jsonl
│ ├── pair_ab.jsonl
│ └── pair_ba.jsonl
├── results.csv
└── analyze_judge.py
Сырые ответы нужны для аудита. Итоговая таблица удобна для расчётов, но не позволяет проверить, почему строка не разобралась или какое объяснение дал судья.
Шаг 6. Рассчитайте согласие и позиционное смещение
Следующий скрипт использует только стандартную библиотеку Python. Он:
- проверяет метки и диапазоны баллов;
- получает победителя прямого режима;
- переводит FIRST и SECOND в A и B;
- измеряет точное согласие с человеком;
- считает коэффициент каппа для поправки на случайное совпадение;
- выделяет устойчивые и нестабильные попарные решения;
- показывает частоту выбора первой и второй позиции;
- выводит связь победителя с более длинным ответом.
#!/usr/bin/env python3
import argparse
import csv
from collections import Counter
LABELS = ("A", "B", "TIE")
PAIR_LABELS = ("FIRST", "SECOND", "TIE")
SCORE_FIELDS = (
"a_correct", "a_complete", "a_relevant", "a_clear",
"b_correct", "b_complete", "b_relevant", "b_clear",
)
def validate_row(row, line_number):
human = row["human_pref"].strip().upper()
if human not in LABELS:
raise ValueError(
f"Строка {line_number}: human_pref должен быть A, B или TIE"
)
for field in SCORE_FIELDS:
try:
value = int(row[field])
except (TypeError, ValueError):
raise ValueError(
f"Строка {line_number}: {field} должен быть целым числом"
)
if not 1 <= value <= 5:
raise ValueError(
f"Строка {line_number}: {field} должен быть от 1 до 5"
)
for field in ("pair_ab", "pair_ba"):
value = row[field].strip().upper()
if value not in PAIR_LABELS:
raise ValueError(
f"Строка {line_number}: {field} должен быть "
"FIRST, SECOND или TIE"
)
for field in ("len_a", "len_b"):
try:
value = int(row[field])
except (TypeError, ValueError):
raise ValueError(
f"Строка {line_number}: {field} должен быть целым числом"
)
if value < 0:
raise ValueError(
f"Строка {line_number}: {field} не может быть отрицательным"
)
def direct_preference(row, tie_margin):
total_a = sum(int(row[name]) for name in (
"a_correct", "a_complete", "a_relevant", "a_clear"
))
total_b = sum(int(row[name]) for name in (
"b_correct", "b_complete", "b_relevant", "b_clear"
))
if abs(total_a - total_b) <= tie_margin:
winner = "TIE"
elif total_a > total_b:
winner = "A"
else:
winner = "B"
return winner, total_a, total_b
def semantic_pair_winner(raw_winner, order):
raw_winner = raw_winner.strip().upper()
if raw_winner == "TIE":
return "TIE"
if order == "AB":
return "A" if raw_winner == "FIRST" else "B"
if order == "BA":
return "B" if raw_winner == "FIRST" else "A"
raise ValueError(f"Неизвестный порядок: {order}")
def exact_agreement(reference, predicted):
if not reference:
return None
matches = sum(r == p for r, p in zip(reference, predicted))
return matches / len(reference)
def cohen_kappa(reference, predicted):
if not reference:
return None
n = len(reference)
observed = sum(r == p for r, p in zip(reference, predicted)) / n
ref_counts = Counter(reference)
pred_counts = Counter(predicted)
expected = sum(
(ref_counts[label] / n) * (pred_counts[label] / n)
for label in LABELS
)
if expected == 1:
return 1.0 if observed == 1 else 0.0
return (observed - expected) / (1 - expected)
def confusion(reference, predicted):
matrix = {
actual: {guess: 0 for guess in LABELS}
for actual in LABELS
}
for actual, guess in zip(reference, predicted):
matrix[actual][guess] += 1
return matrix
def print_metric(name, reference, predicted):
agreement = exact_agreement(reference, predicted)
kappa = cohen_kappa(reference, predicted)
print(f"\n{name}")
print(f" примеров: {len(reference)}")
print(f" точное согласие: {agreement:.3f}")
print(f" Cohen kappa: {kappa:.3f}")
matrix = confusion(reference, predicted)
print(" матрица: строки = человек, столбцы = судья")
print(" A B TIE")
for actual in LABELS:
values = " ".join(
f"{matrix[actual][guess]:4d}" for guess in LABELS
)
print(f" {actual:3s} {values}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("csv_file")
parser.add_argument(
"--direct-tie-margin",
type=int,
default=0,
help="Максимальная разница сумм, считающаяся ничьей",
)
args = parser.parse_args()
if args.direct_tie_margin < 0:
raise ValueError("direct-tie-margin не может быть отрицательным")
with open(args.csv_file, encoding="utf-8", newline="") as source:
rows = list(csv.DictReader(source))
if not rows:
raise ValueError("Файл не содержит примеров")
required = {
"case_id", "human_pref", "pair_ab", "pair_ba",
"len_a", "len_b", *SCORE_FIELDS,
}
missing = required - set(rows[0])
if missing:
raise ValueError(
"Отсутствуют столбцы: " + ", ".join(sorted(missing))
)
human = []
direct = []
pair_ab_semantic = []
pair_ba_semantic = []
stable_human = []
stable_pair = []
first_choices = 0
second_choices = 0
positional_ties = 0
stable_count = 0
unstable_count = 0
always_first = 0
always_second = 0
direct_longer_wins = 0
direct_non_ties = 0
pair_longer_wins = 0
pair_stable_non_ties = 0
detail_rows = []
for index, row in enumerate(rows, start=2):
validate_row(row, index)
case_id = row["case_id"].strip()
human_label = row["human_pref"].strip().upper()
direct_label, total_a, total_b = direct_preference(
row, args.direct_tie_margin
)
raw_ab = row["pair_ab"].strip().upper()
raw_ba = row["pair_ba"].strip().upper()
semantic_ab = semantic_pair_winner(raw_ab, "AB")
semantic_ba = semantic_pair_winner(raw_ba, "BA")
human.append(human_label)
direct.append(direct_label)
pair_ab_semantic.append(semantic_ab)
pair_ba_semantic.append(semantic_ba)
for raw in (raw_ab, raw_ba):
if raw == "FIRST":
first_choices += 1
elif raw == "SECOND":
second_choices += 1
else:
positional_ties += 1
if raw_ab == "FIRST" and raw_ba == "FIRST":
always_first += 1
if raw_ab == "SECOND" and raw_ba == "SECOND":
always_second += 1
len_a = int(row["len_a"])
len_b = int(row["len_b"])
longer = "TIE" if len_a == len_b else ("A" if len_a > len_b else "B")
if direct_label != "TIE" and longer != "TIE":
direct_non_ties += 1
if direct_label == longer:
direct_longer_wins += 1
if semantic_ab == semantic_ba:
stable_count += 1
stable_human.append(human_label)
stable_pair.append(semantic_ab)
if semantic_ab != "TIE" and longer != "TIE":
pair_stable_non_ties += 1
if semantic_ab == longer:
pair_longer_wins += 1
pair_consensus = semantic_ab
else:
unstable_count += 1
pair_consensus = "UNSTABLE"
detail_rows.append({
"case_id": case_id,
"human": human_label,
"direct": direct_label,
"direct_total_a": total_a,
"direct_total_b": total_b,
"pair_ab": semantic_ab,
"pair_ba": semantic_ba,
"pair_consensus": pair_consensus,
"longer": longer,
})
print_metric("Прямая оценка против человека", human, direct)
print_metric(
"Попарный запуск AB против человека",
human,
pair_ab_semantic,
)
print_metric(
"Попарный запуск BA против человека",
human,
pair_ba_semantic,
)
if stable_human:
print_metric(
"Устойчивый попарный консенсус против человека",
stable_human,
stable_pair,
)
total_pairs = len(rows)
positional_non_ties = first_choices + second_choices
print("\nПозиционная устойчивость")
print(f" устойчивые пары: {stable_count}/{total_pairs}")
print(f" нестабильные пары: {unstable_count}/{total_pairs}")
print(f" всегда выбрана первая позиция: {always_first}/{total_pairs}")
print(f" всегда выбрана вторая позиция: {always_second}/{total_pairs}")
print(f" позиционных ничьих: {positional_ties}")
if positional_non_ties:
first_rate = first_choices / positional_non_ties
second_rate = second_choices / positional_non_ties
bias_delta = (
first_choices - second_choices
) / positional_non_ties
print(f" доля FIRST без ничьих: {first_rate:.3f}")
print(f" доля SECOND без ничьих: {second_rate:.3f}")
print(f" signed position bias: {bias_delta:.3f}")
print("\nСвязь с длиной")
if direct_non_ties:
print(
" более длинный ответ победил в прямом режиме: "
f"{direct_longer_wins}/{direct_non_ties} "
f"({direct_longer_wins / direct_non_ties:.3f})"
)
else:
print(" для прямого режима недостаточно сравнимых неничьих пар")
if pair_stable_non_ties:
print(
" более длинный ответ победил в устойчивых парах: "
f"{pair_longer_wins}/{pair_stable_non_ties} "
f"({pair_longer_wins / pair_stable_non_ties:.3f})"
)
else:
print(" для попарного режима недостаточно сравнимых неничьих пар")
print("\nПострочная диагностика")
for item in detail_rows:
print(
f" {item['case_id']}: "
f"human={item['human']} "
f"direct={item['direct']} "
f"totals={item['direct_total_a']}/{item['direct_total_b']} "
f"AB={item['pair_ab']} "
f"BA={item['pair_ba']} "
f"pair={item['pair_consensus']} "
f"longer={item['longer']}"
)
if __name__ == "__main__":
main()
Сохраните скрипт как analyze_judge.py и запустите:
python3 analyze_judge.py results.csv --direct-tie-margin 0
Для проверки синтаксиса до заполнения данных:
python3 -m py_compile analyze_judge.py
Если в протоколе была объявлена граница ничьей 1, команда должна использовать именно её:
python3 analyze_judge.py results.csv --direct-tie-margin 1
Как читать полученные метрики
Точное согласие
Это доля случаев, где метка судьи полностью совпала с человеческой меткой:
agreement = число совпавших меток / число проверенных примеров
Метрика понятна, но зависит от состава набора. Если почти везде побеждает A, судья может получить высокое согласие, постоянно выбирая A. Поэтому рядом выводится матрица ошибок и каппа.
Коэффициент каппа
Каппа сравнивает наблюдаемое согласие с тем, которое можно ожидать из распределения меток. Значение 1 означает полное совпадение, 0 — уровень случайного совпадения при данных долях классов, отрицательное значение — согласие хуже такого уровня.
Не превращайте словесные границы каппы в универсальный стандарт качества. Для решения о выпуске важнее конкретные ошибки: например, сколько опасных ответов судья объявил победителями.
Устойчивость к перестановке
Пара устойчива, если AB и BA после обратного преобразования дают одну смысловую метку. Возможны три устойчивых результата: A, B или TIE.
AB выбрал FIRST → A
BA выбрал SECOND → A
Итог: устойчивый победитель A
Пример позиционной нестабильности:
AB выбрал FIRST → A
BA выбрал FIRST → B
Итог: победитель следует за первой позицией
Нестабильную пару нельзя незаметно засчитать как ничью. TIE — содержательный вердикт судьи, а UNSTABLE — провал повторяемости измерения.
Signed position bias
Скрипт считает:
(число FIRST - число SECOND) / (число FIRST + число SECOND)
Ничьи исключаются. Положительное значение указывает на преобладание первой позиции, отрицательное — второй. Значение около нуля само по себе не гарантирует отсутствия смещения: предпочтение первой позиции в одной категории и второй в другой может взаимно сократиться. Поэтому изучайте построчные данные и категории.
Победы более длинного ответа
Доля показывает связь, а не причинность. Более длинный ответ иногда действительно полнее. Чтобы проверить именно смещение, сравните общую выборку с диагностическими парами, где дополнительная длина не добавляет полезной информации.
Как выбрать между прямой и попарной стратегией
Не выбирайте метод только по одной итоговой цифре. Сопоставьте несколько свойств:
| Вопрос | Прямая оценка | Попарное сравнение |
|---|---|---|
| Можно ли увидеть причину низкого качества? | Да, по отдельным критериям | Частично, через решающий критерий |
| Нужна ли абсолютная шкала? | Да | Нет |
| Легко ли ранжировать две версии? | Через разницу сумм | Да, это основной режим |
| Есть ли риск позиционного смещения? | Ниже при независимых запросах | Высокий без перестановки |
| Сколько запросов нужно на пару? | Два независимых запроса | Два запроса с AB и BA |
| Что делать с нестабильностью? | Повторять оценку кандидата | Отмечать расхождение AB/BA |
Практический выбор может выглядеть так:
- используйте прямую оценку, когда нужны пороги качества и объяснение по критериям;
- используйте попарную, когда нужно сравнить две версии ответа или системы;
- используйте обе, когда автоматический судья влияет на выпуск, обучение или публичный вывод;
- отправляйте человеку случаи, где стратегии расходятся, пара нестабильна или обнаружена критическая ошибка.
Комбинация методов полезна не потому, что большинство всегда право. Расхождение является диагностическим сигналом. Например, попарный судья выбирает многословный ответ, а прямые оценки показывают одинаковую корректность и худшую релевантность. Такой случай стоит изучить вручную.
Проверка результата эксперимента
До интерпретации убедитесь, что эксперимент технически завершён.
- Полнота. Для каждого
case_idесть две прямые оценки, AB, BA и человеческая метка. - Изоляция. Прямая оценка A не содержала B, а запрос BA не содержал вердикт AB.
- Одинаковые настройки. Модель, инструкция и параметры не менялись внутри серии.
- Корректная перестановка. В BA поменялись только ответы, а задача и правила остались прежними.
- Слепая подача. Во входе нет названий моделей или других подсказок об авторстве.
- Валидный формат. Все баллы находятся между 1 и 5, а метки входят в объявленный список.
- Сырые данные. Исходные ответы судьи сохранены и связаны с итоговой строкой.
- Предварительный порог. Граница ничьей и правила критических ошибок записаны до анализа.
Затем ответьте на четыре содержательных вопроса:
- Какая стратегия чаще совпадает с человеком на полном наборе?
- Сохраняется ли вывод на сложных примерах и человеческих ничьих?
- Сколько попарных решений меняется после перестановки?
- Какие признаки объединяют ошибки: длина, стиль, категория задачи или происхождение ответа?
Эксперимент считается воспроизводимым, если другой участник может взять сохранённые задачи, инструкции и конфигурацию, повторить запросы и получить тот же набор рассчитываемых метрик — даже если отдельные вердикты недетерминированной модели изменятся.
Что обычно ломает проверку
Ответы оцениваются в разных условиях
A получил полную задачу, а B — сокращённую. Или во второй запрос попала история первого решения. Это уже не сравнение кандидатов. Формируйте запросы программно из одного шаблона.
Рубрика допускает свободное толкование
Фраза «оцени качество от 1 до 10» не задаёт измерение. Судья сам придумает, что считать качеством. Опишите каждый критерий и крайние значения шкалы.
JSON разобран слишком терпимо
Если код молча извлекает первую цифру из текста, число из объяснения может стать баллом. Проверяйте схему, допустимые значения и обязательные поля. Ошибку разбора храните отдельно.
Нестабильность превращается в ничью
Если AB выбирает A, а BA выбирает B, это не подтверждённая равнозначность. Это чувствительность к порядку. Используйте отдельную метку UNSTABLE и маршрут ручной проверки.
Человеческий эталон создан после просмотра судьи
Человек начинает рационализировать автоматический вердикт. Размечайте эталон заранее и скрывайте решения модели до завершения разметки.
В наборе только очевидные победители
Судья выглядит точным, но тест не показывает, умеет ли он различать близкие ответы. Включите ошибки разных типов, ничьи и пары с несущественными стилевыми различиями.
Длина смешана с полнотой
Если все правильные ответы длиннее неправильных, невозможно понять, на что реагирует судья. Нужны контролируемые пары: короткий правильный против длинного, но ошибочного; одинаковое содержание разного объёма; ответы сопоставимой длины.
Один судья оценивает собственные ответы с открытым авторством
Название модели становится прямой подсказкой. Сначала проведите слепой запуск. Если хотите изучить эффект авторства, повторите эксперимент с раскрытым происхождением как отдельное условие и не смешивайте результаты.
Изменение модели происходит посередине серии
Облачный поставщик может обновить модель без изменения короткого имени. Сохраняйте доступный идентификатор версии, время каждого запроса и сырые ответы. Если обновление подтверждено, разделите серии.
Дополнительные тесты после базового запуска
Повторяемость
Повторите каждый запрос три раза с одинаковыми настройками. Посчитайте, в какой доле повторов сохраняются баллы и победитель. Не голосуйте большинством до сохранения исходных разногласий: вариативность сама является результатом проверки.
Слепой тест длины
Разделите пары на группы по отношению длин:
length_ratio = max(len_a, len_b) / max(1, min(len_a, len_b))
примерные исследовательские группы:
1.00–1.20 — близкая длина
1.21–1.75 — заметная разница
больше 1.75 — сильная разница
Границы в примере являются способом группировки, а не нормативом. Зафиксируйте свои границы заранее и сравните поведение судьи между группами.
Контроль оформления
Создайте копию нескольких ответов без заголовков, маркеров и выделения. Содержание должно остаться тем же. Если победитель меняется, судья реагирует на форму сильнее, чем ожидалось.
Проверка авторства
Соберите ответы нескольких генераторов, а затем оцените их несколькими судьями. В управляющей таблице храните generator_family, но не передавайте поле судье. После слепой оценки сравните частоту выбора ответов из собственного и чужого семейства.
Такой анализ требует сбалансированного дизайна: каждое семейство должно встречаться на сопоставимых задачах и позициях. Иначе различие может объясняться качеством генератора или составом задач, а не самопредпочтением.
Детерминированные проверки перед судьёй
Не поручайте модели то, что можно проверить кодом. Валидность JSON, наличие обязательного поля, ограничение длины, совпадение идентификатора и запрещённая команда должны проверяться обычными тестами. LLM-судья полезен для смысловых различий, а не как замена точным правилам.
Ограничения метода
- Человеческая метка не абсолютна. Эксперты могут расходиться, особенно в творческих и неоднозначных задачах.
- Один набор не представляет весь продукт. Результат относится к выбранным задачам, языку, рубрике и версиям моделей.
- Каппа чувствительна к распределению классов. При редких ничьих или доминировании одного победителя её нужно читать вместе с матрицей ошибок.
- Перестановка выявляет позиционную чувствительность, но не объясняет её механизм. Для причинного вывода нужны дополнительные контролируемые условия.
- Связь с длиной не доказывает смещение. Более длинный ответ может быть действительно полнее.
- Объяснение судьи не гарантирует верность причины. Модель способна сначала выбрать вариант, а затем создать правдоподобное обоснование.
- Параметры генерации не всегда обеспечивают детерминизм. Даже при минимальной случайности облачная реализация может меняться.
- Попарное сравнение плохо создаёт глобальный рейтинг без дополнительного дизайна. Возможны циклы: A лучше B, B лучше C, а C лучше A.
Поэтому LLM-судья не должен быть единственной защитой для решений с высокой ценой ошибки. Его разумная роль — масштабировать предварительную оценку, выявлять спорные случаи и направлять человеческое внимание.
Как встроить результат в рабочий процесс
После эксперимента сформулируйте не только вывод «какой метод лучше», но и операционное правило. Например:
1. Точные структурные требования проверяются кодом.
2. Каждый ответ получает прямые баллы по четырём критериям.
3. Две версии сравниваются в порядках AB и BA.
4. Устойчивая пара и прямая оценка согласны:
решение можно использовать автоматически в низкорисковом процессе.
5. Стратегии расходятся или пара нестабильна:
пример отправляется человеку.
6. Обнаружена критическая ошибка:
ответ отклоняется независимо от общего балла.
7. Выборка ошибок пересматривается после изменения модели или рубрики.
Сохраняйте версии задач, инструкций и анализатора рядом. При изменении хотя бы одного компонента запускайте набор заново. Старые и новые результаты нельзя честно сравнивать, если одновременно поменялись судья, рубрика и состав примеров.
Итоговый чек-лист
- Есть минимум 30–50 разнообразных пар для рабочего измерения.
- Человеческие метки созданы до просмотра автоматических решений.
- Авторство ответов скрыто.
- A и B оцениваются независимо в прямом режиме.
- Попарная оценка запущена в порядках AB и BA.
- Сырые FIRST и SECOND сохранены до преобразования.
- Нестабильность не смешивается с TIE.
- Посчитаны точное согласие, каппа и матрица ошибок.
- Отдельно рассчитаны устойчивость перестановки и signed position bias.
- Проверены диагностические пары с разной длиной и оформлением.
- Ошибки разбора не заменены фиктивными баллами.
- Порог допуска и правило ручной проверки зафиксированы заранее.
Надёжный автоматический судья — не тот, который уверенно объясняет свой выбор, а тот, чьи решения совпадают с независимым эталоном и сохраняются после нерелевантных изменений входа.
После выполнения протокола у вас будут не абстрактные впечатления, а два сопоставимых набора оценок, измерение согласия с человеком и отдельное доказательство того, насколько вывод зависит от позиции ответа. Этого достаточно, чтобы выбрать начальную стратегию, определить маршрут ручной проверки и повторять эксперимент после смены модели или рубрики.