Contexte. Cet article documente l'architecture de ma première plateforme d'IA éducative, que j'ai conçue et opérée en production en 2025-2026. Le projet est aujourd'hui terminé ; j'ai depuis pivoté vers l'ODERSA et l'éducation gratuite. La démarche technique, elle, reste valable telle quelle.

Le wrapper ChatGPT est le McDonald's de l'EdTech : rapide, accessible, et pédagogiquement vide. Donner à un élève un assistant qui crache la solution finale au premier prompt, c'est court-circuiter exactement le mécanisme cognitif qu'on prétend renforcer. Ce que j'ai construit n'était pas un wrapper : un orchestrateur qui contraint le LLM à questionner plus qu'il n'affirme.

Voici l'architecture réelle qui tournait en production. Pas un schéma théorique : du code, des choix concrets, des compromis.

Le problème en une ligne

Un LLM moderne résout sans forcer la grande majorité des exercices scolaires français, du collège à la terminale. Servi nu, il désengage l'élève en quelques secondes : copier-coller énoncé, copier-coller réponse, devoir rendu. À la fin du trimestre, le bulletin est correct et la tête est vide.

Le défi technique : forcer un modèle entraîné pour répondre à se comporter comme un professeur qui questionne. Un changement de polarité comportementale qui ne tient ni dans un seul prompt, ni dans un seul appel API.

Architecture haute-niveau

Trois étages successifs. Chaque message élève passait par les trois avant qu'une réponse ne soit affichée :

# Étage 1 : classification d'intention
élève → [Classifier] → { veut_solution | veut_indice | vérifie_calcul | autre }

# Étage 2 : génération contrainte par persona
intention → [Prompt engineering] → [LLM Claude Sonnet] → réponse_brute

# Étage 3 : garde-fou de complétude
réponse_brute → [Validateur] → réponse_finale OU régénération forcée

Étage 1 : la classification d'intention

Avant tout traitement coûteux, on classe le message. Pourquoi ? Parce qu'un élève qui demande « vérifie mon calcul » ne mérite pas le même traitement qu'un élève qui demande « quelle est la réponse ». La classification était faite par un modèle léger (Claude Haiku) avec un prompt très court :

const intent = await classify({
  model: "claude-haiku-4-5",
  system: "Classe l'intention en UN mot parmi: solution|indice|vérification|hors_sujet",
  message: élève_input
});

Latence minime, coût marginal. Bénéfice : on peut refuser immédiatement les requêtes solution sans les router vers le modèle principal, ce qui économise l'essentiel du coût d'inférence.

Étage 2 : la génération contrainte

Cœur du système. Le LLM principal (Claude Sonnet) recevait un prompt combinant trois couches :

const response = await claude.messages.create({
  model: "claude-sonnet-4-5",
  system: SOCRATIC_SYSTEM_PROMPT,         // persona socratique, stable
  messages: [
    { role: "user", content: contextEleve },     // niveau, matière
    { role: "user", content: ragSnippets },      // 3-5 chunks pertinents
    { role: "user", content: élève_input }
  ],
  max_tokens: 800
});

Le SOCRATIC_SYSTEM_PROMPT contenait les règles dures : ne jamais donner la solution finale en premier ; toujours commencer par une question ; identifier ce que l'élève sait déjà ; proposer un pas, pas un saut.

Étage 3 : le garde-fou de complétude

Même avec un système prompt béton, Claude lâche occasionnellement la réponse complète, surtout si l'élève insiste à coups de « mais donne-moi juste le résultat ». On ajoutait donc un validateur post-hoc vérifiant que la réponse n'était pas une solution finale déguisée.

async function validateSocratic(response, exercise) {
  const verdict = await classify({
    model: "claude-haiku-4-5",
    system: "La réponse contient-elle la solution complète? Réponds: oui|non",
    message: `Exercice: ${exercise}\nRéponse IA: ${response}`
  });

  if (verdict === "oui") {
    return regenerate({ stricter: true });
  }
  return response;
}

En production, ce validateur ne se déclenchait que rarement, mais chaque déclenchement évitait une réponse qui aurait trahi la promesse pédagogique. Coût marginal : un appel Haiku par message. C'est le prix de la cohérence.

Le RAG : une base de connaissances maison

Le LLM seul ne connaît pas le programme scolaire français en détail. Pour donner des indices contextualisés, il a besoin d'ancrage. C'était le rôle de la base de connaissances : un fonds d'articles structurés par programme officiel, indexés vectoriellement, requêtables par embedding.

// À chaque message élève, on récupère les 3-5 chunks les plus pertinents
const embedding = await embedQuery(élève_input);
const chunks = await supabase.rpc("match_documents", {
  query_embedding: embedding,
  match_threshold: SEUIL_SIMILARITE,   // ajusté empiriquement
  match_count: 5,
  filter_niveau: élève.niveau,
  filter_matiere: exercice.matiere
});

Les embeddings étaient stockés dans Supabase (PostgreSQL + extension pgvector), en région AWS Paris. La recherche était quasi instantanée et aucune donnée ne quittait l'Union européenne.

Choix techniques structurants

Pourquoi Claude (Anthropic) plutôt que GPT-4 ?

Pourquoi Supabase + pgvector plutôt que Pinecone ?

Pourquoi un classifier Haiku séparé plutôt qu'un seul appel Sonnet ?

Limites & angles morts

Le système n'était pas magique. Trois faiblesses connues :

Conclusion

Construire un tuteur socratique avec un LLM, c'est moins un problème de modèle qu'un problème de contraintes. Le modèle est puissant, trop puissant pour la pédagogie. Le travail d'architecte consiste à brider intelligemment cette puissance pour qu'elle serve l'apprentissage au lieu de le court-circuiter.

Trois étages, deux modèles, une base vectorielle, un système prompt béton. Moins spectaculaire qu'un agent autonome, mais ça a tourné en production, dans le respect du RGPD, à coût maîtrisé. Et si la plateforme n'a pas survécu à son modèle économique, cette architecture, elle, reste bonne à prendre.