- BrainTools - https://www.braintools.ru -

Как заставить LLM сортировать данные: от наивного подхода до TrueSkill

Если вы когда-нибудь грузили в LLM список и просили выбрать лучшее или отсортировать — вы, скорее всего, получали посредственный результат. Я проверил это на 164 постах своего телеграм-канала, сравнив пять разных методов сортировки. Оказалось, что разница между «дёшево и плохо» и «дёшево и хорошо» — в правильном алгоритме, а не в модели.

В этой статье разберём, почему наивные подходы не работают, как алгоритм из Xbox Live помогает ранжировать контент, и какой метод даёт лучшую корреляцию с реальными данными.

Постановка задачи

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

Первый порыв — скормить весь список в LLM и попросить отсортировать. Но тут возникают проблемы:

  • Ограничение контекста — в окно влезает не всё

  • Деградация внимания [1] — модель внимательнее читает начало списка

  • Нестабильность — один и тот же запрос может дать разные результаты

  • Лимиты вывода — получить обратно 500 отсортированных ID непросто

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

Пять методов сортировки

Метод 1: Bulk (всё в один запрос)

Самый простой подход — отправить все элементы в одном запросе и попросить вернуть отсортированный список ID.

prompt = """Отсортируй эти посты по интересности для разработчика.
Верни только список ID постов в порядке от САМОГО интересного к наименее интересному.

Посты:
[ID:123] Текст поста 1...
[ID:456] Текст поста 2...

Плюсы: один API-вызов, минимальная стоимость.

Минусы: позиционное предпочтение (positional bias) — LLM внимательнее читает начало списка. Элементы из начала получают преимущество независимо от содержания.

Метод 2: Score (независимая оценка)

Каждый элемент оценивается отдельно по шкале от 1 до 100. Затем сортируем по оценкам.

prompt = """Оцени этот пост по шкале от 1 до 100: насколько он интересен для разработчика.

Учитывай:
- Техническую глубину и полезность
- Практическую применимость
- Новизну информации

Пост:
{post_text}"""

Плюсы: нет positional bias, получаем объяснение для каждого элемента.

Минусы: O(n) вызовов API, абсолютные оценки нестабильны — «75 баллов» в разных запросах может означать разное.

Метод 3: Score + Reasoning (сначала анализ, потом оценка)

Модификация Score: заставляем модель сначала выписать плюсы и минусы, а потом ставить оценку.

class ScoreReasonedResult(BaseModel):
    pros: str   # Сначала анализируем плюсы
    cons: str   # Потом минусы
    score: int  # И только потом оценка

Гипотеза была в том, что принудительный «reasoning» улучшит качество оценок. Спойлер: не особо.

Метод 4: TrueSkill Batch

Здесь начинается интересное. Вместо абсолютных оценок используем относительные сравнения.

TrueSkill — алгоритм, разработанный Microsoft для матчмейкинга в Xbox Live. Он позволяет оценить «силу» игрока, даже если тот никогда не играл против конкретного соперника — достаточно истории матчей.

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

  1. Каждому элементу присваиваем начальный рейтинг (μ=25, σ=8.33)

  2. Формируем случайные батчи по 10 элементов

  3. Просим LLM отсортировать батч

  4. Обновляем рейтинги всех участников батча по результатам «матча»

  5. Повторяем [2] несколько раундов

  6. Финальная сортировка по μ − 3σ

from trueskill import Rating, rate

# Инициализация
ratings = {post.id: Rating() for post in posts}

# После каждого "матча" (сортировки батча)
teams = [[ratings[pid]] for pid in sorted_ids]
ranks = list(range(len(sorted_ids)))  # 0 = первое место
new_ratings = rate(teams, ranks=ranks)

Почему батчи эффективнее пар:

  • Пары: один вызов → информация о 2 элементах

  • Батчи по 10: один вызов → информация о 10 элементах

Для 164 постов с парами нужно ~1230 вызовов. С батчами по 10 и 5 раундами — около 85 вызовов. В 15 раз дешевле при сопоставимом качестве.

Метод 5: TrueSkill Pairwise

Классический подход: сравниваем элементы попарно, обновляем рейтинги после каждого сравнения.

prompt = """Какой пост ИНТЕРЕСНЕЕ для разработчика?

[A] {post_a}

[B] {post_b}

