Kajan Siva
← Blog
IA appliquée

Comment j'ai construit un tuteur IA pour ma fille avec LangGraph

23 juin 2026·10 min de lecture

Je suis persuadé qu'on peut utiliser l'IA pour créer une expérience d'apprentissage personnalisée. C'est ce que j'ai commencé à expérimenter avec un tuteur IA pour mes enfants.

Point de départ

Ce projet est né de deux objectifs :

  • J'étais curieux de voir comment je pouvais aider mes enfants avec l'IA.
  • Je voulais apprendre à créer de vrais agents production-ready.

Jusqu'à présent j'avais expérimenté pas mal de choses sur les bases des LLMs, le tool calling et le RAG. Mais je n'avais pas encore créé de vrais agents.

On entend beaucoup de buzz sur le sujet. J'avais besoin de mettre les mains dans le cambouis avec un vrai projet. Pour comprendre ce que sont vraiment les agents, dans quels cas ils sont utiles et comment les créer.

Dans cet article, je te détaille le cheminement par lequel je suis passé pour créer ce projet. Et surtout ce que j'ai appris au passage.

L'idée

Au début, il y avait une idée.

Je voulais un tuteur IA que mes enfants, qui sont en primaire, puissent utiliser de manière autonome.
Niveau fonctionnalité, ils doivent être capables d'ajouter des leçons en uploadant des photos. Et ensuite, de réviser ces leçons grâce au tuteur IA.

La contrainte principale est que le tuteur doit avoir une mémoire long terme. Au fur et à mesure des séances de révisions, il doit se rappeler quels concepts sont déjà maîtrisés et lesquels sont encore bancals.

Au niveau de l'approche pédagogique, il doit utiliser une méthode socratique. Pour ne pas donner les réponses, mais plutôt guider l'élève progressivement.

Le fonctionnement final ressemble à ça actuellement :

Le tuteur IA en action
Le tuteur IA en action

Workflow vs agent

Mon premier apprentissage a été de savoir où placer le curseur entre workflow et agent autonome.
D'un côté, tu as les workflows, qui sont principalement un enchaînement d'interactions avec les LLMs. Très cadré pour un use case précis.
De l'autre, tu as les agents autonomes. Tu donnes des outils à des agents et tu les laisses se débrouiller pour trouver la solution optimale à chaque requête.

Comparaison workflow vs agent

Quand tu écoutes les "experts" sur LinkedIn, tu as l'impression que les agents autonomes sont la solution à tout. C'est loin d'être le cas.
Tu risques de rencontrer des problèmes sérieux en prod si tu prends cette approche de manière aveugle. Thibaut Bardout a fait un article très intéressant sur le sujet.

Après en avoir discuté avec Adrien Maret, j'ai décidé de partir sur une solution hybride. Avec un système agentique qui mélange des workflows et des phases où l'agent sera plus libre de ses actions.

Ajouter une leçon ou modifier la mémoire long terme sont les parties qui vont adopter une approche workflow. Alors que le coeur du flow de révision s'appuiera plus sur la souplesse laissée à l'agent. Une souplesse encadrée, comme on le verra.

Quel framework choisir ?

Une fois que j'étais au clair sur l'idée et la philosophie globale, il me restait encore à choisir le framework à utiliser.
J'ai comparé les solutions suivantes :

La solution maison est celle que j'ai écartée en premier. Créer un agent nécessite de travailler sur plein d'aspects que tu ne réalises pas forcément au début : la mémoire, la persistance, l'observabilité, ...
C'est totalement contreproductif de tout faire from scratch.

Mastra et Vercel AI SDK semblent plus faciles à prendre en main que LangGraph. Mais ils sont plus limités en termes de personnalisation. Et surtout moins recherchés comme compétences par les entreprises.
LangGraph a une bonne notoriété dans l'écosystème et j'ai bien aimé la flexibilité qu'il apporte. Je suis donc parti dessus.

