DEV Community

shashank ms
shashank ms

Posted on

Building Adaptive Learning Systems with LLM: Best Practices and Examples

Adaptive learning systems adjust difficulty in real time based on learner performance. In this tutorial, I will walk through a lightweight Python tutor that maintains a per-topic skill profile, generates coding questions matched to the current level, and updates that level after each answer. The entire pipeline runs against Oxlo.ai using the OpenAI SDK, so you can ship it without retooling your stack.

What you'll need

Step 1: Model the learner

We need a small state object that tracks skills across topics and persists history. I use a plain dictionary and a helper to cap levels between 1 and 10.

import json
from datetime import datetime

LEARNER_PROFILE = {
    "student_id": "dev-001",
    "skills": {"loops": 3, "functions": 2, "classes": 1},
    "history": []
}

def update_skill(topic: str, delta: int):
    old = LEARNER_PROFILE["skills"].get(topic, 5)
    new = max(1, min(10, old + delta))
    LEARNER_PROFILE["skills"][topic] = new
    LEARNER_PROFILE["history"].append({
        "ts": datetime.utcnow().isoformat(),
        "topic": topic,
        "delta": delta,
        "new_level": new
    })

Step 2: Write the system prompt

The prompt is the contract between your code and the model. It tells the tutor how to read the profile and what JSON schema to return. Keep it in a constant so you can tune it without touching business logic.

SYSTEM_PROMPT = """You are an adaptive Python tutor.
You receive a learner profile with skill levels from 1 (beginner) to 10 (expert) and a target topic.
Generate exactly one coding exercise matching the skill level.
Return ONLY a JSON object with these keys:
- "question": the exercise text,
- "starter_code": a skeleton string or empty string,
- "target_topic": the topic being tested,
- "difficulty": the intended level from 1 to 10.
Do not include explanations outside the JSON."""

Step 3: Generate questions with Oxlo.ai

I call Oxlo.ai with the current skill profile injected into the user message. Because Oxlo.ai uses request-based pricing, I can pass the full profile and recent history every time without inflating cost. See https://oxlo.ai/pricing for plan details. I use llama-3.3-70b for reliable instruction following.

from openai import OpenAI

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

def generate_question(topic: str):
    profile_json = json.dumps(LEARNER_PROFILE["skills"], indent=2)
    user_message = f"Target topic: {topic}\nCurrent skill levels:\n{profile_json}"

    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
        response_format={"type": "json_object"},
        temperature=0.7,
    )

    return json.loads(response.choices[0].message.content)

Step 4: Evaluate answers and compute a skill delta

After the student submits an answer, we grade it and decide whether to move the level up, down, or keep it flat. I use qwen-3-32b for this reasoning step and request JSON back.

EVAL_PROMPT = """You are an adaptive Python tutor.
You receive an exercise, the learner's answer, and the current skill level.
Evaluate the answer as correct, partially correct, or incorrect.
Return ONLY a JSON object with these keys:
- "evaluation": one of ["correct", "partial", "incorrect"],
- "delta": +1 for correct, 0 for partial, -1 for incorrect,
- "feedback": a concise 1-sentence explanation.
Do not include text outside the JSON."""

def evaluate_answer(question: dict, answer: str, skill_level: int):
    user_message = (
        f"Exercise: {question['question']}\n"
        f"Starter code: {question.get('starter_code', '')}\n"
        f"Learner answer:\n{answer}\n"
        f"Current skill level: {skill_level}"
    )

    response = client.chat.completions.create(
        model="qwen-3-32b",
        messages=[
            {"role": "system", "content": EVAL_PROMPT},
            {"role": "user", "content": user_message},
        ],
        response_format={"type": "json_object"},
        temperature=0.3,
    )

    return json.loads(response.choices[0].message.content)

Step 5: Close the loop and persist state

Now I wire the pieces into a single session turn. The function asks a question, evaluates the answer, updates the profile, and writes the state to disk so the learner can resume later.

def tutoring_turn(topic: str, student_answer: str) -> dict:
    # Generate a question for the current level
    q = generate_question(topic)

    # Evaluate the student's answer
    current_level = LEARNER_PROFILE["skills"].get(topic, 5)
    result = evaluate_answer(q, student_answer, current_level)

    # Adapt the profile
    update_skill(topic, result["delta"])

    # Persist state
    with open("learner_state.json", "w") as f:
        json.dump(LEARNER_PROFILE, f, indent=2)

    return {
        "question": q["question"],
        "starter_code": q.get("starter_code", ""),
        "evaluation": result["evaluation"],
        "feedback": result["feedback"],
        "new_level": LEARNER_PROFILE["skills"][topic],
    }

Run it

Here is a short script that simulates two turns with a student who is learning loops.

if __name__ == "__main__":
    # Simulate two turns
    turn_1 = tutoring_turn(
        topic="loops",
        student_answer="for i in range(5): print(i)"
    )
    print("--- Turn 1 ---")
    print(json.dumps(turn_1, indent=2))

    turn_2 = tutoring_turn(
        topic="loops",
        student_answer="i = 0\nwhile i < 5:\n    print(i)\n    i += 1"
    )
    print("\n--- Turn 2 ---")
    print(json.dumps(turn_2, indent=2))

Example output after running the script:

--- Turn 1 ---
{
  "question": "Write a for loop that prints numbers 0 through 4.",
  "starter_code": "",
  "evaluation": "correct",
  "feedback": "Correct use of a for loop with range.",
  "new_level": 4
}

--- Turn 2 ---
{
  "question": "Write a while loop that prints the first 5 integers starting from 0.",
  "starter_code": "",
  "evaluation": "correct",
  "feedback": "Correct while loop with proper increment.",
  "new_level": 5
}

Wrap-up

Next, replace the in-memory profile with SQLite so you can serve multiple students from a FastAPI endpoint. You can also add a retrieval step using Oxlo.ai's embeddings endpoint to pull relevant documentation into the prompt when the learner stalls. If you want to experiment without cost concerns, try the evaluation step with deepseek-v3.2 on Oxlo.ai's free tier.

Top comments (0)