Ответь одной буквой: A или B"""

Плюсы: теоретически чище, нет positional bias внутри батча.

Минусы: очень дорого. Для покрытия всех элементов нужно ~15 сравнений на каждый, итого 1230+ вызовов API.

Как работает TrueSkill

У каждого элемента два числа:

  • μ (mu) — средний рейтинг, наше лучшее предположение о «силе»

  • σ (sigma) — неопределённость, насколько мы уверены в оценке

Изначально все элементы равны: μ=25, σ=8.33.

После каждого «матча» система обновляет оба параметра:

  • Победитель: μ растёт, σ падает (мы увереннее в его силе)

  • Проигравший: μ падает, σ тоже падает

  • Ничья: μ сближаются, σ падают у обоих

Финальная сортировка идёт по μ − 3σ — это консервативная оценка. Элемент с высоким μ, но большим σ (мало сравнений) не попадёт в топ, пока система не станет в нём уверена.

Визуально это выглядит как нормальное распределение для каждого элемента. После 5-10 «матчей» распределения перестают пересекаться, и мы получаем стабильный рейтинг.

Результаты эксперимента

Я прогнал все 5 методов на 164 постах своего телеграм-канала, используя модель gpt-4.1-mini. Критерий — «вероятность репоста» (shareability).

Сравнение стоимости и скорости

Метод

API-вызовов

Токенов

Стоимость

Время

Bulk

1

14.5K

$0.006

6 сек

Score

164

81K

$0.056

1.2 мин

Score + Reasoning

164

103K

$0.080

1.3 мин

TrueSkill Batch

17

106K

$0.045

1.8 мин

TrueSkill Pairwise

1230

659K

$0.271

1.8 мин

Bulk в 45 раз дешевле Pairwise. Но дешевизна бесполезна, если результат плохой.

Корреляция с реальными репостами

Главный вопрос: насколько хорошо модель предсказывает, какими постами реально поделятся?

Визуализация корреляции различных подходов. Score побеждает. Но на реальных качественных сравнениях TrueSkill выглядит лучше.

Визуализация корреляции различных подходов. Score побеждает. Но на реальных качественных сравнениях TrueSkill выглядит лучше.

У меня были данные о фактических репостах (forwards) для каждого поста. Я посчитал корреляцию Спирмена между предсказанным рангом и реальными репостами:

Метод

Корреляция (ρ)

p-value

Интерпретация

Score

-0.50

<0.001

Сильная

TrueSkill Batch

-0.46

<0.001

Умеренная

Score + Reasoning

-0.43

<0.001

Умеренная

TrueSkill Pairwise

-0.42

<0.001

Умеренная

Bulk

+0.27

<0.001

Инверсия!

Отрицательная корреляция — это хорошо: меньше ранг (= выше в рейтинге) → больше реальных репостов.

Bulk показал положительную корреляцию — модель ранжирует посты в обратном порядке! Посты с большим количеством репостов оказываются внизу рейтинга. Вероятная причина — positional bias: модель просто ставит выше то, что прочитала первым.

Score оказался лучшим предсказателем при средней стоимости. Дороже Bulk в 9 раз, но реально работает.

TrueSkill Batch — хороший компромисс: дешевле Score, корреляция близкая. Особенно полезен, когда данные добавляются инкрементально — не нужно пересчитывать всё с нуля.

TrueSkill Pairwise — дорогой и не лучший. Тысяча сравнений накапливает случайный шум.

Когда какой метод использовать

Bulk — только для быстрой разведки на маленьких данных (<20 элементов). Для продакшена не годится.

Score — когда нужны объяснения для каждого элемента и важна максимальная точность. Хорош для приоритизации фичей, где важно понять «почему».

TrueSkill Batch — лучший выбор для большинства задач. Особенно если:

  • Данных много (100+ элементов)

  • Данные добавляются постоянно (можно добавлять новые элементы без полного пересчёта)

  • Бюджет ограничен

Score + Reasoning — гипотеза не подтвердилась. Принудительный reasoning не улучшил качество, только увеличил расход токенов.

TrueSkill Pairwise — только если критически важно избежать любого positional bias и бюджет не ограничен.

Неожиданные находки

Интуиция обманывает

Мои интуитивные фавориты оказались в середине рейтинга. Посты, которые я считал лучшими, модель оценила как средние. И судя по корреляции с реальными репостами — модель была права.

Разные аудитории — разные топы

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

Для разработчиков в топе — технические посты про исследования OpenAI, SGR-паттерн, Claude Code.

Для домохозяек TrueSkill вытащил посты про work-life balance, FOMO, медитацию. Bulk и Score с этой аудиторией справились плохо — выдавали технические посты, которые явно не для этой ЦА.

TrueSkill действительно выбирает хорошие посты для аудитории домохозяек, гораздо более привлекательные, чем Score или Bulk

TrueSkill действительно выбирает хорошие посты для аудитории домохозяек, гораздо более привлекательные, чем Score или Bulk

TrueSkill создаёт лучшие подборки

Субъективно, TrueSkill Batch выдаёт более «логичные» группировки. Топ-10 выглядит как осмысленная подборка, а не случайный набор. Вероятно, потому что относительные сравнения точнее абсолютных оценок.

На сложных смысловых вопросах Score-сортировка всё-таки выглядит хуже, чем TrueSkill

На сложных смысловых вопросах Score-сортировка всё-таки выглядит хуже, чем TrueSkill

Реализация

Код эксперимента выложен на GitHub. Основные компоненты:

sorting-demo/
├── sorters/
│   ├── bulk_sort.py          # Метод Bulk
│   ├── score_sort.py         # Метод Score  
│   ├── score_reasoned_sort.py # Score + Reasoning
│   ├── trueskill_sort.py     # TrueSkill Batch
│   └── trueskill_pairwise_sort.py # TrueSkill Pairwise
├── config.py                 # Промпты и настройки
├── llm_client.py            # OpenAI клиент с трекингом токенов
└── dashboard-react/         # React-дашборд для визуализации

Библиотека trueskill для Python:

pip install trueskill

Базовый пример TrueSkill Batch:

from trueskill import Rating, rate
import random

def trueskill_sort(items, compare_batch_fn, batch_size=10, rounds=5):
    """
    items: список элементов для сортировки
    compare_batch_fn: функция, которая принимает батч и возвращает 
                      отсортированные ID (через LLM)
    """
    ratings = {item.id: Rating() for item in items}
    items_dict = {item.id: item for item in items}
    
    for _ in range(rounds):
        # Перемешиваем и разбиваем на батчи
        ids = list(ratings.keys())
        random.shuffle(ids)
        
        for i in range(0, len(ids), batch_size):
            batch_ids = ids[i:i + batch_size]
            if len(batch_ids) < 2:
                continue
                
            batch = [items_dict[id] for id in batch_ids]
            sorted_ids = compare_batch_fn(batch)  # LLM сортирует батч
            
            # Обновляем рейтинги
            teams = [[ratings[id]] for id in sorted_ids]
            ranks = list(range(len(sorted_ids)))
            new_ratings = rate(teams, ranks=ranks)
            
            for id, (new_rating,) in zip(sorted_ids, new_ratings):
                ratings[id] = new_rating
    
    # Сортируем по μ - 3σ (консервативная оценка)
    return sorted(ratings.items(), 
                  key=lambda x: x[1].mu - 3 * x[1].sigma, 
                  reverse=True)

Выводы

  1. Не сгружайте LLM большие списки с просьбой отсортировать. Positional bias делает результат бесполезным.

  2. Относительные сравнения лучше абсолютных оценок. LLM (и людям) проще сказать «A лучше B», чем «A — это 73 из 100».

  3. TrueSkill Batch — оптимальный баланс цены и качества. Дешевле чем Score, точнее чем Bulk.

  4. Score — лучший предсказатель, если бюджет позволяет. Корреляция -0.50 с реальными данными — это уровень приличной ML-модели.

  5. Reasoning этап перед оценкой не помогает. По крайней мере, для задачи ранжирования контента.

Демо-дашборд для сравнения методов: artwist-polyakov.github.io/sorting-demo [3]


Статья основана на идеях из публикации Thariq Shihipar [4] (разработчик Claude Code, Anthropic) о применении TrueSkill для LLM-сортировки.

P.S. Пишу про AI-автоматизацию для бизнеса, Claude Code и подобные эксперименты в телеграм-канале «Поляков считает» [5].

Автор: artwistru

Источник [6]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/24570

URLs in this post:

[1] внимания: http://www.braintools.ru/article/7595

[2] Повторяем: http://www.braintools.ru/article/4012

[3] artwist-polyakov.github.io/sorting-demo: http://artwist-polyakov.github.io/sorting-demo

[4] из публикации Thariq Shihipar: https://www.thariq.io/blog/sorting/

[5] телеграм-канале «Поляков считает»: https://t.me/+oooCvS_tbL5jOTAy

[6] Источник: https://habr.com/ru/articles/987538/?utm_source=habrahabr&utm_medium=rss&utm_campaign=987538

www.BrainTools.ru

Rambler's Top100