Готовиться к собесу по списку «2000 вопросов с ответами» невозможно. Я пробовал: открываешь, листаешь минут сорок, отвечаешь на те, что и так знаешь, закрываешь с чувством выполненного долга. Через неделю в голове ноль. Список не помнит, где ты плаваешь, и не знает, когда ты забудешь то, что вчера повторил.
Лечится это не списком подлиннее, а движком практики: системой, которая помнит каждую встречу человека с каждым вопросом и решает, что показать следующим. Такой движок я собрал для своего тренажёра собесов, и статья целиком про него: корзины Лейтнера для памяти, Elo с поправкой на угадывание для сложности, карта слабых мест, политика сборки сессии. Всё с кодом, графиками и граблями с прода, плюс два отступления: чем в этой задаче неудобен LLM и откуда берётся банк вопросов. Сам я ведущий дата-сайентист, кандидатов собеседую регулярно, так что на проблему смотрю с обеих сторон стола.
Из чего движок состоит
Четыре механизма, каждый отвечает на свой вопрос:
Память: когда повторить то, что юзер уже видел. Корзины Лейтнера.
Сложность: что человеку по силам прямо сейчас. Elo с двумя поправками.
Карта слабых мест: какие темы проседают и просят внимания.
Сборка сессии: как из первых трёх собрать дневную порцию вопросов.
Всё состояние живёт в двух таблицах: у пары «юзер, вопрос» хранятся корзина, срок следующего показа и последний балл, у пары «юзер, подтема» точность, локальный Elo и давность. Весь движок: четыре файла, меньше семисот строк вместе с комментариями и SQL-выборками. Никакой экзотики.
Прежде чем разбирать механизмы по одному, короткое отступление о том, чего в движке нет и почему.
Чем неудобен LLM в рантайме
Соблазн 2026 года очевиден: прикрутить нейросеть, пусть оценивает свободные ответы, пишет разборы и сама решает, что спросить дальше. Я прикинул и отказался, по скучным инженерным причинам.
Деньги: у тренажёра freemium с бесплатным дневным лимитом, и бесплатный юзер, жгущий инференс на каждом ответе, превращается в счёт, растущий вместе с аудиторией. Ритм: петля «ответил, тут же увидел разбор» живёт на десятках миллисекунд, секунда раздумий модели после каждого вопроса убивает ощущение тренажёра. Детерминизм: одинаковый ответ обязан получать одинаковый балл, спор с нейросетью о том, почему вчера было «верно», а сегодня «частично верно», не выигрывает никто. Ну и цена фактической ошибки тут выше обычной: галлюцинацию человек выучит и понесёт на собес. Сверху добавьте зависимость от чужого API с чужими ценами и чужими даунтаймами, и получится ответ, почему нейросетей в рантайме нет. Совсем нет: ни оценки ответов, ни генерации на лету, ни инференс-счетов.
При этом LLM в проекте работает, но в оффлайне, на конвейере контента, где её выдачу успевает проверить человек. Про это ближе к концу, когда дойдём до банка.
Вторая рамка: команда проекта состоит из меня. У нового продукта нет истории ответов, чтобы обучать тяжёлые модели забывания вроде FSRS или IRT, а каждое усложнение архитектуры это будущий баг, чинить который тоже мне. Отсюда правило: берём самый простой механизм, который решает задачу, и наворачиваем сложность только там, где простота ломается об реальность. Дальше видно, что ломается она в неожиданных местах.
Память: корзины Лейтнера
Начну с ликбеза, он короткий. В 1885 году Герман Эббингауз измерил на себе кривую забывания: без повторения выученное осыпается по экспоненте, сначала быстро, потом всё медленнее. Отсюда идея интервального повторения: повторять материал не по расписанию, а прямо перед тем, как он выпадет из головы. Каждое удачное повторение делает след прочнее, и следующую встречу можно откладывать всё дальше.
Себастьян Лейтнер в 1972-м превратил это в систему из ящика с отсеками для бумажных карточек. Карточки лежат в первом отсеке. Ответил верно: карточка переезжает во второй, который перебирают реже. Снова верно: в третий, ещё реже. Ошибся: карточка возвращается в первый, цикл заново. Никакого компьютера, чистая механика, а кривую забывания обслуживает.
У меня то же самое, только отсек называется корзиной и заведён у каждой пары «юзер, вопрос»:
BOX_INTERVALS_DAYS = [0, 1, 3, 7, 21] PASS_THRESHOLD = 0.6 # score >= порога считается «верно» def next_box(current_box: int, passed: bool) -> int: if not passed: return 0 return min(current_box + 1, MAX_BOX)
Ответил верно: вопрос уезжает дальше, пауза растёт с дня до трёх недель. Ошибся: корзина обнуляется, и вопрос вернётся уже в ближайшей сессии.
Почему не SM-2, не FSRS? SM-2 добавляет к корзинам ease-фактор и даёт мало сверху. FSRS честно моделирует вероятность забывания, но её надо учить на данных, которых нет. Пять корзин объяснимы на пальцах и дёшевы в отладке. FSRS в планах на тот момент, когда накопленные ответы позволят её хотя бы провалидировать.
Сложность: Elo, как в шахматах, только вопрос тоже игрок
Второй ликбез. Elo это рейтинговая система, которую физик Арпад Эло придумал для шахматистов. Каждому игроку присвоено число. После партии победитель забирает у проигравшего немного очков, и размер добычи зависит от неожиданности результата: обыграл того, кто сильно выше, получил много, обыграл заведомо слабого, почти ничего. Разница рейтингов переводится в прогноз: при +400 ожидается примерно девять побед из десяти. За десятки партий число сходится к реальной силе игрока. Та же механика сегодня живёт в матчмейкинге онлайн-игр, так что если вас когда-нибудь бесил подбор соперников в доте, вы уже знакомы с героем этого раздела.
Здесь партию играют человек и вопрос. У юзера рейтинг, у вопроса сложность в той же шкале, и каждый ответ это матч:
def expected_score(user_rating, item_difficulty, guess=0.0): e = 1.0 / (1.0 + 10 ** ((item_difficulty - user_rating) / 400)) return guess + (1.0 - guess) * e new_user = user_rating + k * (score - exp) new_item = item_difficulty + k_item * ((1 - score) - (1 - exp))
Про параметр guess чуть ниже, он тут самый интересный. Сначала про то, что двигается.
Двигаются обе стороны. Рейтинг юзера очевидно. Но и сложность вопроса живёт своей жизнью: авторская оценка это лишь стартовая точка, дальше вопрос калибруют люди своими ответами. Вопрос, который валят сильные, дорожает. Вопрос, который щёлкают слабые, дешевеет.
K-факторы у сторон разные, и это важнее, чем кажется:
Юзер: K = 24, но первые 10 ответов K = 48, чтобы новичок калибровался быстрее.
Вопрос: K = 24, пока на него не ответили 50 раз, потом K = 8. Сложность устаканилась, дальше только медленный дрейф.
Без раздельных K получается неприятный эффект: самые нестабильные участники, новички с разогнанным K, сильнее всех дёргали бы откалиброванный банк. Свежий человек с непонятным уровнем приходит и таскает за собой сложность вопросов, которые до него оценили сотни людей. Раздельные коэффициенты эту связь режут.
Ещё обе шкалы зажаты: юзер 600..2400, вопрос 800..1800. Скучная строчка с clamp, но без неё один бот, спамящий первой кнопкой, за ночь укатывает свой рейтинг в минус бесконечность, а сложность случайных вопросов в стратосферу.
Грабли: «подбирай вопросы по рейтингу» звучит правильно и не работает
Первая версия подбора была наивной: берём вопросы со сложностью около рейтинга юзера. Красиво же, равный соперник.
На проде это выглядело так: точность пользователей 18-38%, жалобы «слишком сложно», один человек прямо написал, что чувствует себя тупым. При том что формально всё честно, матч-то равный.
Ошибка в интерпретации. Равный матч в Elo это ожидаемый успех 50%. Коинфлип. Для рейтинговой системы нормально, для обучения ад: человек заваливает каждый второй вопрос и уходит. Рабочая зона обучения устроена иначе (педагоги зовут её зоной ближайшего развития): около 70-85% успеха, чуть труднее комфортного. До неё надо добираться гандикапом.

