Если вы когда-нибудь грузили в LLM список и просили выбрать лучшее или отсортировать — вы, скорее всего, получали посредственный результат. Я проверил это на 164 постах своего телеграм-канала, сравнив пять разных методов сортировки. Оказалось, что разница между «дёшево и плохо» и «дёшево и хорошо» — в правильном алгоритме, а не в модели.
В этой статье разберём, почему наивные подходы не работают, как алгоритм из Xbox Live помогает ранжировать контент, и какой метод даёт лучшую корреляцию с реальными данными.
Постановка задачи
Допустим, вам нужно отсортировать большой список по субъективному критерию: интересность статей, приоритет фичей, качество резюме. Классический поиск или эмбеддинги здесь не помогут — нужна семантическая оценка.
Первый порыв — скормить весь список в LLM и попросить отсортировать. Но тут возникают проблемы:
Ограничение контекста — в окно влезает не всё
Деградация внимания — модель внимательнее читает начало списка
Нестабильность — один и тот же запрос может дать разные результаты
Лимиты вывода — получить обратно 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. Он позволяет оценить «силу» игрока, даже если тот никогда не играл против конкретного соперника — достаточно истории матчей.
Применяем к сортировке:
Каждому элементу присваиваем начальный рейтинг (μ=25, σ=8.33)
Формируем случайные батчи по 10 элементов
Просим LLM отсортировать батч
Обновляем рейтинги всех участников батча по результатам «матча»
Повторяем несколько раундов
Финальная сортировка по μ − 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. Но дешевизна бесполезна, если результат плохой.
Корреляция с реальными репостами
Главный вопрос: насколько хорошо модель предсказывает, какими постами реально поделятся?

У меня были данные о фактических репостах (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 создаёт лучшие подборки
Субъективно, TrueSkill Batch выдаёт более «логичные» группировки. Топ-10 выглядит как осмысленная подборка, а не случайный набор. Вероятно, потому что относительные сравнения точнее абсолютных оценок.

Реализация
Код эксперимента выложен на 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)
Выводы
Не сгружайте LLM большие списки с просьбой отсортировать. Positional bias делает результат бесполезным.
Относительные сравнения лучше абсолютных оценок. LLM (и людям) проще сказать «A лучше B», чем «A — это 73 из 100».
TrueSkill Batch — оптимальный баланс цены и качества. Дешевле чем Score, точнее чем Bulk.
Score — лучший предсказатель, если бюджет позволяет. Корреляция -0.50 с реальными данными — это уровень приличной ML-модели.
Reasoning этап перед оценкой не помогает. По крайней мере, для задачи ранжирования контента.
Демо-дашборд для сравнения методов: artwist-polyakov.github.io/sorting-demo
Статья основана на идеях из публикации Thariq Shihipar (разработчик Claude Code, Anthropic) о применении TrueSkill для LLM-сортировки.
P.S. Пишу про AI-автоматизацию для бизнеса, Claude Code и подобные эксперименты в телеграм-канале «Поляков считает».
Комментарии (23)

rPman
21.01.2026 15:58Я правильно понимаю, речь идет об использовании одной модели (общей) в которую буквально текстом задают вопрос о сравнении? Но модели с этим изначально отвратительно справляются... даже если так делать, нужно так формулировать промпт, что бы модели нужно было для ответа дать один токен (да/нет) и вероятность этого токена (logits/logprobs, не каждый api дает к ним доступ) будет целевой неуверенностью модели в этом ответе, это уже число, с которым можно работать (прогнать такой промпт по старой базе постов и сравнить, вычислить закономерность, скорее всего там будет что то типа перекрывающегося множества часть тех постов для которых ответ да будет давать с хорошими шансами ответ нет но определенной вероятностью).
Но ведь для этого лучше дообучить свою модель, условно у нас есть база старых постов с оценками, тюним модель на следующий промпт ПостA -> оценка (только не численная а один токен, типа оценка от 0 до 9 да еще и зафиксировать жестко доступные токены с помощью structured output/gramma, для llm-ок это удобнее и дает лучше результат, а несколько разрядов в числах ее путают) и теперь сможем делать предсказание на базе своей модели, имея на руках оценку ее правдоподобности.
Точно так же можно тюнить и сравнение двух постов, даем два поста через разделитель и просим сказать 1 или 2 (например промпт - какая статья интереснее), что бы ответ был один токен, вероятность перед этими токенами даст нам численную информацию, с которой можно работать, типа или score = logit('ответ 1') - logit('ответ 2')