Par contre, LangGraph couvre la partie backend. En complément de ça, j'utilise une partie de la lib AI SDK de Vercel, côté front.
Elle permet de créer plus facilement des interfaces IA. Notamment avec les features de streaming et de generative UI.

Le mental model de LangGraph

Avant de te présenter plus en détail comment j'ai modélisé le système agentique de mon projet, prenons du recul sur le fonctionnement et la manière de penser les projets LangGraph.

Les notions de base

LangGraph repose sur différents concepts-clés :

  • Le state : Un état persisté qui évolue au cours des interactions avec l'agent. On y retrouve généralement le tableau des messages ainsi que des propriétés nécessaires au traitement.
  • Les noeuds (nodes) : Les fonctions qui permettent de modifier le state. Que ce soit par un appel à un LLM, une logique interne, ou une interaction avec le monde extérieur.
  • Les arêtes (edges) : Les logiques de routing qui déterminent le prochain noeud que l'agent doit exécuter. On y retrouve des chemins fixes et prédéfinis. Et d'autres chemins, conditionnels, basés sur le contenu du state.

Concrètement, voici le state de mon agent. Chaque propriété y déclare son reducer (comment elle évolue) et sa valeur par défaut :

apps/backend/src/graphs/router.graph.ts
export const RouterState = Annotation.Root({
  // Le fil de conversation : un reducer qui APPEND les nouveaux messages.
  ...MessagesAnnotation.spec,
  // La phase de routing : un scalaire, on garde la dernière valeur.
  phase: Annotation<RoutingPhase>({
    reducer: (_, next) => next,
    default: () => "idle",
  }),
  // Le curseur sur la file de concepts de la séance.
  conceptCursor: Annotation<number>({
    reducer: (_, next) => next,
    default: () => 0,
  }),
  // … intent, lessonId, sessionConceptIds, etc.
});

À chaque interaction de l'utilisateur on repasse dans le graphe, du noeud START au noeud END (sauf reprise après une interruption, qu'on verra plus bas). En suivant un chemin différent en fonction du contexte.

Le fonctionnement général de LangGraph
Le fonctionnement général de LangGraph

La persistance

L'autre particularité de LangGraph est au niveau de la persistance. Chaque thread d'exécution d'un agent peut correspondre à plusieurs échanges avec les utilisateurs. On a donc besoin de persister le détail de tous ces échanges, les résultats générés par les noeuds précédents. Et de pouvoir retrouver toutes ces infos rapidement au prochain appel du frontend.

Pour ça, LangGraph fournit la fonctionnalité de checkpointer. Qui permet de stocker ces informations, indexées sur l'id du thread. Dans mon projet j'ai par exemple décidé de stocker ces informations dans ma base PostgreSQL.

Cette technique allège aussi le travail côté client. Le backend ne fait avancer l'agent qu'avec le dernier message : le checkpointer fournit tout le reste de l'historique du thread.

Les interruptions

Au-delà du fait que le thread peut se dérouler sur plusieurs interactions de l'utilisateur, on peut aussi dans certains cas mettre en pause l'exécution du graphe en attendant un input de l'utilisateur.

C'est le mécanisme qui permet le human-in-the-loop : insérer un point de décision humain au milieu d'un flow autrement automatisé. On s'en sert quand on ne veut pas que l'agent tranche seul, typiquement avant une action sensible ou difficile à annuler.

Dans mon projet, je l'ai appliqué sur un cas particulier d'ingestion de leçons. Si on détecte que la leçon uploadée est potentiellement un doublon, on demande une action explicite de l'élève avant de continuer.
Niveau UX, j'en profite pour désactiver le champ texte et afficher des boutons pour forcer un choix.

Un flow d'interruption
Un flow d'interruption

Dans le code, c'est la fonction interrupt() de LangGraph qui met le graphe en pause et attend la réponse de l'élève :

