GRAAL : Graph-based Research with Agents for Automatic Labelling

Classification automatique dans des nomenclatures statistiques par approche agentique

Meilame Tayebjee

Insee, SSP Lab

Théo Ferry

Insee, SSP Lab

2 April 2026

Plan

  1. Contexte. Défis de la codification automatique
  2. Le framework GRAAL. Architecture multi-agents
  3. Perspectives. Développements futurs

1️⃣ Contexte: le cas d’usage de la codification automatique

  • NAF : Nomenclature statistique des Activités économiques (NAF)
    • ~700 codes finaux
    • Structure hiérarchique sur 5 niveaux
  • COICOP : Classification of Individual Consumption by Purpose
    • ~300 à ~1000 selon la granularité employée
    • Structure hiérarchique sur 5 niveaux

graph TD
  %% Sections
  S_A["Section A<br/>Agriculture, sylviculture et pêche"]
  S_C["Section C<br/>Industrie manufacturière"]

  %% Divisions
  D_01["Division 01<br/>Culture et production animale"]
  D_10["Division 10<br/>Industries alimentaires"]

  %% Groupes
  G_01_1["Groupe 01.1<br/>Cultures non permanentes"]
  G_10_1["Groupe 10.1<br/>Transformation et conservation de la viande"]

  %% Classes
  C_01_11["Classe 01.11<br/>Culture de céréales"]
  C_10_11["Classe 10.11<br/>Transformation de la viande"]

  %% Relations
  S_A --> D_01
  S_C --> D_10

  D_01 --> G_01_1
  D_10 --> G_10_1

  G_01_1 --> C_01_11
  G_10_1 --> C_10_11

Défis

  • A l’heure actuelle, apprentissage supervisé classique, avec des modèles de deep learning (PyTorch)
    • Frugal, performant et rapide, production-ready… 😎
    • … mais nécessite entre 100k et 1M de données labellisées 🫨

Une approche MLOps grandement facilitée par Onyxia mais encore incomplète

  • Data : Stockage S3 (Datalab), versionnage avec MLFlow Datasets ✅
  • Modèle : développement internalisé et distribution de l’architecture via le package
    torchTextClassifiers, entraînement parallélisé avec Argo Workflows, model storage avec MLFlow ✅
  • Déploiement : FastAPI déployé via conteneurisation (Docker) ✅
  • Monitoring : c’est là que le bât blesse ❌
    • Investissements humains nécessaires, pas toujours faits
    • Comment contrôler le modèle en production sans annotation humaine continue ?

Le besoin de s’affranchir des données labellisées

  • Comment faire quand on ne peut pas avoir / n’a pas assez de données labellisées ?
  • Comment faire quand la nomenclature est mise à jour par le métier ?
    • Il faut recoder les données existantes
  • Comment faire quand nos données labellisées ne sont pas fiables ?
    • et qu’idéalement on veuille les corriger…
  • Une première approche zero-shot : le Retrieval-Augmented Generation (RAG)
    • Knowledge base = les notices officielles pour chaque code
    • Très bonne première solution mais…
      • dépendance au chunking…
      • dépendance au modèle d’embedding…
      • saturation du contexte avec les notices…
      • on compare embedding de notice à un embedding de libellé
      • pas de traçabilité, pas de raisonnement…
      • pas de prise en compte de la hiérarchie !

Objectifs

  • On ne veut plus d’embeddings
  • On veut du reasoning: le LLM peut/doit justifier ses choix (chain of thought)
    • Plutôt que lui imposer du contexte comme dans un RAG, laisser le modèle compléter son contexte dynamiquement (= agent !)
    • Idéalement, avec un raisonnement autour de la hiérarchie

2️⃣ Le framework GRAAL

Neo4j : une graph database

graph TD
  ROOT["Root"]

  %% Sections
  J["CODE: J<br/>LEVEL: 1"]
  A["CODE: A<br/>LEVEL: 1"]

  %% Divisions
  J60["CODE: J60<br/>LEVEL: 2"]
  J61["CODE: J61<br/>LEVEL: 2"]
  A01["CODE: A01<br/>LEVEL: 2"]

  %% Groupes
  J60_1["CODE: J60.1<br/>LEVEL: 3"]
  J60_2["CODE: J60.2<br/>LEVEL: 3"]
  A01_1["CODE: A01.1<br/>LEVEL: 3"]

  %% Classes
  J60_11["CODE: J60.11<br/>LEVEL: 4"]
  J60_12["CODE: J60.12<br/>LEVEL: 4"]
  A01_11["CODE: A01.11<br/>LEVEL: 4"]

  %% Relations HAS_PARENT
  J -->|HAS_PARENT| ROOT
  A -->|HAS_PARENT| ROOT

  J60 -->|HAS_PARENT| J
  J61 -->|HAS_PARENT| J
  A01 -->|HAS_PARENT| A

  J60_1 -->|HAS_PARENT| J60
  J60_2 -->|HAS_PARENT| J60
  A01_1 -->|HAS_PARENT| A01

  J60_11 -->|HAS_PARENT| J60_1
  J60_12 -->|HAS_PARENT| J60_1
  A01_11 -->|HAS_PARENT| A01_1

