Сравнение методов сортировки в LLM: Bulk, Pairwise, TrueSkill | Гайд 2026 | AiManual
AiManual Logo Ai / Manual.
21 Янв 2026 Гайд

LLM забывают середину: как правильно сортировать списки от Bulk до TrueSkill

Почему LLM портят ранжирование длинных списков? Сравниваем 5 методов: от простого Bulk до алгоритма TrueSkill. Реальная задача с 164 постами.

Проблема: LLM не умеют сортировать длинные списки

Представьте себе задачу: у вас есть 164 поста из вашего блога. Вам нужно отсортировать их от лучшего к худшему, чтобы обновить архив. Вы даете список GPT-4.5 (самая свежая модель на январь 2026 года) и просите: "Отсортируй по качеству".

Модель кивает, думает минуту и выдает результат. Вы проверяете первую десятку - все логично. Последнюю десятку - тоже. А вот с 50-го по 100-й пост... здесь начинается бардак. Хорошие статьи оказываются в середине, посредственные - выше. Почему?

Деградация внимания: LLM плохо работают с длинными последовательностями. Они запоминают начало и конец, но теряют фокус на середине. Это не баг, это особенность архитектуры трансформеров.

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

Я тестировал все на реальных данных - тех самых 164 постах. Каждый метод оценивал по трем критериям:

  • Корреляция с моей субъективной оценкой (я автор, я знаю, какие посты хорошие)
  • Стоимость в токенах (реальные деньги для GPT-4.5 API)
  • Время выполнения (от секунд до часов)
Метод Корреляция Токены Время Когда использовать
Bulk (все сразу) 0.42 ~85K 2 мин Никогда. Серьезно.
Pairwise тупой 0.67 ~1.2M 4 часа Если денег много, а времени еще больше
Pairwise с кэшем 0.68 ~650K 2.5 часа Для 20-30 элементов
TrueSkill базовый 0.81 ~180K 45 мин Для 50+ элементов
TrueSkill адаптивный 0.85 ~150K 30 мин Для продакшена

1 Bulk метод: почему он не работает

Самый очевидный подход - скормить все элементы разом. Промпт типа "Вот 164 поста, отсортируй их от лучшего к худшему". Звучит логично? Только если не знать, как работают LLM.

# КАК НЕ НАДО ДЕЛАТЬ
posts = ["Пост 1", "Пост 2", ..., "Пост 164"]
prompt = f"""Отсортируй эти посты по качеству:
{chr(10).join(posts)}
Верни JSON с ключами 'rank' и 'post_id'."""

response = llm.generate(prompt)  # Ошибка: 85K токенов!

Проблемы Bulk подхода:

  • Деградация внимания: Модель физически не может удержать в фокусе 164 элемента. Она смотрит на первые 20, запоминает последние 10, а про середину догадывается.
  • Порядок влияет на результат: Если перемешать список и отдать заново - сортировка изменится. Модель придает слишком большое значение позиции элемента.
  • Субъективный критерий: "Качество" - расплывчатое понятие. Без четких метрик модель изобретает свои, причем разные для начала и конца списка.
💡
Корреляция 0.42 означает, что модель угадывает хуже, чем монетка. Это не сортировка, это рандом с предубеждением к краям списка.

2 Pairwise сравнение: правильно, но дорого

Идея проста: вместо сортировки всего списка сравниваем элементы попарно. Для 164 постов нужно сравнить каждый с каждым - это 13 366 сравнений. Да, математика жестока.

# Наивная реализация - работает, но медленно
comparisons = []
for i in range(len(posts)):
    for j in range(i+1, len(posts)):
        prompt = f"""Какой пост лучше?
Пост A: {posts[i][:200]}...
Пост B: {posts[j][:200]}...
Ответь 'A' или 'B'."""
        response = llm.generate(prompt)
        comparisons.append((i, j, response))

Плюсы метода:

  • Корреляция 0.67 - уже лучше случайности
  • Модель фокусируется только на двух элементах
  • Результат стабильнее Bulk подхода

Минусы убийственные:

  • 1.2 миллиона токенов по ценам GPT-4.5 API на январь 2026 - около $12 за одну сортировку
  • 4 часа работы даже с батчингом запросов
  • Непоследовательность: если A > B и B > C, то должно быть A > C, но LLM иногда нарушает эту логику

3 Кэширование сравнений: экономия 50%

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