apps/backend/src/graphs/ingest.ts
export function confirmOverwriteNode(state: IngestState) {
  const payload: ConfirmOverwrite = {
    kind: "confirm_overwrite",
    title: state.collision.title,
    options: [
      { label: "Remplacer", choice: "replace" },
      { label: "Garder les deux", choice: "keep_both" },
      { label: "Annuler", choice: "cancel" },
    ],
  };
  // Met le graphe en pause ; au resume, interrupt() renvoie le choix de l'élève.
  const choice = interrupt<ConfirmOverwrite, OverwriteChoice>(payload);
  return { overwriteChoice: choice };
}

Le state graph de mon agent

Maintenant que tu sais comment LangGraph fonctionne, regardons comment je l'ai appliqué à mon projet.

La vue globale

J'ai deux flows principaux :

  • Celui d'ingestion de leçon. Pour importer de nouvelles leçons dans le système.
  • Celui de la révision. Pour faire une nouvelle session de révision sur une des leçons connues.

À côté de ceux-là, on a deux autres flows mineurs pour cadrer l'agent :

  • Celui pour gérer les hors-sujets. Pour que l'élève reste concentrée sur ses révisions au lieu de commencer à parler de One Piece.
  • Celui de clarification. Un flow fallback qui va demander des clarifications si on n'a pas réussi à déterminer l'intention de l'élève.

Je te présente la vue globale de ce graphe et on rentrera plus en détails sur ces différentes parties 👇🏽

Le graphe de mon tuteur IA
Le graphe de mon tuteur IA

La classification

Vu qu'on rentre dans le graphe à chaque interaction de l'élève, on doit d'abord déterminer à quel endroit du graphe on reprend.

Pour ça, je combine deux techniques.
J'ai une propriété phase qui me permet de faire un routing déterministe. Par exemple, quand on est dans la phase de révision.
Ensuite, j'ai un noeud de classification, qui va déterminer le chemin le plus approprié en fonction des derniers messages échangés, avec un appel LLM.

Quand on est sur le premier échange, on passe forcément par le noeud de classification. Mais quand on est en plein dans la phase de révision, on doit la contourner.
Si on est en pleine phase de révision et qu'on vient de donner la réponse 18 juin 1815, il ne faut surtout pas repasser par la classification : isolée, elle risquerait de prendre cette réponse pour une nouvelle intention (un hors-sujet, par exemple) au lieu d'une réponse attendue. La propriété phase court-circuite ce risque et envoie directement la réponse vers le bon noeud pour l'évaluer.

Ce choix d'entrée tient dans un switch sur la phase :

apps/backend/src/graphs/revise.ts
// Au START : on lit la phase pour savoir où entrer dans le graphe.
export function routeStart(state: { phase: RoutingPhase }) {
  switch (state.phase) {
    case "revising":        return "evaluate";      // en pleine révision → on évalue la réponse
    case "entering_revise": return "revise";        // un chip a fixé la leçon
    case "choosing_lesson": return "resolveLesson"; // on attend le choix de leçon
    case "idle":            return "classify";      // sinon, on classe l'intention
  }
}

Le flow d'ingestion de leçons

Le premier flow principal. Celui qui permet d'ajouter des leçons dans le système.
D'un point de vue fonctionnel, l'élève doit pouvoir uploader des photos de ses leçons. À partir de là, on passe par différentes étapes et appels aux LLMs pour lire et extraire le contenu des photos, détecter des doublons et l'enregistrer en base.

L'extraction ne se contente pas de recopier le texte. Le même appel découpe la leçon en concepts : ses unités enseignables (un événement, une date, une cause). Et chaque concept reçoit une barre de précision : faut-il restituer un fait exact comme une date, une explication avec ses propres mots, ou seulement l'idée générale ?
Ce sont ces concepts, et non la leçon en bloc, que la mémoire suivra et que la révision viendra travailler un à un.

