feat(trainer): разбор типовых ошибок (репетитор C1, движок)
- TrainerEngine.analyzeMistake(problem, value) -> {type, hint} | null: по неверному числовому ответу распознаёт типовую ошибку и даёт адресную подсказку, НЕ выдавая ответ
- solve: уравнение восстанавливается как линейное f(x)=A·x+B по двум точкам (без структуры генератора) -> ловит «забыл разделить на коэффициент»
- общие эвристики: перепутан знак (value≈-correct), близкая арифметическая ошибка (|Δ|≤20%), иначе generic
- работает для solve/compute; пара/корни/неравенство пропускаются
- смоук движка 825/825 (T20: nodivide/sign/arith/generic/null)
- страница НЕ тронута (редизайн в параллельной сессии); показ подсказки на неверном ответе подключу на странице вместе с полировкой ввода систем после редизайна
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -452,6 +452,47 @@
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Разбор типовой ошибки ученика (репетитор, направление C) ──
|
||||
По неверному ЧИСЛОВОМУ ответу пытается распознать типовую ошибку и дать
|
||||
адресную подсказку, НЕ выдавая правильный ответ. Работает для solve/compute.
|
||||
Для solve уравнение восстанавливается как линейное f(x)=A·x+B по двум точкам
|
||||
(без структуры генератора) → ловим «забыл разделить на коэффициент». Плюс
|
||||
общие эвристики: перепутан знак, близкая арифметическая ошибка.
|
||||
Возвращает { type, hint } или null (ошибка не распознана / ответ верный). */
|
||||
function _linAB(problem) {
|
||||
var av = problem.answerVar || 'x';
|
||||
var e0 = {}, e1 = {}; e0[av] = 0; e1[av] = 1;
|
||||
var g0 = evalExpr(problem.lhsExpr, e0) - evalExpr(problem.rhsExpr, e0);
|
||||
var g1 = evalExpr(problem.lhsExpr, e1) - evalExpr(problem.rhsExpr, e1);
|
||||
if (!isFinite(g0) || !isFinite(g1)) return null;
|
||||
return { A: g1 - g0, B: g0 }; // f(x) = A·x + B, корень = -B/A
|
||||
}
|
||||
function analyzeMistake(problem, value) {
|
||||
if (!problem || !isFinite(value)) return null;
|
||||
var kind = problem.kind || 'solve';
|
||||
if (kind !== 'solve' && kind !== 'compute') return null; // пара/корни/неравенство — отдельно
|
||||
var correct = problem.answer;
|
||||
var tol = 1e-6 * Math.max(1, Math.abs(correct));
|
||||
if (Math.abs(value - correct) <= tol) return null; // на самом деле верно
|
||||
|
||||
// структурно: линейное уравнение → «забыл разделить на коэффициент»
|
||||
if (kind === 'solve') {
|
||||
var ab = _linAB(problem);
|
||||
if (ab && Math.abs(ab.A) > 1.5) {
|
||||
var noDivide = -ab.B; // значение на шаге «A·x = -B», ещё не делённое на A (= A·correct)
|
||||
if (Math.abs(value - noDivide) <= Math.max(tol, 1e-6 * Math.abs(noDivide)))
|
||||
return { type: 'nodivide', hint: 'Похоже, ты не разделил обе части на коэффициент при переменной (' + fmtNum(ab.A) + '). Раздели — и получишь ответ.' };
|
||||
}
|
||||
}
|
||||
// перепутан знак ответа
|
||||
if (correct !== 0 && Math.abs(value + correct) <= Math.max(tol, 1e-6 * Math.abs(correct)))
|
||||
return { type: 'sign', hint: 'Кажется, перепутан знак. Проверь знаки при переносе слагаемых через знак «=».' };
|
||||
// близкая арифметическая ошибка
|
||||
if (Math.abs(value - correct) <= Math.max(1, Math.abs(correct) * 0.2))
|
||||
return { type: 'arith', hint: 'Очень близко — похоже на арифметическую ошибку в вычислениях. Пересчитай аккуратно.' };
|
||||
return { type: 'generic', hint: 'Разбери решение по шагам и попробуй похожую задачу.' };
|
||||
}
|
||||
|
||||
/* Система: ученик вводит пару «x = 2; y = 3» (или «2; 3»). Проверяем подстановкой в ОБА уравнения.
|
||||
Метки переменных опциональны; без меток — по порядку answerVars. */
|
||||
function _checkSystem(problem, raw) {
|
||||
@@ -609,6 +650,7 @@
|
||||
generateBatch: generateBatch,
|
||||
verifyRoot: verifyRoot,
|
||||
checkStudentAnswer: checkStudentAnswer,
|
||||
analyzeMistake: analyzeMistake,
|
||||
checkStep: checkStep,
|
||||
makeRng: makeRng,
|
||||
// мелочи наружу для билдера/тестов
|
||||
|
||||
Reference in New Issue
Block a user