DEV Community

shashank ms
shashank ms

Posted on

Building Adaptive Learning Systems with LLM

We are going to build a lightweight adaptive learning tutor that adjusts question difficulty in real time based on student performance. It maintains a running profile of weak topics and uses an LLM to generate the next targeted exercise. This is useful for ed-tech tools, certification prep, or internal training platforms.

What you'll need

Step 1: Design the Tutor Persona

We need a system prompt that forces the model to return structured JSON containing the next question, the target difficulty, and a brief rationale. Keeping the prompt explicit prevents drift across turns.

SYSTEM_PROMPT = """You are an adaptive learning engine. Your job is to help a student master Python programming.

RULES:
- Evaluate the student's last answer briefly.
- Update their profile: topics_weak, topics_strong, current_difficulty (1-10).
- Generate exactly one follow-up question appropriate for their level.
- Return ONLY valid JSON in this format:
{
  "evaluation": "short feedback",
  "current_difficulty": 5,
  "topics_weak": ["decorators"],
  "topics_strong": ["list comprehensions"],
  "next_question": "the question text",
  "hint": "a small hint if they are stuck"
}

Do not wrap the JSON in markdown. Return raw JSON only."""

Step 2: Initialize the Client and State

I instantiate the OpenAI-compatible client pointing at Oxlo.ai and set up an in-memory profile to track the student's session. For a production app you would swap this dictionary for a small database, but a dict keeps the tutorial focused.

from openai import OpenAI
import json

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

student_profile = {
    "current_difficulty": 3,
    "topics_weak": ["loops"],
    "topics_strong": [],
    "history": []
}

Step 3: Build the Evaluation Function

This function sends the current student state and the latest answer to the model, then parses the JSON response to update the profile. I use qwen-3-32b because it handles structured agent workflows reliably, and Oxlo.ai's request-based pricing means the cost stays flat even when I stuff a long conversation history into the context.

def generate_next_question(student_profile, last_answer="I am ready to start."):
    context = json.dumps(student_profile, indent=2)
    
    user_content = f"""Student profile:
{context}

Student's last answer:
{last_answer}

Provide the next adaptive step as raw JSON."""

    response = client.chat.completions.create(
        model="qwen-3-32b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_content},
        ],
    )
    
    raw = response.choices[0].message.content.strip()
    # Clean up accidental markdown fences
    if raw.startswith("

```"):
        raw = raw.split("```

")[1].replace("json", "").strip()
    
    result = json.loads(raw)
    
    # Update profile
    student_profile["current_difficulty"] = result["current_difficulty"]
    student_profile["topics_weak"] = result["topics_weak"]
    student_profile["topics_strong"] = result["topics_strong"]
    student_profile["history"].append({
        "question": result.get("next_question", ""),
        "answer": last_answer,
        "evaluation": result["evaluation"]
    })
    
    return result

# Seed the first question
first_turn = generate_next_question(student_profile)
print("Q:", first_turn["next_question"])
print("Hint:", first_turn.get("hint", ""))

Step 4: Run the Interactive Loop

Now I wire the function into a simple command line loop so you can type answers and watch the model adapt. Each turn appends to the history, and because Oxlo.ai charges per request rather than per token, growing the context window does not inflate your bill. You can see our pricing at https://oxlo.ai/pricing.

def run_tutor():
    print("Adaptive Python Tutor")
    print("Type 'exit' to quit.\n")
    
    # Prime with first question
    turn = generate_next_question(student_profile)
    print(f"[Difficulty {student_profile['current_difficulty']}]")
    print("Question:", turn["next_question"])
    if turn.get("hint"):
        print("Hint:", turn["hint"])
    
    while True:
        answer = input("\nYour answer: ").strip()
        if answer.lower() in ("exit", "quit"):
            break
        
        turn = generate_next_question(student_profile, last_answer=answer)
        print(f"\nFeedback: {turn['evaluation']}")
        print(f"[Difficulty {student_profile['current_difficulty']}]")
        print("Next Question:", turn["next_question"])
        if turn.get("hint"):
            print("Hint:", turn["hint"])
    
    print("\nFinal profile:", json.dumps(student_profile, indent=2))

if __name__ == "__main__":
    run_tutor()

Run it

Save the complete script as adaptive_tutor.py, substitute your API key, and run python adaptive_tutor.py. Below is a sample session where the student incorrectly explains a list comprehension and the tutor adjusts.

$ python adaptive_tutor.py
Adaptive Python Tutor
Type 'exit' to quit.

[Difficulty 3]
Question: Write a list comprehension that extracts even numbers from 0 to 10.
Hint: Use range(11) and the modulo operator.

Your answer: [x for x in range(11) if x / 2 == 0]

Feedback: You used division instead of modulo. Remember, x % 2 == 0 checks evenness.
[Difficulty 3]
Next Question: Correct the list comprehension to filter even numbers using modulo.
Hint: The operator is %.

Next steps

Replace the in-memory dictionary with a SQLite table so the tutor remembers students across restarts. You could also add a second pass through Oxlo.ai's deepseek-v3.2 to generate unit tests that verify the student's code before the tutor evaluates the explanation.

Top comments (0)