0Pricing
AI Prompt Engineering · Урок

Сравнительная оценка: A и B

Запросы для попарного сравнения, позволяющие ранжировать результаты без абсолютной оценки.

«Сравнительная оценка: A и B» — бесплатный урок AI Prompt Engineering на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Prompt Engineering, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Prompt Engineering содержит 4 уроков всего.

Что такое попарное оценивание (A против B)?

Попарное оценивание (также называемое сравнительным оцениванием) предоставляет судье два ответа на один и тот же вопрос и спрашивает, какой из них лучше. Вместо оценки одного ответа по шкале от 1 до 5 судья выносит относительное решение: A лучше, B лучше или это ничья.

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

Базовый запрос для попарного оценивания

Самый простой запрос для попарного судьи спрашивает, какой ответ лучше и почему. Важно заставить судью сделать выбор — не позволяйте ему уклоняться от сравнения с помощью расплывчатого ответа «оба хороши».

import anthropic
import json

client = anthropic.Anthropic(api_key='sk-ant-...')

PAIRWISE_PROMPT = (
    'Given the same question, compare these two responses and decide which is better.\n\n'
    'Question: {question}\n\n'
    'Response A:\n{response_a}\n\n'
    'Response B:\n{response_b}\n\n'
    'Which response is better? You must pick A, B, or TIE (use TIE only if '
    'they are truly equal in all meaningful ways).\n\n'
    'Return JSON: {{"winner": "A" or "B" or "TIE", '
    '"reason": "<one sentence explaining why the winner is better>"}}'
)

def pairwise_judge(question, response_a, response_b):
    prompt = PAIRWISE_PROMPT.format(
        question=question,
        response_a=response_a,
        response_b=response_b
    )
    r = client.messages.create(
        model='claude-opus-4-5',
        max_tokens=150,
        messages=[{'role': 'user', 'content': prompt}]
    )
    return json.loads(r.content[0].text)

result = pairwise_judge(
    'What is machine learning?',
    'Machine learning is a subset of AI.',
    'Machine learning is a method of data analysis that automates model building.'
)
print(result)

Случайное изменение порядка для уменьшения позиционного смещения

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

import anthropic
import json
import random

client = anthropic.Anthropic(api_key='sk-ant-...')

def debiased_pairwise_judge(question, response_a, response_b):
    def single_comparison(first, second, first_label, second_label):
        prompt = (
            f'Question: {question}\n\n'
            f'Response {first_label}:\n{first}\n\n'
            f'Response {second_label}:\n{second}\n\n'
            f'Which is better? Reply with {first_label}, {second_label}, or TIE.'
        )
        r = client.messages.create(
            model='claude-opus-4-5',
            max_tokens=10,
            messages=[{'role': 'user', 'content': prompt}]
        )
        return r.content[0].text.strip()

    # Run A-first
    result_ab = single_comparison(response_a, response_b, 'A', 'B')
    # Run B-first
    result_ba = single_comparison(response_b, response_a, 'B', 'A')

    if result_ab == 'A' and result_ba == 'A':
        return 'A', 'Consistent: A wins in both orderings'
    elif result_ab == 'B' and result_ba == 'B':
        return 'B', 'Consistent: B wins in both orderings'
    else:
        return 'TIE', f'Inconsistent: {result_ab} then {result_ba} — position bias detected'

winner, reason = debiased_pairwise_judge(
    'Explain a hash table.',
    'A hash table maps keys to values.',
    'A hash table is a data structure using a hash function to store key-value pairs for O(1) lookup.'
)
print(f'Winner: {winner} — {reason}')

Попарное оценивание по нескольким критериям

Просите судью сравнивать ответы по конкретным измерениям, а не просто спрашивать, «какой из них в целом лучше». Попарное сравнение по отдельным измерениям даёт полезную информацию о том, почему предпочтение отдано одному ответу.

import anthropic
import json

client = anthropic.Anthropic(api_key='sk-ant-...')

DIMENSIONAL_PAIRWISE = (
    'Compare Response A and Response B on each dimension.\n\n'
    'Question: {question}\n\n'
    'Response A: {response_a}\n\n'
    'Response B: {response_b}\n\n'
    'For each dimension, say A, B, or TIE:\n'
    '1. ACCURACY: Which is more factually correct?\n'
    '2. COMPLETENESS: Which answers the question more fully?\n'
    '3. CLARITY: Which is easier to understand?\n'
    '4. CONCISENESS: Which avoids unnecessary length?\n\n'
    'Return JSON: {{"accuracy":"A/B/TIE", "completeness":"A/B/TIE", '
    '"clarity":"A/B/TIE", "conciseness":"A/B/TIE", "overall":"A/B/TIE"}}'
)

