ГлавнаяБлогАлгоритм интервального повторения: как мы строили планировщик
Алгоритмы

Алгоритм интервального повторения: как мы строили планировщик

Разбираем алгоритм интервального повторения в приложении для слов: модель состояния, почему не FSRS, и как починили снежный ком. Читайте и применяйте!

Al
Редакция Algolitalgolit.ru
7 мин чтения2 августа 2026 г.

Алгоритм интервального повторения: как мы строили планировщик и что сломалось

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

Модель: два числа и конечный автомат

Вся модель — это два числа и конечный автомат. Никаких сложных зависимостей.

class Word:
    state: 'LEARNING' | 'REVIEW'
    correct: int      # успешных ответов в LEARNING
    interval: int     # дней, значимо только в REVIEW
    difficulty: float # начинается с 1.0
    due: date

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

После выпуска — лестница удвоения: 1 → 2 → 4 → 8 → 16 → 32 → 64 → 128 → 256 → 512 дней. Правильный ответ — шаг вверх, ошибка — откат, при серьёзном провале слово возвращается в LEARNING и снова зарабатывает девять ответов.

Почему мы не взяли FSRS

Вопрос, который возникает у всех: почему не FSRS — алгоритм, на который перешёл Anki? Он моделирует память через difficulty, stability и retrievability, предсказывая вероятность вспомнить слово сегодня и выбирая день, когда вероятность пересекает целевую retention. Это лучше. Но мы не взяли его по двум причинам.

Нужны логи просмотров, которых у нас не было. FSRS подгоняет параметры под реальную историю. В первый день истории нет — пришлось бы ставить дефолтные параметры и сложную систему ради нуля выгоды.

Лестница удвоения отлаживается. Когда пользователь пишет, что слово слишком часто возвращается, я читаю его историю за десять секунд и объясняю причину. С подобранной моделью я бы смотрел на три числа. На этапе, когда ты ещё выясняешь, что твой продукт делает не так, возможность объяснить систему пользователю ценнее пары процентов retention.

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

Сигналы: бинарная оценка теряет слишком много

Единственное место, где мы не пошли простым путём, — оценка ответа. Большинство карточек сводят ответ к «верно/неверно», выбрасывая почти всю информацию. Два пользователя ответили правильно, но один думал девять секунд и использовал подсказку.

Поэтому difficulty стартует с 1.0 и меняется не только от правильности:

def update_difficulty(word, answer):
    if answer.attempt > 1:
        difficulty += a  # справился, но не с первой попытки
    if answer.used_hint:
        difficulty += b
    if answer.time_ms > slow_for(word.exercise_type):
        difficulty += c
    if answer.type == SPEECH and answer.recognition_score < threshold:
        difficulty += d
    recent_error_rate = errors(word, last_sessions=10) / attempts(word, last_sessions=10)
    difficulty = blend(difficulty, recent_error_rate)
    return clamp(difficulty, MIN, MAX)

def next_interval(word):
    return round(ladder_step(word.interval) / word.difficulty)

Константы a, b, c, d я намеренно не раскрываю — они подобраны вручную, на глаз. Важна форма: медленный, с подсказкой, со второй попытки «правильно» — не то же самое, что мгновенный ответ, и интервал должен быть короче.

Речевой сигнал — самый ценный. Упражнения на произношение проходят через распознавание речи, и низкий балл у слова, которое пользователь «знает», — ранний признак, что он знает его только визуально. А такое знание и рушится в разговоре.

Скользящая ошибка за последние 10 сессий — противовес. Одиночный ответ шумен: люди тыкают не туда в автобусе. Слово должно быть стабильно трудным, чтобы коэффициент сдвинулся.

Что сломалось: снежный ком

Планировщик был в порядке. Очередь — нет.

В модели нет ограничения на число слов, которые становятся due в один день. Пользователи добавляют слова пачками — сорок слов в воскресенье вечером. Через три дня все сорок одновременно выходят из LEARNING. Ещё через шестнадцать — снова вместе. Лестница детерминирована, поэтому пачки держат строй вечно.

Каждая дата по отдельности верна. Но во вторник приложение говорит: «у вас 380 слов на повторение». Люди не повторяют 380 слов — они закрывают приложение. А система, которую не открывают, хуже отсутствия: интервалы истекают, число растёт, к пятнице уже 520.

Мы видели это в данных: перед оттоком сессии не удлинялись, а укорачивались и прекращались. Бэклог никого не перегружал — он просто деморализовал.

Фикс: ограничиваем сессию, а не добавление

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

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

def build_session(user):
    due = words_due_for(user)  # может быть 500
    ranked = sort_by(due, [overdue_days desc, difficulty desc])
    return take(ranked, SESSION_SIZE)  # всегда 10

Что изменилось. Видимое число перестало быть угрозой. В интерфейсе нет «380», только сессия из десяти и план на три сессии сегодня. Работа та же, но не выглядит стеной.

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

Бэклог остался, но тает медленно, а не героическим забегом. Это нормально — тот же компромисс, что и лестница, только для очереди.

Что ещё открыто

Две вещи, на которые у меня нет хорошего ответа.

Долгие отсутствия. Человек пропал на два месяца и вернулся к очереди, где все интервалы взорвались. Сброс всего в LEARNING — слишком жестоко; игнорировать разрыв — первый сеанс будет полон забытых слов. Сейчас сортировка по просроченности скрывает симптом, но это не решение.

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

Практический вывод

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

А если пользователь пропадает на два месяца? Поделитесь в комментариях, как вы решаете эту задачу. Мне до сих пор не даёт покоя.

#интервальное повторение#планировщик#алгоритмы#Python
Al
Редакция Algolit

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

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →