Vous demandez à un LLM de remplir un formulaire en JSON et une fois sur cinq il vous renvoie du texte introductif comme « Voici le JSON demandé : » avant le bloc, ou il invente un guillemet en trop, ou il oublie une virgule. Votre pipeline casse. Les sorties structurées d'Ollama résolvent ce problème en 2026 : on impose au modèle un format JSON strict via un schéma, et la réponse est garantie syntaxiquement valide.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Ce qu'est une « sortie structurée » et pourquoi elle change la vie d'un développeur qui consomme un LLM en local
- Comment forcer Ollama à respecter un JSON Schema brut (l'approche minimaliste)
- Comment utiliser Pydantic pour avoir un objet Python typé directement, sans parser
- Là où le modèle hallucine malgré le schéma — un piège que personne ne documente
Pourquoi les sorties structurées changent tout
Section intitulée « Pourquoi les sorties structurées changent tout »Avant 2024, pour obtenir du JSON d'un LLM, vous deviez :
- Écrire un prompt qui demandait gentiment « renvoie uniquement du JSON sans texte autour »
- Croiser les doigts
- Encadrer votre
json.loads()d'untry/exceptparce qu'une fois sur cinq, ça pétait
Depuis Ollama 0.5 (et amélioré dans la 0.24 actuelle), le paramètre format vous permet de passer un schéma JSON au modèle. Côté serveur, Ollama force la génération token par token à respecter ce schéma. Résultat : le JSON est valide à 100 %, même sur des petits modèles 7 B.
Concrètement, les sorties structurées vous permettent de :
- Extraire des entités d'un texte libre (email, ticket, CV) → JSON typé
- Classifier une entrée dans une catégorie d'une liste prédéfinie (énumération)
- Faire des agents locaux qui produisent des structures de données consommées par votre code
- Analyser des images et obtenir une description structurée (cf. guide Vision)
Prérequis et installation
Section intitulée « Prérequis et installation »Pour suivre ce guide, vous avez besoin :
-
Un Ollama qui tourne en local avec un modèle qui supporte les sorties structurées. Les modèles validés en 2026 :
Fenêtre de terminal ollama pull qwen3 # 5.2 Go, le plus stable# ouollama pull gemma4 # 9.6 Go, vision + tools natifs# ouollama pull llama3.1 # 4.7 Go, valeur sûreSi vous n'avez pas encore Ollama installé, suivez le guide d'installation Ollama d'abord.
-
Un projet Python avec les dépendances suivantes (épinglées pour la reproductibilité) :
pyproject.toml [project]name = "lab-ollama-structured"requires-python = ">=3.10"dependencies = ["ollama==0.6.2","pydantic[email]==2.13.4",]Fenêtre de terminal uv sync# ou : pip install "ollama==0.6.2" "pydantic[email]==2.13.4" -
Vérifier qu'Ollama écoute sur le port par défaut :
Fenêtre de terminal curl http://localhost:11434/api/tagsVous devez voir la liste des modèles installés.
Méthode 1 : JSON Schema brut (la fondation)
Section intitulée « Méthode 1 : JSON Schema brut (la fondation) »C'est la méthode la plus directe : on écrit le schéma JSON à la main et on le passe au paramètre format du client Ollama.
Cas d'usage : on reçoit un email de support et on veut extraire les informations clés en JSON pour les router vers la bonne équipe.
import jsonfrom ollama import Client
MODEL = "qwen3"HOST = "http://localhost:11434"
EMAIL = """De: marie.dupont@acme-corp.frObjet: Problème avec l'API key
Bonjour, mes appels API retournent 401 Unauthorized depuis ce matin.Numéro client : ACME-4271. Plan Pro, carte à jour.
Marie Dupont, Lead Developer chez ACME Corp"""
# Le schéma JSON : la structure que le modèle DOIT respecterSCHEMA = { "type": "object", "properties": { "expediteur_nom": {"type": "string"}, "expediteur_email": {"type": "string", "format": "email"}, "entreprise": {"type": "string"}, "numero_client": {"type": "string"}, "categorie": { "type": "string", "enum": ["authentification", "facturation", "performance", "bug", "autre"], }, "priorite": {"type": "string", "enum": ["basse", "normale", "haute", "critique"]}, "resume": {"type": "string"}, }, "required": ["expediteur_nom", "expediteur_email", "entreprise", "categorie", "priorite", "resume"],}
client = Client(host=HOST)response = client.chat( model=MODEL, messages=[ {"role": "system", "content": "Tu extrais les informations d'un email de support en JSON."}, {"role": "user", "content": EMAIL}, ], format=SCHEMA, # 👈 ICI la magie options={"temperature": 0},)raw = response["message"]["content"]print(raw)
# Le json.loads() ci-dessous ne peut PAS échouer : Ollama garantit la validité.parsed = json.loads(raw)print(parsed["categorie"], "→", parsed["priorite"])Sortie réelle (validée sur H100 avec qwen3) :
{ "expediteur_nom": "Marie Dupont", "expediteur_email": "marie.dupont@acme-corp.fr", "entreprise": "ACME Corp", "numero_client": "ACME-4271", "categorie": "authentification", "priorite": "haute", "resume": "Problème avec l'API key, appels API retournent 401 Unauthorized"}Méthode 2 : Pydantic (la plus pratique au quotidien)
Section intitulée « Méthode 2 : Pydantic (la plus pratique au quotidien) »Écrire un schéma JSON à la main pour chaque sortie est vite pénible et sujet aux fautes de frappe. Pydantic est une bibliothèque Python qui vous permet de décrire votre structure comme une classe Python : Pydantic génère le schéma JSON automatiquement, valide la réponse du LLM, et vous retourne un objet Python avec l'autocomplétion dans votre IDE.
Pydantic (de l'anglais pydantic, contraction de « Python » + « pedantic ») est la lib de validation de données la plus utilisée en Python en 2026. Elle équipe FastAPI, OpenAI SDK, et la plupart des frameworks IA.
from typing import Literalfrom ollama import Clientfrom pydantic import BaseModel, EmailStr, Field
MODEL = "qwen3"HOST = "http://localhost:11434"
class TicketSupport(BaseModel): """Ticket extrait d'un email entrant.""" expediteur_nom: str expediteur_email: EmailStr expediteur_role: str | None = None entreprise: str numero_client: str | None = None categorie: Literal["authentification", "facturation", "performance", "bug", "autre"] priorite: Literal["basse", "normale", "haute", "critique"] resume: str = Field(description="Synthèse en 1-2 phrases du problème exposé.")
EMAIL = """De: paul.martin@datalab.ioObjet: Latence très élevée depuis hierVos endpoints /v1/embeddings prennent 8-12 secondes depuis hier soir.Numéro client : DTL-998. — Paul, CTO Datalab"""
client = Client(host=HOST)response = client.chat( model=MODEL, messages=[{"role": "user", "content": EMAIL}], # Pydantic génère le JSON Schema automatiquement format=TicketSupport.model_json_schema(), options={"temperature": 0},)
# Validation + objet typé en une ligneticket = TicketSupport.model_validate_json(response["message"]["content"])print(ticket.categorie) # autocomplétion IDEprint(ticket.priorite)Sortie réelle :
performancehauteMéthode 3 : énumérations et types contraints
Section intitulée « Méthode 3 : énumérations et types contraints »L'un des cas d'usage les plus rentables est la classification. Un LLM est très bon pour catégoriser un texte, mais il a tendance à inventer de nouvelles catégories ou à formuler à sa façon. Avec Literal[...] (Pydantic) ou enum (JSON Schema), il est forcé de choisir dans votre liste.
from typing import Literalfrom pydantic import BaseModel
class Classification(BaseModel): sentiment: Literal["positif", "neutre", "negatif"] urgence: Literal["faible", "moyenne", "haute"] departement_concerne: Literal["support", "ventes", "tech", "rh", "autre"]C'est la garantie que votre code aval n'aura jamais à gérer une catégorie inattendue.
Le piège : le schéma garantit la structure, pas la véracité
Section intitulée « Le piège : le schéma garantit la structure, pas la véracité »Voici le piège que personne ne mentionne dans les tutos. Regardez ce code :
class Personne(BaseModel): nom: str email: EmailStr age: int pays: Literal["France", "Belgique", "Suisse", "Canada", "Autre"]
# Prompt pauvre : on demande des infos non fourniesresponse = client.chat( model="qwen3", messages=[{"role": "user", "content": "Donne-moi les informations d'un utilisateur."}], format=Personne.model_json_schema(),)print(response["message"]["content"])Sortie réelle obtenue sur H100 :
{"nom": "Utilisateur", "email": "utilisateur@example.com", "age": 25, "pays": "France"}Le JSON est syntaxiquement valide. Pydantic ne lèvera aucune exception. Mais ces données sont purement inventées : le LLM a halluciné un utilisateur générique pour respecter le schéma. C'est un problème de fond pour les applications de production.
Modèles testés et performances (H100, mai 2026)
Section intitulée « Modèles testés et performances (H100, mai 2026) »Mesures réelles sur NVIDIA H100 PCIe 80 Go avec Ollama 0.24 :
| Modèle | VRAM | Tokens/s | Fiabilité schéma |
|---|---|---|---|
qwen3 | 5.2 Go | ~160 | Excellente |
gemma4 | 9.6 Go | ~120 | Excellente |
llama3.1 | 4.7 Go | ~140 | Bonne |
mistral | 4.4 Go | ~100 | Moyenne (parfois inventif) |
Mon choix par défaut en 2026 :
qwen3— le meilleur compromis taille / vitesse / fidélité au schéma.
Q : Mon modèle ignore le format et me renvoie du texte libre.
R : Vérifiez que vous utilisez Ollama serveur 0.5 minimum (ollama --version). Sur les versions plus anciennes, seul le mode format="json" (sans schéma) existe. Mettez à jour avec curl -fsSL https://ollama.com/install.sh | sh.
Q : model_validate_json lève une ValidationError sur un email non conforme.
R : Bon signe — Pydantic vous protège d'un email mal formé. Le LLM en a produit un techniquement valide selon email-validator, mais qui ne correspond pas à votre contexte. Loggez l'erreur et faites un fallback (champ à None, retry avec un prompt plus précis).
Q : Peut-on utiliser des schémas imbriqués (objets dans objets) ?
R : Oui, Pydantic gère parfaitement les modèles imbriqués. Définissez vos classes l'une dans l'autre et model_json_schema() produira un schéma JSON avec $defs/$ref. Ollama suit.
Q : Quelle différence entre format="json" (legacy) et format=schema ?
R : format="json" (chaîne magique) force uniquement du JSON valide, sans contrainte de structure → le modèle peut inventer les clés. format=<dict de schéma> force la structure exacte. Toujours préférer le schéma.
Q : Le modèle remplit-il des champs nullable quand l'info manque ?
R : Variable. Avec qwen3 et un prompt explicite (« mets à null si l'information n'est pas dans le texte »), oui ~90 % du temps. Sans instruction explicite, il préfère souvent inventer.
À retenir
Section intitulée « À retenir »- Les sorties structurées rendent le JSON garanti — fini les
try/exceptautour dejson.loads(). - JSON Schema brut pour des cas simples ; Pydantic dès qu'il y a plus de 3 champs ou de la validation métier.
Literal(Pydantic) ouenum(JSON Schema) pour la classification : interdit au modèle d'inventer une catégorie.- Le schéma garantit la structure, pas la véracité : combinez avec un contexte riche et autorisez
Noneexplicitement. - qwen3 et gemma4 sont les modèles les plus fiables sur ce cas d'usage en 2026.