def dimensional_compare(question, a, b):
    r = client.messages.create(
        model='claude-opus-4-5',
        max_tokens=200,
        messages=[{'role': 'user', 'content': DIMENSIONAL_PAIRWISE.format(
            question=question, response_a=a, response_b=b
        )}]
    )
    return json.loads(r.content[0].text)

result = dimensional_compare(
    'How does HTTPS work?',
    'HTTPS encrypts web traffic using SSL/TLS.',
    'HTTPS secures HTTP using TLS. The browser and server perform a handshake, exchange certificates, and establish an encrypted channel for all data transfer.'
)
print(result)

Построение турнира: круговая система

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

from itertools import combinations
from collections import defaultdict

def round_robin_tournament(question, responses):
    """
    responses: list of (label, text) tuples
    Returns ranking by win count.
    """
    wins = defaultdict(int)
    ties = defaultdict(int)

    # Every pair compared once
    for (label_a, text_a), (label_b, text_b) in combinations(responses, 2):
        winner, reason = debiased_pairwise_judge(question, text_a, text_b)

        if winner == 'A':
            wins[label_a] += 1
        elif winner == 'B':
            wins[label_b] += 1
        else:  # TIE
            ties[label_a] += 1
            ties[label_b] += 1

        print(f'{label_a} vs {label_b}: {winner}')

    # Rank by wins, then ties
    ranking = sorted(
        responses,
        key=lambda x: (wins[x[0]], ties[x[0]]),
        reverse=True
    )
    print('\nFinal ranking:')
    for i, (label, _) in enumerate(ranking, 1):
        print(f'{i}. {label}: {wins[label]} wins, {ties[label]} ties')
    return ranking

Рейтинг Эло для непрерывного ранжирования

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

import math

def elo_update(rating_a, rating_b, winner, k=32):
    """
    Update Elo ratings after a match.
    winner: 'A' (A won), 'B' (B won), 'TIE' (draw)
    Returns (new_rating_a, new_rating_b)
    """
    expected_a = 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
    expected_b = 1 - expected_a

    if winner == 'A':
        score_a, score_b = 1, 0
    elif winner == 'B':
        score_a, score_b = 0, 1
    else:  # TIE
        score_a, score_b = 0.5, 0.5

    new_a = rating_a + k * (score_a - expected_a)
    new_b = rating_b + k * (score_b - expected_b)
    return new_a, new_b

# Simulate ratings for 4 model variants
ratings = {'ModelA': 1000, 'ModelB': 1000, 'ModelC': 1000, 'ModelD': 1000}

# After running many pairwise comparisons:
ratings['ModelA'], ratings['ModelB'] = elo_update(ratings['ModelA'], ratings['ModelB'], 'A')
ratings['ModelC'], ratings['ModelD'] = elo_update(ratings['ModelC'], ratings['ModelD'], 'TIE')

ranking = sorted(ratings.items(), key=lambda x: x[1], reverse=True)
for model, score in ranking:
    print(f'{model}: {score:.0f}')

Когда использовать попарное и абсолютное оценивание

Используйте попарное оценивание, когда:

  • Вы хотите ранжировать несколько моделей или вариантов запросов
  • Трудно определить пороги абсолютных оценок
  • Вы сравниваете результаты, которые оба «хороши», но хороши по-разному

Используйте абсолютное оценивание (по системе критериев), когда:

  • Вам нужен порог «пройдено/не пройдено» («достаточно ли хорош этот ответ?»)
  • Для каждого запроса у вас есть только один ответ для оценивания
  • Вам нужны оценки по отдельным критериям для отладки

Масштабное попарное оценивание на основе выборки

Полный перебор всех пар для N ответов требует N*(N-1)/2 сравнений — O(N^2). При большом N используйте случайную выборку: сравнивайте каждый ответ с K случайно выбранными соперниками, а не со всеми остальными. Это позволяет приблизительно восстановить настоящий рейтинг с гораздо меньшими затратами.

import random
from collections import defaultdict

def sampled_tournament(question, responses, k_opponents=5):
    """
    Compare each response against k random opponents.
    More efficient than full round-robin for large N.
    """
    wins = defaultdict(int)
    n = len(responses)

    for i, (label, text) in enumerate(responses):
        # Sample k random opponents (not self)
        opponent_indices = random.sample(
            [j for j in range(n) if j != i],
            min(k_opponents, n - 1)
        )
        for j in opponent_indices:
            opp_label, opp_text = responses[j]
            winner, _ = debiased_pairwise_judge(question, text, opp_text)
            if winner == 'A':
                wins[label] += 1
            elif winner == 'B':
                wins[opp_label] += 1

    ranking = sorted(responses, key=lambda x: wins[x[0]], reverse=True)
    for i, (label, _) in enumerate(ranking, 1):
        print(f'{i}. {label}: {wins[label]} wins')
    return ranking