artwistru Автор
21.01.2026 15:58В эксперименте structured output, не один токен.
Идея с logprobs интересная — это даст confidence «бесплатно».Но:
1. Файнтюн привязан к критерию. Начну писать про кулинарию вместо AI — модель переобучать. Поменяю ЦА с разработчиков на домохозяек — снова переобучать. TrueSkill работает на любом критерии, просто меняешь промпт.
2. Для 164 постов файнтюн — из пушки по воробьям
3. Logprobs в OpenAI API есть, но не на structured output
4. Главная проблема не в confidence отдельного сравнения, а в positional bias у BulkTrueSkill даёт confidence «из коробки»: σ (sigma) = неопределённость. Много матчей → низкий σ → высокий confidence. И работает на любых критериях без дообучения.
Файнтюн + logits точнее для стабильной задачи в продакшене с миллионами и миллиардами образцов. TrueSkill — для исследований и меняющихся критериев.У себя я использую файнтюн только на задаче управления поисковыми фразами в агентстве - их там реально миллионы.

rPman
21.01.2026 15:58Я бы не просто 'новая тема - переобучать', а делал бы переобучение с появлением каждого нового поста (как он вылежится нужный период времени, например мы собираем лайки за первые сутки, первую неделю, первый месяц...).
Никто не требует обучать большую модель... можно собирать из постов embendings, добавлять к ним какие то свои цифровые параметры (размер статьи, количество подписчиков у автора, количество постов у автора и т.п. вплоть до времени дня или день недели публикации, расстояние до ближайшей популярной публикации в ближайшие сутки и т.п. много чего можно придумать) и имея на руках эту базу, обучить маленькую нейронку предсказывать целевое число на ней.
2. Для 164 постов файнтюн — из пушки по воробьям
в смысле 164... вам нужно выгрузить другие телеграм каналы (желательно вашей тематики) нужны тысячи, желательно десяток тысяч постов, потому что прогнозировать только по своей маленькой выборке действительно глупо.
upd. когда речь о маленьком количестве, кажется методы в статье имеют больший смысл, заморачиваться по крупному наверное неоправданно сложно

artwistru Автор
21.01.2026 15:58Тут вопрос один: не является ли это оверкиллом и не плодит ли новые точки недетерминированности/отказа.
у меня в канале есть пост про поиск для сайта на векторах. Так вот он рассыпался на запросе <дымо ход>. Да да, тот самый, который решается легким костылем к эластику разбивается в мире векторных эмбеддингов.
я прихожу к выводу, что надо тренировать эмбеддинги, но если мы тренируем его, то меняется все пространство векторных представлений, значит мы его переобраьатыыаем каждый раз после тренировки.
то есть в вашем примере мы получаем двойной файнтюн. И это лишь одно из возможных неудобств

rPman
21.01.2026 15:58не понимаю, зачем нужно тренировать эмбендинги
у вас опечатка/ошибка, которую используемая модель для извлечения эмбендингов не поняла... так используйте изначально другую.

artwistru Автор
21.01.2026 15:58какую бы модель использовали вы, чтобы решить эту задачу?

rPman
21.01.2026 15:58у меня плохо с практикой, я просто беру то что советуют
везде и все сразу говорят про intfloat/multilingual-e5 small/base/large (до 0.6b), если маленькие модели то для ку сегмента лучше наверное sber
ну там всякие sbert или rubert-tiny2
но главное другое, текст нужно перед подачей в модель вычищать, от ссылок например, и соответственно тут же можно и проверку правописания провести (правда я бы предварительно еще по параграфам прошел бы, классифицируя как тот же код или как текст, что бы не вылечить лишнего).

artwistru Автор
21.01.2026 15:58Ну вот на практике иногда трен эмбеддингов не такая плохая вещь. Тут же важно, чтобы задача решалась. Косяк из примера был на руберте. Но на данных из 1с вести себя криво начинает приблизительно всё.
Вот вы и сами говорите, что: предобработка, тесты моделей, замена моделей если начинается неадекват. То есть банальная задача рейтинга становится супер инженерной (дорогой).
Я в метариале описал методы, которые в основе своей примитивны. Не требуется предобработка. Точек отказа почти нет. Результаты адекватные.

rPman
21.01.2026 15:58да я собственно так и подумал, вариант в статье по простому заметно дешевле чем заниматься более сложной разработкой.. с другой стороны большую часть этого сможет сделать ИИ как кодер и девопс почти сам (просто непривычно и страшно давать ему полные права без контроля)