Par rapport à notre discussion initiale sur workflow vs agent autonome, on voit ici qu'on alterne entre des noeuds déterministes et des noeuds LLMs. C'est ce qui permet de réduire le risque sur les interactions où on a besoin d'une forte garantie, comme le fait de stocker en base la leçon.

Le flow de révision

C'est le coeur du système. C'est aussi là que la mémoire long terme paie.

  • On hydrate la séance. On charge tout ce qui personnalise l'échange : profil de l'élève, contenu de la leçon, et son niveau de maîtrise concept par concept.
  • On choisit quoi réviser, pas comment l'interroger. À partir de la maîtrise connue, on sélectionne de façon déterministe une file de concepts, en priorité aux lacunes : d'abord les concepts jamais vus, puis ceux encore fragiles, puis ceux maîtrisés mais qu'il est temps de revoir (répétition espacée). On plafonne à cinq concepts par séance. Le reste est reporté à plus tard. C'est cette file qu'on stocke dans le state, pas les questions.
  • On déroule la boucle socratique, concept par concept. Pour chaque concept, l'agent formule ses questions à la volée. Plusieurs allers-retours sont possibles : quand l'élève bloque, l'agent donne des indices progressifs, sans jamais livrer la réponse. Un noeud d'évaluation tranche après chaque réponse : concept acquis, ou on continue. Avec une limite de tentatives pour ne jamais s'enliser.
  • On capitalise au fil de l'eau. Dès qu'un concept est traité de bout en bout, on écrit la mémoire de maîtrise. On n'attend pas la fin de la séance : même interrompue, elle laisse une trace.
  • On clôt par une analyse de séance. Une dernière passe relit tout le dialogue pour affiner, si c'est justifié, le profil d'apprentissage de l'élève : ce qui la motive, ce qui la fait décrocher, comment elle apprend le mieux.

En code, les trois priorités s'empilent, puis on plafonne :

apps/backend/src/graphs/revise.ts
// Sélection déterministe des concepts à réviser : les lacunes d'abord.
export function selectConcepts(concepts: HydratedConcept[], now: Date, max = 5) {
  const unknown = concepts.filter((c) => c.mastery === null);          // jamais vus
  const weak = concepts.filter((c) =>                                  // encore fragiles
    c.mastery?.level === "emerging" || c.mastery?.level === "developing");
  const staleSecure = concepts.filter((c) =>                           // acquis mais à revoir
    c.mastery?.level === "secure" &&
    isSecureDue(c.mastery.lastReviewedAt, c.mastery.reviewStep, now)); // répétition espacée
  return [...unknown, ...weak, ...staleSecure].slice(0, max);
}

La gestion des cas imprévus

Pour finir, on a les deux flows pour gérer les cas à la marge.

Quand on n'arrive pas à comprendre ce que l'élève demande dans notre contexte, on renvoie simplement un message de clarification.

Quand l'élève pose une question hors sujet, on la recadre gentiment pour lui rappeler que cet agent est là pour l'aider à réviser.

Ces gestions de cas à la marge sont importantes pour garantir une bonne expérience et pour optimiser les coûts. Pour éviter qu'un agent de révision ne soit transformé en ChatGPT qui répond à tout.

Prochaines étapes

Où est-ce que j'en suis avec tout ça ?

J'ai fini d'implémenter la version MVP de ce projet, centré sur une matière : l'histoire. La prochaine étape va être de laisser ma fille, qui est en CM2, le tester pendant quelques semaines.
Ensuite, je verrai comment itérer sur le projet (l'adapter à d'autres matières, donner plus d'outils à la boucle agentique de révision, ajouter une interface vocale, ...)

Côté contenu, dans cet article je me suis concentré sur la partie state graph. Il y a encore d'autres aspects qui mériteraient des articles dédiés (la gestion de la mémoire, l'observabilité, les evals, ...).
Si ce projet te parle, dis-moi sur LinkedIn les sujets que tu voudrais que j'aborde dans les prochains articles.