Selector в действии
Алгоритм одной картинкой
Заголовок раздела «Алгоритм одной картинкой»flowchart TD S[Состояние ученика · вектор P(L)] --> Loop["Каждая задача из пула"] Loop --> P["Совместный P(solve) — геом. среднее по навыкам"] P --> C["Близость к цели exp(-(p-0.7)^2/0.03)"] P --> R["Доля навыков с P(L) < 0.4"] C --> Score["итог = близость + 0.15 * редкость"] R --> Score Score --> Sort[Сортировка по убыванию итога] Sort --> Recent[Исключить задачи из последних 5 ответов] Recent --> TopN[Топ-N рекомендаций]Это все подвижные части алгоритма. Никаких нейросетей, градиентов и цикла обучения — только взвешенный рейтинг.
Шаг за шагом
Заголовок раздела «Шаг за шагом»1. Состояние ученика
Заголовок раздела «1. Состояние ученика»state = { student_id: "S-12", mastery: { "linear_eq.expand_brackets": 0.166, // упал после серии ошибок "arith.signs": 0.831, // силён "arith.distributive_law": 0.45, "linear_eq.move_to_one_side": 0.62, "linear_eq.divide_by_coefficient": 0.78, // ... }, history: [/* последние ответы */]}2. Каждой задаче считаем «насколько подходит»
Заголовок раздела «2. Каждой задаче считаем «насколько подходит»»Для задачи T-147 с навыками [expand_brackets, signs]:
Closeness:
Rarity (1 из 2 навыков ниже 0.4):
Score:
3. Top-N после сортировки
Заголовок раздела «3. Top-N после сортировки»Селектор пробегает по pool, считает score для каждой, сортирует по убыванию, выкидывает задачи из последних 5 (чтобы не повторяться) и возвращает top-5.
Реальный код
Заголовок раздела «Реальный код»export function recommend( state: StudentState, pool: Task[], topN = 5, opts?: SelectorOptions, params?: BktParams): ScoredTask[] { const recentIds = new Set( state.history.slice(-5).map((h) => h.task_id) ); const scored = pool .filter((t) => !recentIds.has(t.id)) .map((t) => scoreTaskForStudent(state, t, opts, params)); scored.sort((a, b) => b.score - a.score); return scored.slice(0, topN);}Что делать, если все задачи плохо подходят
Заголовок раздела «Что делать, если все задачи плохо подходят»Бывает: у Ивана и в pool нет задач только на скобки + лёгкая арифметика — все включают сложные навыки. Что делать?
Селектор всё равно вернёт наименее плохой вариант. Но топ-1 будет
с низким closeness. Это сигнал:
- учителю: «база задач не покрывает реальные проблемы класса»;
- Andri (куратору контента): «нужны задачи попроще на этот навык».
В UI учителя можно показывать индикатор «слабая рекомендация» (closeness < 0.3) — это часть объяснимости.
Tie-break и rare-skill bonus
Заголовок раздела «Tie-break и rare-skill bonus»Если две задачи имеют почти одинаковый closeness — селектор предпочтёт
ту, которая тренирует слабый навык через rareSkillBonus. Это
важно:
- без бонуса селектор может зависнуть в зоне комфорта;
- с бонусом он исследует слабые места, чтобы их подтянуть.
Параметр rareSkillBonus = 0.15 — компромисс:
- слишком маленький (0.05): селектор «не лезет» в слабые навыки, ученик застревает на тех, что уже умеет;
- слишком большой (0.5): селектор всё время даёт сложные задачи, фрустрация.
0.15 даёт примерно: «при равном closeness, задача с одним слабым навыком из двух предпочтительнее».
Когда селектор «ошибается»
Заголовок раздела «Когда селектор «ошибается»»Если ты прогоняешь селектор и видишь странную рекомендацию — почти всегда причина в одном из:
- Mastery vector неинициализирован — для нового навыка нет ,
код падает на дефолт
params.pInit = 0.2. Это норм. - Discontinuity history — ученик пропустил 2 недели, его mastery устарело, нужен decay. Не делаем для MVP.
- Bias таксономии — Andri разметил один навык на 80% задач, второй на 5%. Селектор «сваливается» в первый. Решается на уровне разметки.
См. главу про объяснимость — она делает причины рекомендаций видимыми, что упрощает дебаг и поднимает доверие учителя.