Pourquoi un pipeline multi-agent
Quand nous avons commencé le tuteur IA de Studeia, la tentation était évidente : appeler l'API Claude avec un long system prompt et les messages de l'apprenant. Ça fonctionne en démo. Ça échoue en production.
Quatre problèmes structurels :
-
Pas de mémoire persistante de l'apprenant — chaque tour traite l'apprenant à zéro. L'apprenant vient de rater le concept X 3 fois dans des quizzes ? Le LLM ne le sait pas. Le tuteur répète l'explication de base qu'il a déjà donnée la semaine dernière.
-
Pas d'ancrage dans le matériel du cours — l'institution a des supports de cours, des slides, des transcriptions de vidéos. Le LLM pur n'y accède pas. Il invente des faits. Il cite des concepts erronés.
-
Pas de modération réelle — un system prompt avec « soyez prudent » est insuffisant. Un apprenant en souffrance psychologique apparaît. Un apprenant tente un jailbreak. Un apprenant utilise un langage inapproprié. Le tuteur doit réagir de manière appropriée sans désactiver la fonctionnalité pour les apprenants légitimes.
-
Pas de feedback loop — le système n'apprend pas. Les mêmes misconceptions se répètent. Le professeur n'a pas de visibilité sur les points faibles collectifs de la classe.
Solution : séparer les responsabilités en agents spécialisés.
L'architecture
Message de l'apprenant
↓
PRÉ-LLM (synchrone, zéro coût LLM)
1. StudentModelService.getSnapshot()
2. RetrievalAgent.retrieve()
3. PedagogicalAgent.select()
4. buildEnrichedPrompt()
↓
LLM PRINCIPAL (streaming, SSE vers le client)
5. router.stream() avec fallback automatique
Claude Sonnet → GPT-4o → Grok-3 → Gemini Pro
↓
POST-LLM (arrière-plan via after(), fire-and-forget)
6. EvaluationAgent (Haiku)
7. ContentAgent (Haiku)
8. SupervisorAgent (Haiku)
Détaillons chacun.
1. StudentModelService — le « profil cognitif »
Avant tout LLM call, nous chargeons le snapshot de l'apprenant :
const snapshot = await StudentModelService.getSnapshot({
userId,
courseId,
});
// Retourne :
{
conceptMastery: Map<conceptId, { probability, confidenceInterval }>,
misconceptions: Misconception[], // actives + en résolution
episodicMemory: Episode[], // ce qui a fonctionné auparavant
quizContext: {
totalAttempts,
avgScore,
passRate,
weakAreas: string[] // concepts avec mastery < 0.4
},
recentHistory: Message[] // fenêtre glissante 10 msgs
}
ConceptMastery utilise une distribution Beta bayésienne — chaque concept a alpha (succès) + beta (échecs). Probabilité = alpha / (alpha + beta). Intervalle de confiance via percentiles 5% et 95%.
EpisodicMemory enregistre des insights pédagogiques : « l'analogie de la pizza a fonctionné pour expliquer les fractions », « la métaphore du tuyau d'eau a échoué pour l'électricité ». Le système apprend ce qui fonctionne avec chaque apprenant.
Zéro coût LLM. Tout en requêtes Prisma + calcul déterministe.
2. RetrievalAgent — RAG tenant-scoped
Au lieu que le LLM essaie de se souvenir de faits sur les mathématiques, la biologie, l'histoire, nous le laissons citer le matériel de l'institution elle-même.
const chunks = await retrieve({
query: reformulatedQuery, // 1. reformule la requête avec contexte
filters: { tenantId, courseId }, // 2. isolation absolue
k: 10,
tenantOnlyMode: true, // 3. ne cite jamais le contenu d'une autre institution
boostByWeakAreas: snapshot.quizContext.weakAreas, // 4. priorise les chunks des zones faibles
});
Le RAG per-tenant est critique. L'établissement XYZ a son propre matériel sur le baccalauréat. L'Université ABC a son propre matériel sur le Calcul. Le tuteur cite le matériel CORRECT de l'institution, pas un agrégat générique.
Chaque chunk a des métadonnées : { source: "course_lesson", courseId, lessonId, lessonTitle, moduleTitle }. Quand le tuteur répond, il cite : « Comme expliqué dans le cours "Géométrie Analytique" du module 3... »
Voyage AI génère les embeddings (1024 dimensions, fallback OpenAI). pgvector stocke. tenantOnlyMode: true garantit que WHERE tenantId = X est toujours dans la requête. Règle critique du projet : zéro fuite cross-tenant.
3. PedagogicalAgent — adaptation de stratégie
Déterminisme pur. Évalue la maîtrise de l'apprenant dans le domaine spécifique et sélectionne l'une des 5 stratégies :
| Maîtrise | Stratégie | Comportement |
|---|---|---|
| < 0.3 | direct_instruction | Explication claire, exemples concrets, pas-à-pas |
| 0.3-0.5 | scaffolding | Indices progressifs, questions guidées simples |
| 0.5-0.7 | socratic | Questions menant à la découverte |
| 0.7-0.9 | guided_practice | Exercices avec feedback, application pratique |
| > 0.9 | challenge | Problèmes complexes, connexions entre concepts |
Ajustements supplémentaires par divergence quiz vs chat :
- Maîtrise élevée en chat + quiz faible → « compréhension superficielle » → nudge DOWN
- Maîtrise faible + quiz élevé → « apprenant discret » → nudge UP
- Taux de réussite au quiz < 40% → plafond à scaffolding (ne passe pas encore à socratic)
Ajuste également par âge (User.isMinor), style d'apprentissage, domaine (les mathématiques vs la littérature ont des profils différents).
Zéro coût LLM. Sortie : stratégie sélectionnée + instructions spécifiques à ajouter au system prompt.
4. Orchestrator — buildEnrichedPrompt
Monte le system prompt enrichi :
Vous êtes un tuteur IA pour le cours "Calcul I" de l'institution "Établissement XYZ".
MAÎTRISE DE L'APPRENANT :
- Limites : maîtrise 0.78 (élevée)
- Dérivées : maîtrise 0.42 (moyenne)
- Intégrales : maîtrise 0.15 (faible)
MISCONCEPTIONS ACTIVES :
- « L'apprenant confond domaine et image dans les fonctions » (3 occurrences, statut : en résolution)
- « L'apprenant applique la dérivée d'une somme à un produit » (5 occurrences, statut : actif)
PERFORMANCE AUX QUIZ :
- 14 tentatives au total, avgScore 67%, passRate 71%
- Zones faibles : intégrales (moy. 45%), règle de la chaîne (moy. 52%)
STRATÉGIE PÉDAGOGIQUE : guided_practice
- L'apprenant a une maîtrise moyenne des dérivées. Présentez des exercices graduels.
- Renforcez la connexion entre limites et dérivées (il maîtrise déjà les limites).
- Abordez proactivement la misconception sur la dérivée d'un produit.
CONTEXTE RAG (du matériel du cours) :
[Cours 3.2 "Règle du produit"] (Module : Calcul Différentiel)
"La dérivée de f(x).g(x) N'EST PAS f'(x).g'(x). La règle correcte est..."
[Cours 3.5 "Exercices résolus"] (Module : Calcul Différentiel)
"Exemple : dériver (x^2 + 1).(x - 3) en utilisant la règle du produit..."
QUIZ RÉCENT (prochaine conversation) :
L'apprenant vient de répondre à un quiz inline avec 2 questions, il en a réussi 1.
INSTRUCTIONS :
- Répondez en français
- Citez le matériel du cours lorsque c'est pertinent (utilisez [Cours X.Y])
- Reconnaissez ce que l'apprenant a réussi avant de pointer une erreur
- Pour cet âge (User.ageRange = "young_adult") : langage décontracté sans être trop informel
5. LLM principal — streaming avec fallback
const stream = await router.stream({
taskType: "chat_tutor",
messages: enrichedMessages,
options: { tenantId, userId, sessionId }
});
for await (const chunk of stream.textStream) {
yield chunk; // SSE vers le client
}
Le LLM Router effectue :
- Résout le provider via
TenantTaskModelConfig(l'admin a choisi Claude Sonnet, ou GPT-4o, etc.) - Résout la clé API via cascade : TenantApiKey → ProviderApiKey global → process.env
- Vérification du circuit breaker (état Redis). Si le provider est en OPEN : passe directement au fallback
- Middleware de metering : rate limit + vérification de crédit + calculateur de coût
- Stream Vercel AI SDK (prend en charge les tools, le multimodal, la sortie structurée)
- En cas d'erreur : fallback automatique vers le prochain provider dans la chaîne
Chaîne de fallback par tier :
Tier Sonnet (moyen) : Claude Sonnet → GPT-4o → Grok-3-fast → Gemini Pro
Tier Haiku (rapide) : Claude Haiku → GPT-4o-mini → Grok-3-mini → Gemini Flash
Tier Opus (complexe) : Claude Opus → GPT-4.5 → Grok-3 → Gemini 2.5 Pro
Le tenant ne se retrouve JAMAIS sans tuteur. Si Anthropic tombe → OpenAI prend le relais. Si OpenAI tombe aussi → xAI. Etc.
6. EvaluationAgent — feedback loop
En arrière-plan après la réponse du tuteur :
after(async () => {
const evaluation = await router.generateDirect({
taskType: "chat_evaluation",
messages: [
{ role: "user", content: "L'apprenant a dit : '...'. Le tuteur a répondu : '...'. Classifiez." }
]
});
// evaluation : {
// understanding: "partial",
// detectedMisconceptions: [{ description, concepts, severity }],
// suggestedNextStep: "..."
// }
// Met à jour ConceptMastery via Bayesian update
await conceptMasteryEngine.updateFromTurn({
userId, courseId, evaluation
});
// Persiste/met à jour les misconceptions
for (const misc of evaluation.detectedMisconceptions) {
await misconceptionResolutionService.upsert({
userId, source: "chat", ...misc
});
}
});
Coût : ~$0.001 par tour (Haiku). Ne bloque PAS la requête de l'apprenant.
Les misconceptions ont un cycle de vie en 3 états : active → resolving → resolved. La machine à états détermine les transitions basées sur les preuves (mise à jour de la maîtrise, réussite au quiz, tuteur a abordé explicitement).
7. ContentAgent — pré-génération proactive
after(async () => {
// L'apprenant démontre une faible maîtrise du concept X
// Pré-génère un exercice de suivi pendant que l'apprenant lit la réponse actuelle
const exercise = await router.generateDirect({
taskType: "content_generation",
messages: [...]
});
// Cache Redis 30min
await redis.set(`next-exercise:${userId}:${conceptId}`, exercise, 1800);
});
Quand l'apprenant finit de lire la réponse et dit « donne-moi un exercice », Studeia le sert INSTANTANÉMENT depuis le cache. Aucune latence perceptible.
Coût : ~$0.001 par tour (Haiku).
8. SupervisorAgent — modération
S'exécute en arrière-plan après chaque tour. Classifie en 5 niveaux de sévérité x 8 catégories.
Catégories : langage inapproprié, violence, illégal, sexuel, off_topic, harassment, self_harm, jailbreak_attempt.
Sévérité : low → medium → high → critical → safety.
3 infractions (LOW/MEDIUM en 7 jours) = mise en quarantaine 48h. CRITICAL = quarantaine 7 jours.
Self-harm (severity=safety) ne punit JAMAIS l'apprenant. Au lieu de cela :
- Tuteur interrompu avec un message d'accueil bienveillant
- Ressources de crise (France : numéro national prévention suicide 3114, SAMU 15)
- Cooldown Redis 24h (pas de quarantaine)
- Email URGENT immédiat à l'administrateur institutionnel
Philosophie : le self-harm est une crise, pas une infraction. Détails dans Safety Supervisor.
Coût : ~$0.001 par tour (Haiku).
Chiffres de production
Après 6 mois en production :
- ~30 ms de latence supplémentaire des agents pré-LLM
- ~$0.005-$0.05 coût moyen par tour
- 91% taux de rétention des apprenants après 7 jours (vs ~40% benchmark tuteur IA sans state)
- 3.2x taux de détection des misconceptions vs baseline single-call
- 0 incident grave de sécurité (catégories high/critical)
Compromis honnêtes
Ce qui n'a PAS fonctionné :
-
Nous avons essayé un « MasterAgent » coordinateur via LLM pour choisir dynamiquement le prochain agent. Le coût a doublé, la latence a augmenté de 800ms, la qualité ne s'est PAS améliorée. Nous sommes retournés au déterminisme dans l'Orchestrator.
-
Nous avons essayé le fine-tuning de Llama sur le matériel des cours. Coûteux pour chaque tenant. Le RAG fonctionne mieux pour les connaissances dynamiques (l'institution met à jour son matériel chaque semaine — le fine-tune deviendrait obsolète).
-
Nous avons essayé le « consensus » entre 3 LLMs (Claude + GPT + Gemini) et pris la réponse majoritaire. Coût 3x sans gain de qualité significatif. Supprimé — la chaîne de fallback est suffisante.
Open source ?
Nous évaluons l'open-source des composants déterministes (StudentModelService, RetrievalAgent, PedagogicalAgent) sous forme de package npm. Les agents pilotés par LLM (Evaluation, Content, Supervisor) ont des prompts qui sont la propriété intellectuelle de Studeia et restent fermés.
Si cela vous intéresse : ouvrez une issue sur github.com/donattocosta-lang/studeia/issues.