Лечится одной константой: целимся не в рейтинг, а в рейтинг минус 150. По формуле выше это ожидаемые 70% без учёта угадывания и 78% с ним. После фикса кривая точности у живых юзеров поднялась туда, куда и должна была, а поток жалоб кончился.
Задним числом очевидно, да. Но «подбирай по рейтингу» так соблазнительно симметрично, что я оставил это в проде и получил урок вместе с пользователями.
Поправка на угадывание: четверть правильных ответов достаётся даром
Вопросы у нас с четырьмя вариантами. Человек, не знающий вообще ничего, набирает 25% просто тыкая. Классический Elo про это не в курсе: он решит, что перед ним игрок, выигрывающий каждый четвёртый матч у сильных соперников, и выдаст ему рейтинг повыше реального.
Поправка приходит из IRT, там это называется guessing-параметром трёхпараметрической логистической модели (3PL). Смысл: у вероятности успеха есть пол, ниже которого она не опускается даже при нулевом знании, и ожидание сдвигается ровно на него:
где N это число вариантов ответа.
Насколько это существенно? Симуляция на формулах движка: юзер с истинным уровнем 900 заявил себя середняком и стартовал с 1150. Движок без поправки застревает на 976, на 76 пунктов выше правды, и застревает навсегда, это устойчивая точка. С поправкой сходится ровно к 900.