Utilisation du graph comme support pour des agents

  • On définit un ensemble d’outils commun à tous les agents:
    • get_code_information
    • get_children
    • get_descendants
    • get_siblings
    • etc.
  • Ils utilisent tous la syntaxe Neo4j

Les agents

  • Un agent = un prompt, un type d’input et d’output, et des tools
  • Les closers
    • CodeChooser:

    • MatchVerifier:

      Code
      class MatchVerifier(BaseAgent):
          def __init__(self, graph: Graph):
              super().__init__(graph)
      
          def get_agent_name(self) -> str:
              return "MatchVerifier Agent"
      
          def get_instructions(self) -> str:
              return """
                      Tu es un agent spécialisé dans la vérification de la validité d'une correspondance entre un libellé textuel et le code qui lui a été associé. Tu as accès à des outils pour interroger une base de données de nomenclature statistique structurée en graph database Neo4j.
                  """
      
          def get_output_type(self):
              return MatchVerificationResult
      
          def build_prompt(self, match_verification_input: MatchVerificationInput) -> str:
              """
              Construire le prompt pour l'agent de vérification de correspondance.
              """
              prompt = f"""
              Vérifie si le code suivant correspond bien à l'activité décrite.
      
              Activité : {match_verification_input.activity}
      
              Code proposé : {match_verification_input.code}
              Explication proposée : {match_verification_input.proposed_explanation}
      
              Réponds en fournissant :
              1. Un booléen indiquant si la correspondance est valide.
              2. Un niveau de confiance entre 0 et 1.
              3. Une explication concise de ta décision.
              """
              return prompt
  • Text2Code (classifieur)
    • le Navigator est un agent qui hérite de Text2Code
  • Code2Text (générateur de données synthétiques)

Exemple de classification par Navigator

description = """
Entreprise de fabrication artisanale de pains spéciaux 
et viennoiseries, avec vente directe en boutique
"""
{
  "code": "10.71Z",
  "label": "Fabrication de pain et pâtisserie fraîche",
  "confidence": 0.94,
  "path": ["C", "10", "10.7", "10.71", "10.71Z"],
  "justification": "Fabrication artisanale (C) de produits 
                    de boulangerie (10.7) avec vente directe : ce code correspond donc au libellé fourni."
}

Evaluation

  • Evaluer un agent classifieur
    • Si on fait confiance à notre dataset de test : évaluation straightforward
    • Sinon : évaluation manuelle !
      • Prédiction ground truth, prédiction du classifieur, jugement du CodeChooser et du MatchVerifier mis bout à bout
      • Evaluation conjointe de tous les composants
  • Evaluer la génération de données synthétiques
    • moins évident
    • réentraîner un modèle sur un dataset synthétique et voir si on atteint des perfs équivalentes sur le même test set

Perspectives

  • Un cahier des charges respecté
    • Plus d’embeddings
    • Du raisonnement et de la traçabilité
    • Utilisable pour n’importe quelle nomenclature : il suffit de créer une base Neo4j avec les notices et tout fonctionne !
      • GRAAL n’est qu’un distributeurs de tools, de prompts et de structures d’appels au LLM (validation input/output)
  • La tâche principale va être l’évaluation 🎯
  • On va en faire quoi ?
    • Un modèle agentique est trop lourd pour la production (pas encore de GPU, temps d’inférence trop élevé…)
    • Cependant, GRAAL peut être utilisé pour:
      • Monitoring du modèle déployé: MatchVerifier analyse les logs de l’API et évalue le modèle en continu, appelle le Navigator si besoin de recoder…
      • Recodage d’une base (même workflow que le monitoring)
      • Génération de données synthétiques (Code2Text)

Merci de votre attention !

🔗 github.com/InseeFrLab/GRAAL