Por qué un pipeline multi-agente
Cuando empezamos el tutor IA de Studeia, la tentación fue obvia: llamar a la API de Claude con un system prompt largo y los mensajes del alumno. Funciona en demos. Falla en producción.
Cuatro problemas estructurales:
-
Sin memoria persistente del alumno — cada turno trata al alumno desde cero. ¿El alumno acaba de fallar el concepto X 3 veces en quizzes? El LLM no lo sabe. El tutor repite la explicación básica que ya dio la semana pasada.
-
Sin grounding en el material del curso — la institución tiene apuntes, slides, transcripciones de clases en video. El LLM puro no accede a ellos. Inventa hechos. Cita conceptos erróneos.
-
Sin moderación real — un system prompt con "sé seguro" es débil. Aparece un alumno en sufrimiento mental. Un alumno intenta jailbreak. Un alumno usa lenguaje inadecuado. El tutor necesita reaccionar adecuadamente sin desactivar la funcionalidad para alumnos legítimos.
-
Sin feedback loop — el sistema no aprende. Los mismos misconceptions se repiten. El profesor no tiene visibilidad de los puntos débiles colectivos de la clase.
Solución: separar responsabilidades en agentes especializados.
La arquitectura
Mensaje del alumno
↓
PRE-LLM (síncrono, cero costo LLM)
1. StudentModelService.getSnapshot()
2. RetrievalAgent.retrieve()
3. PedagogicalAgent.select()
4. buildEnrichedPrompt()
↓
LLM PRINCIPAL (streaming, SSE al cliente)
5. router.stream() con fallback automático
Claude Sonnet → GPT-4o → Grok-3 → Gemini Pro
↓
POST-LLM (background via after(), fire-and-forget)
6. EvaluationAgent (Haiku)
7. ContentAgent (Haiku)
8. SupervisorAgent (Haiku)
Vamos por cada uno.
1. StudentModelService — el "perfil cognitivo"
Antes de cualquier llamada LLM, cargamos el snapshot del alumno:
const snapshot = await StudentModelService.getSnapshot({
userId,
courseId,
});
// Retorna:
{
conceptMastery: Map<conceptId, { probability, confidenceInterval }>,
misconceptions: Misconception[], // activas + resolving
episodicMemory: Episode[], // qué funcionó antes
quizContext: {
totalAttempts,
avgScore,
passRate,
weakAreas: string[] // conceptos con mastery < 0.4
},
recentHistory: Message[] // sliding window 10 msgs
}
ConceptMastery usa distribución Beta bayesiana — cada concepto tiene alpha (successes) + beta (failures). Probabilidad = alpha / (alpha + beta). Intervalo de confianza via percentiles 5% y 95%.
EpisodicMemory registra insights pedagógicos: "la analogía de la pizza funcionó para explicar fracciones", "la metáfora del caño de agua falló para electricidad". El sistema aprende qué funciona con cada alumno.
Cero costo LLM. Todo son queries Prisma + cálculo determinístico.
2. RetrievalAgent — RAG tenant-scoped
En lugar de que el LLM intente recordar hechos sobre matemáticas, biología o historia, dejamos que cite el material de la propia institución.
const chunks = await retrieve({
query: reformulatedQuery, // 1. reformula la query con contexto
filters: { tenantId, courseId }, // 2. aislamiento absoluto
k: 10,
tenantOnlyMode: true, // 3. nunca cita contenido de otra institución
boostByWeakAreas: snapshot.quizContext.weakAreas, // 4. prioriza chunks de áreas débiles
});
El RAG per-tenant es crítico. El preuniversitario XYZ tiene material propio sobre el examen de ingreso. La Universidad ABC tiene material propio sobre Cálculo. El tutor cita el material CORRECTO de la institución, no un agregado genérico.
Cada chunk tiene metadata: { source: "course_lesson", courseId, lessonId, lessonTitle, moduleTitle }. Cuando el tutor responde, cita: "Como se explicó en la clase 'Geometría Analítica' del módulo 3..."
Voyage AI genera los embeddings (1024 dimensiones, fallback OpenAI). pgvector almacena. tenantOnlyMode: true garantiza que WHERE tenantId = X está siempre en la query. Regla crítica del proyecto: cero leakage cross-tenant.
3. PedagogicalAgent — adaptación de estrategia
Determinismo puro. Evalúa el mastery del alumno en el dominio específico y selecciona una de 5 estrategias:
| Mastery | Estrategia | Comportamiento |
|---|---|---|
| < 0.3 | direct_instruction | Explicación clara, ejemplos concretos, paso a paso |
| 0.3-0.5 | scaffolding | Pistas progresivas, preguntas guiadas simples |
| 0.5-0.7 | socratic | Preguntas que llevan al descubrimiento |
| 0.7-0.9 | guided_practice | Ejercicios con feedback, aplicación práctica |
| > 0.9 | challenge | Problemas complejos, conexiones entre conceptos |
Ajustes adicionales por divergencia entre quiz y chat:
- Mastery alta en el chat + quiz bajo → "comprensión superficial" → nudge DOWN
- Mastery baja + quiz alto → "alumno callado" → nudge UP
- Quiz pass rate < 40% → cap en scaffolding (todavía no avanza a socratic)
También ajusta por edad (User.isMinor), estilo de aprendizaje, dominio (matemáticas vs literatura tienen perfiles distintos).
Cero costo LLM. Output: estrategia seleccionada + instrucciones específicas para agregar al system prompt.
4. Orchestrator — buildEnrichedPrompt
Construye el system prompt enriquecido:
Eres un tutor IA para el curso "Cálculo I" de la institución "Preuniversitario XYZ".
DOMINIO DEL ALUMNO:
- Límites: mastery 0.78 (alto)
- Derivadas: mastery 0.42 (medio)
- Integrales: mastery 0.15 (bajo)
MISCONCEPTIONS ACTIVAS:
- "El alumno confunde dominio con imagen en funciones" (3 ocurrencias, estado: resolving)
- "El alumno aplica la derivada de la suma al producto" (5 ocurrencias, estado: active)
RENDIMIENTO EN QUIZ:
- 14 intentos en total, avgScore 67%, passRate 71%
- Áreas débiles: integrales (prom. 45%), regla de la cadena (prom. 52%)
ESTRATEGIA PEDAGÓGICA: guided_practice
- El alumno tiene dominio medio en derivadas. Presenta ejercicios graduales.
- Refuerza la conexión entre límites y derivadas (ya domina los límites).
- Aborda proactivamente el misconception sobre la derivada de un producto.
CONTEXTO RAG (del material del curso):
[Clase 3.2 "Regla del Producto"] (Módulo: Cálculo Diferencial)
"La derivada de f(x).g(x) NO es f'(x).g'(x). La regla correcta es..."
[Clase 3.5 "Ejercicios Resueltos"] (Módulo: Cálculo Diferencial)
"Ejemplo: derivar (x^2 + 1).(x - 3) usando la regla del producto..."
QUIZ RECIENTE (próxima conversación):
El alumno acaba de responder un quiz inline con 2 preguntas, acertó 1.
INSTRUCCIONES:
- Responde en español
- Cita el material del curso cuando sea relevante (usa [Clase X.Y])
- Reconoce lo que el alumno acertó antes de señalar el error
- Para esta edad (User.ageRange = "young_adult"): lenguaje casual sin ser demasiado informal
5. LLM principal — streaming con 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 al cliente
}
El LLM Router hace:
- Resuelve el proveedor via
TenantTaskModelConfig(el admin eligió Claude Sonnet, GPT-4o, etc.) - Resuelve la API key via cascada: TenantApiKey → ProviderApiKey global → process.env
- Circuit breaker check (estado en Redis). Si el proveedor está en OPEN: salta directo al fallback
- Middleware de metering: rate limit + credit check + cost calculator
- Stream con Vercel AI SDK (soporta tools, multimodal, structured output)
- En caso de error: fallback automático al siguiente proveedor en la cadena
Cadena de fallback por tier:
Tier Sonnet (medio): Claude Sonnet → GPT-4o → Grok-3-fast → Gemini Pro
Tier Haiku (rápido): Claude Haiku → GPT-4o-mini → Grok-3-mini → Gemini Flash
Tier Opus (complejo): Claude Opus → GPT-4.5 → Grok-3 → Gemini 2.5 Pro
El tenant NUNCA se queda sin tutor. Si Anthropic cae → OpenAI toma el relevo. Si OpenAI también cae → xAI. Etc.
6. EvaluationAgent — feedback loop
En background después de la respuesta del tutor:
after(async () => {
const evaluation = await router.generateDirect({
taskType: "chat_evaluation",
messages: [
{ role: "user", content: "El alumno dijo: '...'. El tutor respondió: '...'. Clasifica." }
]
});
// evaluation: {
// understanding: "partial",
// detectedMisconceptions: [{ description, concepts, severity }],
// suggestedNextStep: "..."
// }
// Actualiza ConceptMastery via Bayesian update
await conceptMasteryEngine.updateFromTurn({
userId, courseId, evaluation
});
// Persiste/actualiza misconceptions
for (const misc of evaluation.detectedMisconceptions) {
await misconceptionResolutionService.upsert({
userId, source: "chat", ...misc
});
}
});
Costo: ~$0.001 por turno (Haiku). NO bloquea el request del alumno.
Los misconceptions tienen un ciclo de vida de 3 estados: active → resolving → resolved. La máquina de estados determina las transiciones basándose en evidencia (actualización de mastery, aprobación de quiz, abordaje explícito por parte del tutor).
7. ContentAgent — pre-generación proactiva
after(async () => {
// El alumno demuestra dominio débil en el concepto X
// Pre-genera un ejercicio de seguimiento mientras el alumno lee la respuesta actual
const exercise = await router.generateDirect({
taskType: "content_generation",
messages: [...]
});
// Cache Redis 30min
await redis.set(`next-exercise:${userId}:${conceptId}`, exercise, 1800);
});
Cuando el alumno termina de leer la respuesta y dice "dame un ejercicio", Studeia lo sirve INSTANTÁNEAMENTE desde el cache. Sin latencia perceptible.
Costo: ~$0.001 por turno (Haiku).
8. SupervisorAgent — moderación
Corre en background después de cada turno. Clasifica en 5 niveles de severidad x 8 categorías.
Categorías: lenguaje inapropiado, violencia, ilegal, sexual, off_topic, harassment, self_harm, jailbreak_attempt.
Severidad: low → medium → high → critical → safety.
3 strikes (LOW/MEDIUM en 7 días) = cuarentena 48h. CRITICAL = cuarentena 7 días.
Self-harm (severity=safety) NUNCA penaliza al alumno. En cambio:
- El tutor se interrumpe con un mensaje de acogida
- Recursos de crisis (líneas de ayuda locales según el país del tenant)
- Cooldown Redis 24h (no cuarentena)
- Email URGENTE inmediato al admin institucional
Filosofía: self-harm es una crisis, no una infracción. Detalles en Safety Supervisor.
Costo: ~$0.001 por turno (Haiku).
Números en producción
Después de 6 meses en producción:
- ~30 ms de latencia adicional de los agentes pre-LLM
- ~$0.005-$0.05 costo promedio por turno
- 91% tasa de retención de alumnos después de 7 días (vs ~40% benchmark de tutor IA sin estado)
- 3.2x tasa de detección de misconceptions vs baseline de llamada única
- 0 incidentes graves de safety (categorías high/critical)
Trade-offs honestos
Cosas que NO funcionaron:
-
Intentamos un "MasterAgent" coordinador via LLM para elegir el siguiente agente dinámicamente. El costo se duplicó, la latencia subió 800ms, la calidad NO mejoró. Volvimos al determinismo en el Orchestrator.
-
Intentamos hacer FineTune de Llama con material de cursos. Caro para cada tenant. RAG funciona mejor para knowledge dinámico (la institución actualiza el material cada semana — el fine-tune quedaría stale).
-
Intentamos "consensus" entre 3 LLMs (Claude + GPT + Gemini) y tomar la respuesta con mayoría. Costo 3x sin ganancia significativa de calidad. Lo eliminamos — la cadena de fallback es suficiente.
¿Código abierto?
Estamos evaluando hacer open-source de los componentes determinísticos (StudentModelService, RetrievalAgent, PedagogicalAgent) como paquete npm. Los agentes LLM-driven (Evaluation, Content, Supervisor) tienen prompts que son IP de Studeia y permanecerán cerrados.
Si te interesa: abre un issue en github.com/donattocosta-lang/studeia/issues.