76 пунктов звучит безобидно, пока не вспомнить, что вся зона подбора шириной 150. Половина зоны съедена систематической ошибкой: слабому юзеру стабильно приносят вопросы «на вырост», он стабильно их валит. Причём чем слабее человек, тем больше в его верных ответах удачи и тем сильнее его рейтинг врёт. Ирония в том, что страдают ровно те, кого продукт должен опекать.
У формата «собери систему из блоков» (есть у нас и такой, для секций систем-дизайна) вариантов не четыре, там g равен нулю.
Отдельно про угадывание и Лейтнер. Случайный верный тык льстит рейтингу, а заодно отправляет невыученный вопрос в дальнюю корзину: SRS честно решает «знает» и прячет вопрос на неделю. Каждый четвёртый тык лжёт системе памяти. Поэтому в интерфейсе рядом с вариантами живёт кнопка «не знаю»: честная сдача, ноль баллов, вопрос вернётся в ближайшую сессию. Заодно по событиям сдачи видно вопросы с невнятной формулировкой: где сдаются чаще, чем ошибаются, там надо переписывать текст, а не винить юзера.
Холодный старт: самооценка плюс разогнанный K
Новый юзер это рейтинг из воздуха. Ставить всем 1200 и ждать, пока Elo разберётся? При K = 24 он сходится десятки ответов, и всё это время человек получает вопросы не своего уровня. Первое впечатление будет испорчено ровно у всех.
Делаем проще. В онбординге человек сам говорит, зачем пришёл: «начинаю с нуля», «латаю пробелы», «освежаю перед собесом». Это стартовый рейтинг 1000, 1150 или 1350. Люди в самооценке уровня на удивление адекватны, а промах она даёт в разы меньший, чем слепые 1200 для всех.
Дальше калибровочная мини-сессия: шесть вопросов лесенкой сложности от стартовой точки, со смещениями -200, -100, 0, +100, +200 и финальным +50. Первый вопрос почти наверняка по зубам (вход без фрустрации), дальше ступеньки вверх. На первых десяти ответах K удвоен до 48. Кстати, эти шесть калибровочных вопросов не тратят дневной лимит бесплатного тарифа, было бы свинством брать плату за настройку.