# Умный pairwise с кэшем и транзитивностью
cache = {}
def compare_cached(i, j):
    key = frozenset([i, j])
    if key in cache:
        return cache[key]
    
    # Проверяем транзитивность через уже известные сравнения
    for k in range(len(posts)):
        if (frozenset([i, k]) in cache and 
            frozenset([k, j]) in cache):
            if cache[frozenset([i, k])] == i and \
               cache[frozenset([k, j])] == k:
                cache[key] = i
                return i
    
    # Делаем реальный запрос к LLM
    result = llm_compare(posts[i], posts[j])
    cache[key] = result
    return result

Это снижает количество запросов примерно вдвое. Но все равно остается дорого для больших наборов данных. Для 164 элементов нужно ~650K токенов вместо 1.2M.

Ловушка транзитивности: LLM не всегда следуют логике транзитивности. Если в кэше ошибка (модель сказала A > B, но это было неверно), она распространяется на все производные сравнения. Нужна осторожность.

4 TrueSkill: алгоритм из мира гейминга

TrueSkill - рейтинговая система, которую Microsoft разработала для Xbox Live. Она оценивает силу игроков на основе побед и поражений. Идеально подходит для нашей задачи: каждый пост - "игрок", каждое сравнение - "матч".

Как это работает:

  1. Каждому элементу присваиваем рейтинг (μ) и неопределенность (σ)
  2. При сравнении A и B обновляем рейтинги на основе результата
  3. После многих сравнений рейтинги стабилизируются
  4. Сортируем по μ (чем выше, тем лучше)
import trueskill

# Инициализируем рейтинги
ratings = {i: trueskill.Rating() for i in range(len(posts))}

# Выбираем пары для сравнения умно
for _ in range(500):  # Всего 500 сравнений вместо 13K
    # Выбираем элементы с близкой неопределенностью
    i, j = select_uncertain_pair(ratings)
    
    winner = llm_compare(posts[i], posts[j])
    
    if winner == i:
        ratings[i], ratings[j] = trueskill.rate_1vs1(
            ratings[i], ratings[j]
        )
    else:
        ratings[j], ratings[i] = trueskill.rate_1vs1(
            ratings[j], ratings[i]
        )

# Сортируем по рейтингу
sorted_indices = sorted(
    ratings.keys(), 
    key=lambda x: trueskill.expose(ratings[x]), 
    reverse=True
)

Преимущества TrueSkill:

  • Корреляция 0.81 - значительно лучше pairwise
  • 180K токенов - в 7 раз дешевле наивного pairwise
  • 45 минут - вместо 4 часов
  • Учитывает неопределенность: элементы с плохо определенным рейтингом сравниваются чаще

5 Адаптивный TrueSkill: следующий уровень

Базовый TrueSkill хорош, но можно лучше. Адаптивная версия:

def adaptive_trueskill_sort(posts, target_correlation=0.85):
    ratings = {i: trueskill.Rating() for i in range(len(posts))}
    comparisons_made = 0
    
    while True:
        # Вычисляем текущую корреляцию с контрольной выборкой
        current_corr = compute_correlation(ratings, ground_truth)
        
        if current_corr >= target_correlation:
            break  # Достигли целевой точности
            
        # Адаптивно выбираем следующую пару
        if comparisons_made < 100:
            # Первые 100 сравнений: случайные пары
            i, j = random_pair()
        elif comparisons_made < 300:
            # Следующие 200: пары с максимальной неопределенностью
            i, j = most_uncertain_pair(ratings)
        else:
            # Дальше: пары вокруг границ рейтинга
            i, j = boundary_pair(ratings)
        
        # Сравниваем и обновляем
        winner = llm_compare(posts[i], posts[j])
        # ... обновление рейтингов
        comparisons_made += 1
    
    return sorted_by_rating(ratings)

Адаптивный алгоритм останавливается, когда достигает заданной корреляции. Для моих 164 постов хватило 350 сравнений вместо 500. Экономия еще 30%.

Практические советы: что делать с вашими данными

Итак, вы хотите отсортировать свой список. Какой метод выбрать?

Размер списка Метод Почему
1-10 элементов Bulk (да, здесь можно) Модель справится, деградация внимания не критична
11-30 элементов Pairwise с кэшем Проще реализовать, TrueSkill - overkill
31-100 элементов TrueSkill базовый Оптимальное соотношение цена/качество
100+ элементов TrueSkill адаптивный Единственный разумный выбор

Ошибка №1: нечеткий критерий сортировки