artwistru Автор
21.01.2026 15:58мне тоже это не нравится и полные yolo права я даю ему только на виртуалках.
у меня на днях появилась гипотеза ограничить все опасные действия хуками, если у меня получится направлять в ответ на сомнительные действия hook с exit code 2 и печатать причину в stdout/stderr — то вероятно я смогу перестать бояться, что мои пароли уплывут какому-то хакеру.

Mortello
21.01.2026 15:58Как будто репостам не хватает нормировки по просмотрам, это важно

artwistru Автор
21.01.2026 15:58Я когда супруге показал идею, она возразила, типа ну у тебя за год канал в 7 раз вырос. Но прикол в том, чтл глазиков там у всех постов приблизительно одинаково. Ну с поправкой на стандартное отклонение.
А так да, я согласен, но тут это не очень нужно.

bt2901
21.01.2026 15:58Заземление в реальную статистику репостов - отлично, но всё равно не хватает эксперимента по устойчивости предсказанного рейтинга между разными запусками модели (с разным рандомным сидом, или там стояла нулевая температура?) Если сид не влияет, то всё равно будет зависимость от порядка постов, если их перемешать.
Для TrueScore Batch это вдвойне интересно, потому что добавляется ещё фактор разбиения постов на батчи. Аргумент, что TrueScore можно считать инкрементально, не пересчитывая все сравнения при добавлении новых объектов, работает только при условии того, что полученные в итоге оценки более-менее воспроизводимы и устойчивы.

artwistru Автор
21.01.2026 15:58Интересная мысль. Попробую посчитать на днях.
Нас как бы интересует, насколько сильно мутирует топ N между независимыми запусками каждого из вариантов (есть подозрение, что bulk тут окажется на высоте из за позиционного предпочтения)
Я сделаю прогон на на выходных. Интересно посчитать. Если есть идеи по метрикам — накидывайте

artwistru Автор
21.01.2026 15:58прогнал по топ-20 посты для разработчиков. Коэф Жаккара 0,78 для TrueSkill, а Bulk 0,51.
чутьё подвело. 0,78 — это что то около 18 постов из топа пересекаются. для bulk — 11 постов пересекаются.

artwistru Автор
21.01.2026 15:58Прогнал 10 запусков × 4 метода × 7 критериев. Гипотеза не подтвердилась — Bulk оказался наименее стабильным

как вам в рейтинге вдохновения разброс на десятки строк Std ранга 17,8 у Bulk — пост может прыгать на ±18 позиций между запусками. На box plots видны выбросы до 60-140 позиций.
Единственное исключение — критерий «boring» (Bulk: жаккард 0,82). Видимо, оценка скучности достаточно простая, чтобы модель справлялась даже при positional bias.

видимо признак очень простой для предсказания TrueSkill выигрывает и по корреляции с реальными репостами, и по воспроизводимости — усреднение по матчам сглаживает случайный шум.

vblll
21.01.2026 15:58А почему предполагается линейная зависимость между интересом (или сюрпризом, как обратной величиной вероятности события) и количеством репостов? Для нелинейных зависимостей корреляция неинформативна, а линейная тут под вопросом, репост действие многофакторное.

artwistru Автор
21.01.2026 15:58Я использовал в расчетах корреляцию Спирмена, не Пирсона. Спирмен — ранговая корреляция, она не предполагает линейность, только монотонность: если модель считает пост лучше, он должен получать больше репостов. Форма зависимости (линейная, логарифмическая, степенная) не важна.
По поводу многофакторности — абсолютно согласен. Репост зависит от времени публикации, охвата в момент поста, темы, длины, наличия картинки и ещё десятка факторов. Модель видит только текст.
Поэтому корреляция ~0,50 — это не «модель идеально предсказывает виральность», а «модель выделяет что-то релевантное из текста, что коррелирует с репостами». Учитывая шум от внешних факторов, даже 0,5 — неплохой сигнал.
Если бы я хотел предсказывать репосты точнее, добавил бы фичи: день недели, время, длину, наличие ссылок, эмодзи и т.д. Но задача была другая — сравнить методы сортировки между собой на одинаковых данных.
Если честно, то мне больше нравится как разбились посты на группы по интересам, чем предсказательная сила репоста. TrueSkill реально показывает себя отличным способом делать произвольные рейтинги.
Cordekk
Score лучший предсказатель, потому что в канале люди ведь не занимаются оценкой постов, они просто репостят то, что им в конкретный момент времени зашло.
artwistru Автор
Это правда, но в целом, если пост полезный, у него и будет много пересылок (хотя бы в сохраненные сообщения).
А оценить пост как полезный можно любым из методов. В целом рейтинговать полезность или интересность для домохозяек схожие задачи.