Lorsque l'on développe des applications industrielles ou des agents autonomes basés sur des LLM, le prompt en texte libre atteint rapidement ses limites : hallucinations, dérive du format de sortie, et difficulté de maintenance. Structurer ses prompts en JSON change radicalement la donne.
1. Pourquoi utiliser des prompts en JSON ?
Un prompt structuré en JSON apporte des bénéfices fondamentaux pour l'ingénierie logicielle :
- Structure claire : Facile à parser, à versionner via Git, et à tester unitairement.
- Séparation des responsabilités : Délimitation stricte entre les instructions, le contexte, les données dynamiques et les contraintes.
- Réutilisabilité : Utilisation de templates et d'injection de variables propres.
- Réduction de l'ambiguïté : Le format tabulaire ou hiérarchique aide le modèle à structurer sa propre attention.
- Interopérabilité API : Alignement naturel avec les charges utiles (payloads) des API modernes (OpenAI, Anthropic, LangChain, etc.).
2. Principe Fondamental : Réduire l'Espace de Liberté
Un LLM « devine » la suite logique tant qu'il dispose de degrés de liberté. Pour obtenir un comportement déterministe, l'objectif est de verrouiller l'espace des possibles à travers 5 axes majeurs :
- Un rôle strict et non narratif : Éviter les adjectifs vagues et spécifier un périmètre technique précis.
- Des instructions procédurales : Découper la tâche en étapes algorithmiques claires.
- Des contraintes négatives explicites : Définir clairement ce que le modèle ne doit pas faire.
- Un contrat de sortie formel : Imposer un schéma de type strict (JSON-first).
- Un mécanisme d'auto-contrôle (Self-check) : Demander au modèle de valider sa propre sortie avant de la rendre.
3. Structure de Base Recommandée
Voici un modèle générique, robuste et extensible :
{
"role": {
"persona": "...",
"scope": [...]
},
"task": {
"objective": "...",
"context": "..."
},
"instructions": [
{
"step": 1, "action": "...", "details": "..."
}
],
"input": {},
"constraints": {
"must": [],
"must_not": []
},
"output_contract": {
"type": "json",
"schema": {}
}
}
4. Bonnes Pratiques Opérationnelles
4.1 Séparer instructions et données
❌ Mauvais : Mélanger la consigne et le contenu directement dans une chaîne de caractères.
✅ Bon : Isoler le corps du texte dans un objet input dédié.
"instructions": ["Résumer en 3 lignes"],
"input": {
"text": "Le garbage collector de Java fonctionne..."
}
4.2 Verrouiller le contrat de sortie (JSON-first)
Pour s'assurer que la sortie est directement exploitable par votre code aval, spécifiez un schéma de type précis et interdisez les propriétés superflues :
"output_contract": {
"type": "json",
"schema": {
"type": "object",
"required": ["result", "confidence"],
"properties": {
"result": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 }
},
"additionalProperties": false
}
}
L'attribut "additionalProperties": false est crucial si vous utilisez des validateurs de schémas (comme ajv en Node.js) pour rejeter les hallucinations de clés.
4.3 Privilégier les contraintes négatives strictes (must_not)
Les LLM réagissent beaucoup mieux aux interdictions explicites qu'aux recommandations vagues de type « évitez de... ».
"constraints": {
"must_not": [
"Ajouter du contexte non demandé",
"Expliquer les bases conceptuelles",
"Utiliser des exemples fictifs"
]
}
5. Exemple Avancé : Code Reviewer ultra-précis
Voici un exemple complet intégrant l'ensemble de ces patterns pour une tâche de revue de code automatisée :
{
"role": {
"persona": "senior_code_reviewer",
"scope": ["python", "production_code"],
"exclusions": ["style_guide_basics", "tutorial"]
},
"task": {
"objective": "Identifier les problèmes critiques dans le code fourni"
},
"instructions": [
{ "step": 1, "action": "scan", "details": "Lire l'intégralité du code" },
{ "step": 2, "action": "detect", "details": "Identifier bugs, failles de sécurité, anti-patterns" },
{ "step": 3, "action": "classify", "details": "Classer chaque anomalie par sévérité" },
{ "step": 4, "action": "self_check", "details": "Vérifier la conformité stricte au schéma de sortie" }
],
"input": {
"language": "python",
"code": "def validate_email(email):\n return '@' in email"
},
"constraints": {
"must_not": [
"Corriger le code à la place du développeur",
"Ajouter des suggestions hors scope"
]
},
"output_contract": {
"type": "json",
"schema": {
"issues": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "severity", "description"],
"properties": {
"type": { "enum": ["bug", "security", "performance"] },
"severity": { "enum": ["low", "medium", "high", "critical"] },
"description": { "type": "string" }
},
"additionalProperties": false
}
}
}
}
}
6. Attention : JSON ≠ Prompt API natif
Deux options s'offrent à vous :
- Option A (Le texte brut) : Vous passez le JSON stringifié au sein d'une consigne textuelle globale demandant au modèle de s'y conformer.
- Option B (Le mapping API - Recommandé) : Vous mapmez les blocs logiques de votre JSON directement dans les messages système et utilisateur de l'API (ex:
systempour le rôle et les contraintes,userpour l'input et le contrat de sortie).
Considérer vos prompts non plus comme de la prose littéraire, mais comme des interfaces logicielles strictes, transforme radicalement la fiabilité de vos intégrations IA. Moins d'ambiguïté, des schémas validables, et une maintenabilité accrue : voilà la clé d'un passage à l'échelle réussi en production.
