As of July 1, 2026, the honest product reality is this: large language models can sound like excellent math tutors, but they are unreliable calculators. OpenAI’s own Structured Outputs docs use a math tutoring app as the canonical example for schema-shaped UI responses — and still warn that structured output “can still contain mistakes” (OpenAI Structured Outputs). Recent research on frontier models finds that even medal-winning systems remain “stubbornly bad at basic arithmetic” as operand length grows, with most errors tied to tokenization misalignment and carry mistakes rather than deep reasoning (AI-rithmetic, February 2026).
That combination defines the engineering job. A math tutor agent is not “prompt the model to invent a worksheet.” It is a split architecture:
- Deterministic code generates random parameters, computes the canonical answer, and verifies student submissions.
- The LLM turns verified facts into learner-friendly wording, hints, and step narratives.
- Evaluators check that explanations match the ground truth before anything reaches a student.
OpenAI’s Study Mode, launched July 29, 2025, follows a similar product instinct: Socratic questioning and scaffolded hints instead of instant answers — but OpenAI also states it is powered by custom system instructions and can show “inconsistent behavior and mistakes across conversations” (OpenAI Study Mode announcement). If you are building your own tutor, you need stronger guarantees than prompt tone alone.
TL;DR
- Do not ask the LLM to invent random numbers and the correct answer in one shot. That couples two hard problems: sampling and arithmetic.
- Generate problems with seeded, parametric templates. Same seed → same worksheet. Different seed → new numbers, same difficulty profile.
- Compute answers outside the model with SymPy, a sandboxed Python evaluator, or domain-specific solvers. Treat the LLM as a narrator, not a calculator.
- Use Structured Outputs for shape, not truth. JSON schema guarantees field presence; it does not guarantee
2 + 2 = 5is impossible (OpenAI Structured Outputs). - Verify every explanation against ground truth before display. Reject or regenerate when steps disagree with the canonical solution.
- Evaluate with scenario tests plus answer-key checks, not vibes. See Trace Grading vs Scenario Testing for the broader eval pattern.
What You Will Learn Here
- Why math tutoring breaks the “one prompt, one model” pattern
- Which LLM inference limitations matter for problem generation, answers, and explanations
- A reference architecture that separates truth, language, and pedagogy
- How to generate random but reproducible problems with templates and seeds
- A minimal TypeScript/Python implementation you can adapt
- Verification, grading, and production eval hooks you can wire into CI
Inferred Article Shape
This article assumes you want an agent that can:
- generate a math problem on demand (optionally at a chosen difficulty),
- produce the correct answer and solution steps,
- explain how to solve it in plain language,
- stay consistent when the same seed is reused,
- avoid shipping arithmetic hallucinations to learners.
If your scope is only step-by-step tutoring on fixed textbook problems — no random generation — you can skip the generator sections and keep the compute-then-explain and verification parts.
Why Math Tutors Fail When Built Like Chatbots
Most demo tutors look like this:
User: "Give me a hard algebra problem"
|
v
LLM invents problem + answer + steps in one completion
|
v
Polished explanation (sometimes wrong)
That design fails for predictable reasons:
| Failure mode | What goes wrong | Why the LLM is a bad owner |
|---|---|---|
| Random operands | ”Random” values drift across turns | LLMs do not expose a real RNG; they predict plausible tokens |
| Arithmetic | Final answer is wrong | Frontier models still degrade on multi-digit addition (AI-rithmetic) |
| Rule consistency | A + B ≠ B + A in edge cases | Models often violate algebraic invariants under perturbation (addition diagnostic, April 2025) |
| Step validity | Steps look coherent but skip logic | Benchmark success ≠ step-level correctness in education settings (AIME-Con 2025 paper) |
| Reproducibility | Same request → different worksheet | Non-deterministic sampling unless you control seeds elsewhere |
A wrong answer destroys trust faster than a bland explanation. The fix is to move truth out of the model and treat the LLM as a presentation layer on verified facts.
Reference Architecture: Truth, Language, Pedagogy
+---------------------------+
| Problem spec (JSON) |
| topic, difficulty, seed |
+-------------+-------------+
|
v
+------------------+ +------------------+ +------------------+
| Parametric | | Ground-truth | | LLM explainer |
| generator |-->| engine (SymPy / |-->| (hints, steps, |
| (seeded RNG) | | sandboxed code) | | Socratic tone) |
+------------------+ +--------+---------+ +--------+---------+
| |
v v
+--------+---------+ +--------+---------+
| Canonical result | | Draft explanation|
| problem + answer | | structured JSON |
+--------+---------+ +--------+---------+
\ /
v v
+-----+----------------------+-----+
| Verifier / grader |
| - numeric equality |
| - step replay |
| - policy (hint-not-answer) |
+---------------+---------------+
|
v
+----------+-----------+
| Student-facing UI |
+----------------------+
Three roles, three owners:
- Generator — software you control completely.
- Ground-truth engine — symbolic or numeric code with tests.
- LLM — language, scaffolding, misconception diagnosis.
This is the same neuro-symbolic split described in tool-augmented math stacks: parse with the model, compute with SymPy, explain with the model again (SymPy MCP ecosystem, math-logic-mcp).
LLM Inference Limitations That Actually Matter Here
1. Randomness is not a model feature
If you need “10 new problems every morning” or “same worksheet after refresh,” use seed + deterministic code, not “please randomize.”
// Same seed => same operands. Different seed => new worksheet.
function mulberry32(seed: number) {
return function () {
seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function randInt(rng: () => number, min: number, max: number) {
return Math.floor(rng() * (max - min + 1)) + min;
}
Store { seed, templateId, params } in your session or database. Rehydrate the exact problem later without trusting chat memory.
2. Structured Outputs shape JSON, not mathematics
OpenAI’s docs are explicit: Structured Outputs “ensure schema adherence” but “can still contain mistakes” (Structured Outputs guide). Use schemas for UI fields like:
{
"steps": [{ "explanation": "string", "expression": "string" }],
"hintLevel": "nudge | scaffold | reveal",
"finalAnswer": "string"
}
Never treat schema compliance as proof the math is correct.
3. Tokenization affects arithmetic
Research on frontier models shows number tokenization changes error patterns; comma-separated or reversed digit grouping can shift accuracy (Tokenization counts, 2024). Practical takeaway: even if the model gets the answer right sometimes, do not build grading on model mental math.
4. Explanations can contradict themselves
Education-focused evaluations report both calculation slips and logical reasoning errors in model-generated solutions (AIME-Con 2025 paper). Your verifier must check final value and intermediate steps, not rhetorical confidence.
Implementation: Parametric Problem Templates
Start with templates, not free-form generation. Each template defines:
- allowed operand ranges,
- constraints (integer-only, no negative divisors, etc.),
- canonical solver function,
- difficulty metadata for API filters and template selection.
type ProblemTemplate = {
id: string;
topic: "linear-equation" | "quadratic-factor" | "percent-change";
params: (rng: () => number) => Record<string, number>;
render: (p: Record<string, number>) => string;
solve: (p: Record<string, number>) => { answer: string; steps: string[] };
};
const linearEquation: ProblemTemplate = {
id: "linear-equation-v1",
topic: "linear-equation",
params: (rng) => {
const m = randInt(rng, 2, 9);
const x = randInt(rng, -12, 12);
const b = randInt(rng, -20, 20);
const c = m * x + b;
return { m, b, c, x };
},
render: ({ m, b, c }) => `Solve for x: ${m}x + ${b} = ${c}`,
solve: ({ m, b, c, x }) => ({
answer: String(x),
steps: [
`Subtract ${b} from both sides: ${m}x = ${c - b}`,
`Divide both sides by ${m}: x = ${x}`,
],
}),
};
export function generateProblem(template: ProblemTemplate, seed: number) {
const rng = mulberry32(seed);
const params = template.params(rng);
const prompt = template.render(params);
const truth = template.solve(params);
return { seed, templateId: template.id, prompt, truth, params };
}
Why this pattern holds up in production:
- Difficulty is explicit in code (
min 2–9, integer solutions), not inferred from prompt tone. - Each template ships with a unit-testable solver.
- Bug reports replay from
{ seed, templateId }alone.
Implementation: Ground Truth With SymPy
For algebra beyond hand-written templates, push computation to SymPy:
import sympy as sp
def solve_linear(equation: str, variable: str = "x") -> dict:
x = sp.Symbol(variable)
lhs, rhs = equation.split("=")
expr = sp.Eq(sp.sympify(lhs.strip()), sp.sympify(rhs.strip()))
solution = sp.solve(expr, x)
return {
"answer": str(solution[0]),
"verified": sp.simplify(sp.sympify(lhs) - sp.sympify(rhs).subs(x, solution[0])) == 0,
}
Patterns worth copying from MCP math tools:
verify_arithmeticfor simple expressions with zero symbolic deps (math-logic-mcp).solve_equation/simplify_expressionfor algebra and calculus.- Return proof steps from the tool, not from the model.
Security note: never eval() raw model output. Accept only structured params your generator created, or parse through a strict AST policy before execution — the same guardrail used in production SymPy MCP servers.
Implementation: LLM Explainer Prompt
Once truth exists, the model’s job is narrower and safer:
type ExplainRequest = {
problem: string;
canonicalAnswer: string;
canonicalSteps: string[];
hintLevel: "nudge" | "scaffold";
learnerAttempt?: string;
};
const system = `
You are a math tutor. You MUST NOT change the correct answer or canonical steps.
You may rephrase steps, add intuition, and ask Socratic questions.
If the learner attempt is wrong, diagnose the likely misconception without revealing
the full solution when hintLevel=nudge.
Return JSON matching the MathExplanation schema.
`;
OpenAI’s Structured Outputs cookbook uses the same separation: schema for steps[] and final_answer, with the model reasoning about a user-supplied question (Structured Outputs intro). In your app, the question and final answer are inputs, not model inventions.
Pedagogy alignment: OpenAI Study Mode emphasizes Socratic questions, hints, and knowledge checks instead of immediate answers (Study Mode announcement). Encode that as hintLevel policy in code, not hope in the prompt.
Verification Loop (Non-Negotiable)
Before returning anything to the UI:
Draft explanation
|
v
1) Parse finalAnswer
|
v
2) Compare to canonicalAnswer (symbolic or numeric tolerance)
|
v
3) Replay canonicalSteps OR re-solve with SymPy
|
v
4) Optional LLM judge: "Do steps support the answer?"
|
v
Pass -> render | Fail -> regenerate once -> escalate to template-only steps
Minimal verifier:
function verifyExplanation(
canonical: { answer: string; steps: string[] },
draft: { finalAnswer: string; steps: { expression: string }[] }
) {
if (canonical.answer.trim() !== draft.finalAnswer.trim()) {
return { ok: false, reason: "final_answer_mismatch" };
}
// Stronger: evaluate each draft.expression with your symbolic engine
return { ok: true };
}
If verification fails, fall back to deterministic steps from the template. A correct but plain explanation beats a charismatic wrong one.
Agent Orchestration Flow
For a full agent (generate → explain → tutor → grade):
Student Orchestrator Tools / Services
| | |
|-- "Give me medium algebra"| |
| |-- pick template + seed ------>| generator
| |<-- problem, truth ------------|
| |-- explain(truth) ------------>| LLM
| |<-- draft JSON -----------------|
| |-- verify(truth, draft) ------>| verifier
|<-- problem + hints ------| |
| | |
|-- student attempt ------>|-- grade attempt ------------->| grader
|<-- feedback -------------|<-- compare to truth ----------|
Tool design tips:
generate_problem(templateId, seed, difficulty)— deterministic.compute_solution(problemSpec)— SymPy / sandbox.explain_solution(truth, pedagogyPolicy)— LLM only.grade_attempt(truth, attempt)— deterministic first; LLM for misconception labels second.
This mirrors the broader agent stack from How to Learn Production-Grade AI Agent Engineering: explicit tools, policy gates, traces, and eval hooks — not one unbounded chat loop.
Evaluation: What to Measure Before Launch
| Layer | What to test | Example pass criteria |
|---|---|---|
| Generator | same seed → same problem | 100% match on 1k seeds |
| Solver | template answers vs SymPy | 100% equality on generated set |
| Explainer | verified drafts | ≥ 99% pass verifier |
| Pedagogy | hint policy | 0 full answers when hintLevel=nudge |
| Regression | model swap | answer-key tests unchanged |
Use scenario tests for tutoring behavior (“student asks for answer repeatedly — still scaffolds”) and trace or answer-key grading for correctness drift after prompt or model changes (Trace Grading vs Scenario Testing).
For learner-outcome guardrails, pair engineering evals with the learning-science caution from How to Use AI to Learn Faster Without Becoming Dependent on It: measure whether your tutor builds skill, not just completion.
Pre-Launch Checklist
Before shipping:
- Which topics are template-safe on day one? Start with integer linear equations, percent word problems, factoring quadratics with integer roots — not open-ended proof generation.
- Do you need reproducible assignments? If yes, persist
seedin session state and share links. - What happens when verification fails? Default: render canonical steps from the template, skip LLM embellishment.
- Does the architecture match the claim? A “verified practice engine” needs ground-truth code; a prompt-only tutor will eventually ship wrong answers.
- How will regressions get caught? Pre-launch: answer-key suite in CI. Post-launch: wrong-answer reports plus periodic SymPy replay.
What Not to Overclaim
Editorial judgment, stated plainly:
- This architecture reduces arithmetic risk; it does not solve open-ended proof tutoring or word-problem comprehension alone.
- LLM-generated hints can still be pedagogically weak while being mathematically aligned.
- Study Mode-style Socratic behavior helps learning outcomes, but UI copy should not imply formal verification unless you built the verifier layer.
Ship honesty as a feature.
Sources
Primary and strong technical sources:
- OpenAI — Structured model outputs — schema adherence vs correctness; math tutoring UI example (accessed July 2026).
- OpenAI — Introduction to Structured Outputs (cookbook) — math reasoning JSON schema example.
- OpenAI — Introducing Study Mode — July 29, 2025 product behavior and stated limitations.
- AI-rithmetic (arXiv:2602.10416, February 2026) — frontier-model arithmetic error patterns.
- Do LLMs Truly Grasp Addition? (arXiv:2504.05262, April 2025) — commutativity and symbolic perturbation failures.
- Tokenization counts (arXiv:2402.14903, 2024) — tokenization effects on arithmetic.
- Mathematical Computation and Reasoning Errors by LLMs (AIME-Con 2025) — step-level error taxonomy in educational settings.
- lwsinclair/sympy-mcp — SymPy tools exposed via MCP.
- ismailkerimov/math-logic-mcp — verified arithmetic and symbolic routing pattern.
Related reading on this site: