Le problème que l'on ne voit pas venir
Quand on connecte un LLM à un design system, il peut malheureusement halluciner des composants. Classes CSS inventées, HTML fantaisiste, balises qui n'existent nulle part dans votre code. Sur réfugiés.info — plateforme gouvernementale, des milliers utilisateurs, 8 langues — nous avons résolu ce problème en n'apprenant pas le design system au LLM. Nous lui avons donné simplement 3 directives markdown (spoiler, c'est facile à mettre en place). Le reste, c'est le pipeline qui s'en charge.
Mais reprenons depuis le début.
Réfugiés.info aide des centaines de milliers de personnes chaque année à naviguer au sein de la jungle des démarches administratives françaises pour un public primo-arrivant. L'équipe éditoriale maintient des centaines de fiches de procédures, traduites et mises à jour en permanence.
Quand nous avons commencé à intégrer l'IA dans ce workflow éditorial, l'une de nos contraintes était que notre agent sache pré-rédiger une fiche directement.
Problème : comment faire comprendre à l'IA comment utiliser les composants de notre design system, le DSFR et plus spécifiquement la version React (react-dsfr).
Le DSFR (Design System de l'État français) est le système de composants UI utilisé par les services publics numériques en France. Il est riche — accordéons, callouts, badges, tuiles — et rigoureusement normé. Autant d'éléments interactifs qu'un LLM (Large Language Model, le cerveau qui tourne derrière nos agents IA) livré à lui-même va imiter mal, ou pas du tout.
Pourquoi le structured output ne suffit pas
Le réflexe d'ingénieur, c'est de contraindre le LLM avec du structured output — un schéma JSON (JavaScript Object Notation), une forme de donnée normée mais aussi très verbeuse. En théorie, ça force la machine à produire exactement la structure attendue.
Voici un exemple pour afficher un composant accordéon en JSON :
{
"type": "accordion",
"title": "Étape 1 : rassembler les documents",
"stepNumber": 1,
"content": [
{
"type": "paragraph",
"text": "Vous aurez besoin de :"
},
{
"type": "list",
"ordered": false,
"items": [
"Passeport en cours de validité",
"Justificatif de domicile"
]
}
]
}
D'un autre côté il existe un format beaucoup plus simple et éprouvé sur les interfaces de chat IA du marché. Le format markdown.
Exemple sur le même composant accordéon en markdown cette fois-ci :
:::accordeon{title="Étape 1 : rassembler les documents" stepNumber="1"}
Vous aurez besoin de :
- Passeport en cours de validité
- Justificatif de domicile
:::
Simple, limpide, moins verbeux
| | Structured Output (JSON) | Directives Markdown | | ----------------------------------- | ------------------------------------------------- | ------------------------------------ | | Tokens consommés | Élevé (structure JSON verbeuse) | Faible (markdown natif LLM) | | Hallucinations | Rares sur la structure, fréquentes sur le contenu | Rares sur les deux | | Ajout d'un composant | Modifier le schéma + redéployer | Ajouter une directive à la whitelist | | Courbe d'apprentissage éditeurs | Élevée (JSON) | Faible (markdown) | | Human in the Loop | Difficile (relire du JSON) | Naturel (relire du markdown) |
Le pivot : markdown d'abord, composants ensuite
Les LLMs connaissent le markdown mieux que n'importe quelle syntaxe propriétaire. C'est leur terrain natif — celui sur lequel ils ont été entraînés. Moins de tokens, moins d'ambiguïté, moins d'hallucinations.
Mais le markdown standard ne couvre pas les composants interactifs (accordéons, callouts, encadrés). C'est là qu'entrent en jeu les directives remark.
remark-directive est une extension du standard markdown qui ajoute une syntaxe de blocs custom : les :::directives. Simple, lisible, et surtout — facile à apprendre pour un LLM en quelques exemples dans son prompt.
Trois directives qui couvrent 100% de nos besoins sur Réfugiés.info
On a défini trois directives qui couvrent la grande majorité de nos besoins éditoriaux sur réfugiés.info :
Accordéon (:::accordeon)
:::accordeon{title="Étape 1 : rassembler les documents"}
Vous aurez besoin de :
- Passeport en cours de validité
- Justificatif de domicile
:::
Callout Important (:::important)
:::important
Vous ne pouvez pas faire cette démarche en ligne.
Vous devez vous présenter en personne à la préfecture.
:::
Callout Info (:::good-to-know)
:::good-to-know
Vous pouvez suivre l'avancement de votre dossier sur le portail en ligne.
:::
Chaque directive se mappe directement sur un composant react-dsfr réel :
:::accordeon{title="Étape 1" stepNumber="1"} → <RIAccordion title="..." stepNumber={1}>
:::important → <CallOut> + titre "Important" (localisé)
:::good-to-know → <CallOut> + titre "À savoir" (localisé)
Le titre des callouts est auto-généré selon la langue de l'utilisateur. L'éditeur n'a pas à s'en préoccuper — et le LLM non plus.
Le pipeline : de la directive au composant React
Une fois que l'éditeur (humain ou IA) a produit du markdown avec ces directives, voilà ce qui se passe :
Markdown brut
→ remark-parse + remark-directive // Parser le texte, identifier les :::
→ remark-restore-hierarchy // Reconstituer l'imbrication des blocs
→ remarkDirectiveToComponent // Valider + taguer pour react-markdown
→ react-markdown + components={...} // Renderer les composants DSFR
La fonction clé est remarkDirectiveToComponent : elle vérifie que chaque directive appartient à une whitelist (toggle, important, good-to-know). Si oui, elle ajoute les métadonnées pour react-markdown. Si non, elle reconvertit en texte brut.
Pourquoi la whitelist est non-négociable
Sans whitelist, un contenu comme "La formation commence à 9:00" peut être interprété comme une directive :00 par le parser — et provoquer un crash silencieux. Ce n'est pas théorique. On l'a rencontré plusieurs fois sur nos fiches produites.
La whitelist + reconstruction du texte brut pour les directives inconnues = zéro faux positif en production.
La galère de l'indentation (et comment on l'a résolue)
Les directives remark natives utilisent l'indentation pour exprimer l'imbrication — une logique similaire au YAML. Le problème : les LLMs (et BlockNote.js, notre éditeur) fonctionnent bien mieux avec du markdown "plat", sans indentation.
Résultat : des fermetures ambiguës qui cassent le parser.
// Entrée plate (ce que produit le LLM)
:::accordeon
:::important
:::
:::
// Le parser ne sait pas quelle ::: ferme quel bloc
La solution : une étape de normalisation qui réécrit les indentations avec des longueurs différentes selon la profondeur d'imbrication.
// normalizeMarkdown.ts
const MAX_LENGTH = 12;
const stack: number[] = [];
// Ouverture d'un bloc : longueur = MAX_LENGTH - profondeur
const newLen = Math.max(3, MAX_LENGTH - stack.length);
stack.push(newLen);
// Fermeture : on retrouve la bonne longueur dans la pile
const len = stack.pop();
Résultat après normalisation :
::::::::::::accordeon // 12 :
:::::::::::important // 11 :
::::::::::: // ferme "important"
:::::::::::: // ferme "toggle"
Une fermeture courte ne peut plus fermer un bloc extérieur. Zéro ambiguïté, zéro hallucination de structure.
L'humain reste au centre
On ne remplace pas l'équipe éditoriale. On lui donne un outil qui parle sa langue.
Sur réfugiés.info, le workflow est clair : l'IA rédige un premier jet, l'éditeur valide et affine. Le Content Playground — notre interface éditoriale interne construite sur BlockNote.js — expose les mêmes directives sous forme de blocs visuels. L'éditeur voit des accordéons et des composants rendus visuellement, pas du code. Et à tout moment ils peuvent également relire la source markdown via un petit onglet "source" dans leur éditeur. Le markdown étant nativement un format peu verbeux il ne faut pas beaucoup de temps même à des non-tech pour identifier ce qui pourrait être mal formaté et corriger au besoin.
Ce qui change avec l'IA dans la boucle :
-
Vitesse : un premier jet structuré en secondes au lieu de minutes
-
Cohérence : les mêmes 3 directives partout, pas de HTML artisanal
-
Contrôle : l'éditeur a le dernier mot, toujours
Ce qui ne change pas :
-
La validation humaine : chaque fiche est relue avant publication
-
La qualité éditoriale : le ton, la clarté, l'adaptation culturelle restent humains
-
La responsabilité : c'est l'équipe éditoriale qui signe, pas l'IA
Pour une plateforme qui touche des centaines de milliers d'utilisateurs dans 8 langues, la publication assistée par l'IA n'est pas un raccourci. C'est un levier de productivité avec garde-fous.
4 règles pour connecter un design system à un LLM
Si vous construisez un workflow IA sur un design system — gouvernemental ou propriétaire :
-
N'essayez pas d'apprendre le HTML au LLM. Définissez un vocabulaire markdown minimal (3-5 directives) qui couvre vos cas d'usage principaux.
-
Whitelist obligatoire. Validez chaque directive avant de la rendre. Les faux positifs arrivent — et ils cassent en silence.
-
Normalisez avant de parser. Les LLMs ne sont pas fiables sur l'indentation. Aplatissez, puis reconstituez.
-
Human in the Loop, non négociable. L'IA génère, l'éditeur valide. Le design system garantit la cohérence visuelle en sortie. C'est un workflow à trois, pas une délégation.
Questions fréquentes
Est-ce que cette approche fonctionne avec d'autres design systems que le DSFR ?
Oui. Le principe est indépendant du design system : vous définissez vos directives markdown, vous les mappez sur vos composants, vous validez via whitelist. Nous utilisons le DSFR (Design System de l'État français) parce que c'est notre contexte, mais la même architecture fonctionne avec Material UI, Chakra, shadcn/ui ou n'importe quel système de composants.
Pourquoi pas du JSON Schema / structured output plutôt que du markdown ?
Le structured output (JSON) fonctionne bien pour des données structurées simples. Pour du contenu éditorial riche — paragraphes, listes imbriquées, composants interactifs — le markdown est plus naturel pour le LLM (moins de tokens, moins d'hallucinations) et plus lisible pour l'éditeur humain qui doit relire et valider.
Quel LLM utilisez-vous ?
Notre pipeline est agnostique côté modèle — les directives markdown fonctionnent avec GPT, Claude ou Mistral. Nous adaptons le agents au gré des évolutions du marché. Le point clé n'est pas le modèle, c'est le vocabulaire de sortie qu'on lui impose et le pipeline de validation en aval. Nos agents tournent actuellement sur Letta, un framework open-source pour agents IA stateful avec mémoire persistante, ce qui nous permet de garder le contexte éditorial d'une session à l'autre sans re-prompter à chaque fois.
Comment gérez-vous les traductions en 8 langues ?
Les directives sont language-agnostic. :::important reste :::important quelle que soit la langue du contenu. Les labels visibles ("Important", "À savoir") sont localisés automatiquement au moment du rendu côté React, pas dans le markdown source.
L'IA peut-elle créer de nouvelles directives toute seule ?
Non, et c'est voulu. La whitelist est fermée. Si le LLM invente une directive qui n'existe pas, elle est silencieusement reconvertie en texte brut. C'est un garde-fou, pas une limitation.
Jérémie Gisserot est freelance senior design + dev + direction artistique, spécialisé dans les produits numériques à impact (service public, ESS). Il travaille sur réfugiés.info depuis 2024 et construit des workflows IA avec validation humaine.