Интерпретация расхождений

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

import anthropic
import json

client = anthropic.Anthropic(api_key='sk-ant-...')

def deep_analyze_tie(question, response_a, response_b):
    """When A vs B is a genuine tie, ask the judge to explain strengths of each."""
    analysis_prompt = (
        f'These two responses to the same question are closely matched in quality.\n\n'
        f'Question: {question}\n\n'
        f'Response A: {response_a}\n\n'
        f'Response B: {response_b}\n\n'
        f'Analyze both:\n'
        f'1. What does A do better than B?\n'
        f'2. What does B do better than A?\n'
        f'3. For what audience or context would you prefer A?\n'
        f'4. For what audience or context would you prefer B?'
    )
    r = client.messages.create(
        model='claude-opus-4-5',
        max_tokens=400,
        messages=[{'role': 'user', 'content': analysis_prompt}]
    )
    return r.content[0].text

analysis = deep_analyze_tie(
    'Explain async/await in Python.',
    'Async/await allows non-blocking code execution.',
    'Async/await lets you write asynchronous code that looks synchronous, '
    'using the asyncio event loop to handle I/O without blocking.'
)
print(analysis[:300])

Запись результатов попарного оценивания

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

import json
import datetime

def logged_pairwise(question, response_a, response_b,
                    label_a='A', label_b='B', log_file='pairwise_log.jsonl'):
    winner, reason = debiased_pairwise_judge(question, response_a, response_b)

    entry = {
        'timestamp': datetime.datetime.utcnow().isoformat(),
        'question': question[:100],  # Truncate for storage
        'label_a': label_a,
        'label_b': label_b,
        'winner': winner,
        'reason': reason,
        'response_a_length': len(response_a.split()),
        'response_b_length': len(response_b.split()),
    }

    with open(log_file, 'a') as f:
        f.write(json.dumps(entry) + '\n')

    print(f'{label_a} vs {label_b}: {winner} — {reason}')
    return winner

logged_pairwise(
    'What is SQL?',
    'SQL is a database query language.',
    'SQL (Structured Query Language) is used to query and manage relational databases.',
    label_a='ModelV1',
    label_b='ModelV2'
)

Попарное оценивание и абсолютное оценивание: компромиссы

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

Главный компромисс заключается в следующем: для N ответов попарное оценивание требует O(N^2) сравнений, а абсолютное — O(N). При большом масштабе становятся необходимыми попарное оценивание на основе выборки или рейтинги Эло.

Проверка знаний: устранение смещения при попарном оценивании

Как эффективнее всего уменьшить позиционное смещение при попарном оценивании с помощью LLM?

Повторение: сравнительное оценивание A против B

Попарное оценивание определяет, какой из двух ответов лучше, вместо того чтобы оценивать каждый по абсолютной шкале. Чтобы контролировать позиционное смещение, выполняйте каждое сравнение дважды, меняя порядок, и принимайте только согласующиеся результаты. Для N ответов используйте круговой турнир (все пары) при малом N или турниры на основе выборки при большом N. Рейтинги Эло формируют непрерывное ранжирование по множеству попарных сравнений. Используйте попарное оценивание по отдельным критериям (точность, полнота и ясность оцениваются независимо), чтобы получать полезную диагностическую информацию. Записывайте все результаты для регрессионных проверок и анализа тенденций в разных версиях моделей.

Часто задаваемые вопросы

Урок «Сравнительная оценка: A и B» бесплатный?

Да — полный текст урока «Сравнительная оценка: A и B» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Prompt Engineering, подпишись на CoddyKit PRO. Курс AI Prompt Engineering содержит 4 уроков всего.

Чему я научусь в уроке «Сравнительная оценка: A и B»?

Запросы для попарного сравнения, позволяющие ранжировать результаты без абсолютной оценки. Ты практикуешь AI Prompt Engineering с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Prompt Engineering?

Предыдущий опыт не требуется. AI Prompt Engineering на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Сравнительная оценка: A и B»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Prompt Engineering?

Да. Каждый урок AI Prompt Engineering включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Оценка результатов LLM с помощью LLM
  2. Запросы для оценки по критериям
  3. Сравнительная оценка: A и B
  4. Калибровка и предвзятость судей на основе LLM
← Назад к AI Prompt Engineering