"Отсортируй по качеству" - это плохой промпт. Модель сама решает, что такое качество. У меня был случай, когда GPT-4.5 решил, что "качество" = "длина текста". Длинные посты оказались вверху, короткие - внизу.

# ПЛОХО
prompt = "Какой пост лучше? Пост A: {...} Пост B: {...}"

# ХОРОШО
criteria = """
Оцени посты по этим критериям:
1. Практическая полезность (0-10)
2. Глубина проработки темы (0-10)
3. Уникальность информации (0-10)
4. Качество примеров кода (0-10)

Суммируй баллы и выбери победителя.
"""

Ошибка №2: игнорирование лимитов контекста

Даже с TrueSkill нужно следить за длиной контекста. Современные модели типа Claude 3.5 Opus имеют окно в 200K токенов, но это не значит, что можно запихнуть туда все. Длинные контексты дороги и медленны.

💡
Используйте суммаризацию для длинных текстов. Вместо полного поста отправляйте: заголовок + первые 200 слов + последние 100 слов + ключевые тезисы. Точность почти не страдает, а токены экономятся.

Ошибка №3: отсутствие валидации

Вы отсортировали список. И что дальше? Как проверить, что сортировка хорошая? Нужна контрольная выборка.

Мой метод:

  1. Вручную оцениваю 20 случайных постов (от 1 до 10 баллов)
  2. Сравниваю свою оценку с оценкой алгоритма
  3. Вычисляю корреляцию Пирсона
  4. Если меньше 0.8 - увеличиваю количество сравнений в TrueSkill

Интеграция в продакшен

TrueSkill отлично работает в асинхронных очередях. Вот как выглядит pipeline:

async def sort_pipeline(posts):
    """Асинхронная сортировка с TrueSkill"""
    # 1. Инициализация
    ratings = initialize_ratings(posts)
    
    # 2. Планирование сравнений
    comparison_queue = create_comparison_queue(ratings)
    
    # 3. Параллельные запросы к LLM
    async with aiohttp.ClientSession() as session:
        tasks = []
        for i, j in comparison_queue[:100]:  # Первые 100 пар
            task = compare_async(session, posts[i], posts[j])
            tasks.append(task)
        
        results = await asyncio.gather(*tasks)
    
    # 4. Обновление рейтингов
    for (i, j), winner in zip(comparison_queue[:100], results):
        update_trueskill_ratings(ratings, i, j, winner)
    
    # 5. Повторяем, пока не достигнем целевой точности
    return get_sorted_list(ratings)

Для масштабирования на тысячи элементов добавьте:

  • Кэширование результатов сравнений в Redis
  • Балансировку нагрузки между несколькими инстансами LLM
  • Мониторинг качества через контрольные точки

Что будет дальше? Прогноз на 2027

Методы сортировки эволюционируют. Вот что я ожидаю:

  1. Нативные ранжировщики в LLM: Модели типа GPT-5 (когда выйдет) будут иметь встроенную функцию "sort_by_quality". Пока что ее нет.
  2. Специализированные модели для ранжирования: Как Top-K оптимизаторы, но для сортировки.
  3. Гибридные подходы: TrueSkill + эмбеддинги + few-shot обучение.
  4. Автоматический подбор критериев: Модель сама определяет, по каким метрикам сортировать ваши данные.

Пока этого не случилось, TrueSkill остается лучшим выбором. Он балансирует между точностью, стоимостью и скоростью. Bulk метод - для наивных, pairwise - для перфекционистов с большим бюджетом, TrueSkill - для инженеров, которые считают деньги.

Главный вывод: Не доверяйте LLM сортировать длинные списки целиком. Разбивайте задачу на попарные сравнения. Используйте алгоритмы типа TrueSkill для эффективного планирования этих сравнений. И всегда проверяйте результат на контрольной выборке.

Мои 164 поста теперь отсортированы. Верхние 20 - действительно лучшие. Нижние 20 - те, что стоит переписать. А в середине... там теперь порядок, а не хаос. TrueSkill сэкономил мне $10 и 3.5 часа времени. Для бизнеса это значит, что можно сортировать каталоги товаров, ранжировать поисковую выдачу, отбирать лучших кандидатов - и все это с предсказуемым качеством и бюджетом.

Следующий шаг - применить тот же подход к ревью кода. Представьте: 100 pull requests нужно отсортировать по качеству изменений. TrueSkill справится. Но это уже другая история.