На графике ожидаемые траектории для сильного юзера с истинным уровнем 1400: со старта 1350 и разогнанным K система оказывается в паре десятков пунктов от цели за десять ответов. Со слепого старта 1200 и K = 24 после сорока ответов рейтинг всё ещё врёт на сотню. Сорок ответов это три дня занятий. Три дня неправильных вопросов против десяти минут.
Рейтинг не один: подтемы и карта дыр
Общий рейтинг лжёт по-другому: он средняя температура по больнице. Человек с сильным SQL и провальной статистикой в среднем «средний», и оба типа вопросов приходят ему мимо уровня.
Поэтому Elo ведётся ещё и по подтемам (их в банке за две сотни: оконные функции, джойны, метрики классификации, вот это всё). Цель подбора считается по подтеме вопроса: Elo подтемы минус 150, а общий рейтинг только фолбэк для нетронутых подтем. Прайор новой подтемы равен текущему общему рейтингу: сильный юзер не начинает каждую тему «с нуля».
Поверх рейтингов живёт карта слабых мест. По каждой подтеме копится точность и давность, приоритет считается в одну строчку:
weakness = (1 - accuracy) * 0.7 + staleness * 0.3 # staleness: дни без касания / 30, с капом
С точностью была засада, которую я заметил не сразу: среднее за всю жизнь инертно, как супертанкер. Человек был слаб в A/B-тестах, подтянулся, а карта ещё месяц рисует ему дыру, потому что старые провалы тянут среднее вниз. Заменил на экспоненциальное среднее с альфой 0.2: погоду делают последние ~10 ответов, первые пять попыток считаются обычным средним, чтобы EMA не дёргалась на пустом месте. Карта ожила.
Сборка сессии: политика поверх математики
Сессия это 12 вопросов, минут на десять. Состав фиксированный микс:
Доля | Откуда | Зачем |
|---|---|---|
40% | повторения, у которых подошёл срок | закрепление |
30% | новое, приоритет слабым подтемам | рост охвата |
20% | недавние ошибки | добить проваленное |
10% | случайное из зоны подбора | чтобы не превращалось в рутину |
И щепотка человеческого поверх цифр. Первый вопрос дня всегда полегче: порог входа в ежедневную привычку должен быть низким, человек пришёл после работы, не надо встречать его с порога вопросом про изоляцию транзакций. Сложных вопросов подряд не больше двух, третий переставляется дальше по списку.
Самое спорное правило называется анти-фрустрационным: если человек провалил вопрос, который выше его рейтинга больше чем на 300 пунктов, вопрос НЕ идёт в повторения и в работу над ошибками. Звучит как потакание лени, но у Лейтнера другая работа: он про «вот-вот забуду», а не про «никогда не знал». Гонять непосильный вопрос по корзинам значит регулярно бить человека об стену и жечь его лимит. Вопрос просто отложен и вернётся сам, когда рейтинг подтемы дорастёт. Освоенного это не касается: что человек хоть раз решил, повторяется при любой сложности, это уже его знание.
Отдельный режим «симуляция собеса» устроен иначе: пять секций по три вопроса, сложность не от рейтинга, а от уровня позиции (джун 1050, мидл 1250, сеньор 1450), и перед вариантами пауза: сформулируй ответ про себя, потом жми и смотри варианты. Тест тренирует узнавание, собес требует вспоминания вслух, это разные мышцы. Пауза с необходимостью проговорить хотя бы мысленно закрывает часть разрыва.
Откуда берутся вопросы
Движку нужен банк, покрывающий сетку «подтема на диапазон сложности», иначе подбирать будет не из чего. Сейчас в банке 2700 с лишним вопросов на 236 подтем, и собирался он не из головы.
Начал я вообще не с вопросов, а с вакансий. Распарсил их по своим ролям и сверил с разборами реальных собесов: что рынок спрашивает на самом деле, а не что я думаю, что он спрашивает. Разница нашлась быстро: например, кандидатов по DS уже вовсю гоняют по LLM и RAG, а в моём первоначальном плане тем этого блока почти не было. Карта тем банка пошла от этого среза.
Дальше вопросы приходят двумя путями. Часть собрана парсингом открытых источников: разборы собесов, публичные списки вопросов, обсуждения. Часть пишет LLM в оффлайне: даю модели материалы по теме, она изучает и генерирует черновики вопросов, с вариантами, ключом и разбором. Черновик не равен вопросу: каждый я верифицирую руками, ключ, разбор, дистракторы, сложность. Это моя предметная область, я сам провожу собеседования, так что этап быстрый, но обязательный: часть черновиков умирает именно здесь.
На 2700 вопросов человеческая память не рассчитана, поэтому за гигиеной следят скрипты. Валидатор схемы проверяет, что буквы правильных ответов сбалансированы и правильный вариант не выделяется длиной: тесты, где верный ответ всегда самый подробный, щёлкаются без знания предмета. Дедуп-скан со стеммированным Жаккаром ловит смысловые повторы между подтемами. Слоп-скан отстреливает категоричные дистракторы: вариант со словом «всегда» или «никогда» опытный сдаватель тестов отбрасывает не читая.
Со сложностью в банке есть системный перекос, за которым приходится следить отдельно: авторская разметка тяготеет к круглым числам, джуновские вопросы получают ценники в районе 1000-1080, мидловые 1200-1300, и в сетке заводятся пустые полосы, где вопросов будто бы нет, хотя концепты давно покрыты соседними ценниками. Лечится это не добором новых вопросов, а перекалибровкой существующих. Впрочем, авторская сложность в любом случае только стартовая точка: дальше её двигают ответы людей, как описано в разделе про Elo.
Ощущение «продукт понимает, что мне нужно» в итоге складывается не из большой модели, а из четырёх простых механизмов, каждый из которых допилен там, где об него споткнулась реальность. Кривой забывания почти полтора века, Elo придумали для шахматных турниров, поправка на угадывание приехала из психометрики прошлого века. Вся магия это аккуратно собранная классика, и для задачи «довести человека до собеса подготовленным» её хватает с запасом.
Пощупать движок вживую можно в тренажёре «Сеньорчик». Он для тех, кто готовится к собесам по DS/ML, аналитике данных, дата-инженерии, QA или Java-бэкенду: 2700 вопросов с мгновенным разбором, десять минут в день, а что показать следующим, решает всё описанное выше.
