Contrainte des LLM avec une sortie structurée : Ollama, Qwen3 & Python ou Go
Quelques façons d'obtenir une sortie structurée d'Ollama
Les grands modèles de langage (LLMs) sont puissants, mais en production, nous souhaitons rarement des paragraphes libres. Au contraire, nous voulons des données prévisibles : des attributs, des faits ou des objets structurés que vous pouvez alimenter dans une application. C’est la sortie structurée des LLM.
Il y a un certain temps, Ollama a introduit le support de sortie structurée (annonce), permettant de contraindre les réponses d’un modèle afin qu’elles correspondent à un schéma JSON. Cela libère des pipelines d’extraction de données cohérents pour des tâches comme le catalogage des fonctionnalités des LLM, le benchmarking des modèles ou l’automatisation de l’intégration système.
Dans cet article, nous aborderons :
- Qu’est-ce que la sortie structurée et pourquoi elle est importante
- Une méthode simple pour obtenir une sortie structurée à partir des LLM
- Comment la nouvelle fonctionnalité d’Ollama fonctionne
- Exemples d’extraction des capacités des LLM :
Qu’est-ce que la sortie structurée ?
Normalement, les LLM génèrent du texte libre :
« Le modèle X prend en charge la raisonner avec une chaîne de pensée, possède une fenêtre de contexte de 200K et parle anglais, chinois et espagnol. »
Cela est lisible, mais difficile à analyser.
Au contraire, avec une sortie structurée, nous demandons un schéma strict :
{
"name": "Model X",
"supports_thinking": true,
"max_context_tokens": 200000,
"languages": ["English", "Chinese", "Spanish"]
}
Ce JSON est facile à valider, à stocker dans une base de données ou à alimenter dans une interface utilisateur.
Méthode simple pour obtenir une sortie structurée à partir des LLM
Les LLM comprennent parfois ce que le schéma est et nous pouvons demander au LLM de retourner une sortie en JSON en utilisant un schéma particulier. Le modèle Qwen3 d’Alibaba est optimisé pour la raisonner et les réponses structurées. Vous pouvez explicitement lui demander de répondre en JSON.
Exemple 1 : Utilisation de Qwen3 avec ollama
en Python, en demandant un JSON avec un schéma
import json
import ollama
prompt = """
Vous êtes un extracteur de données structurées.
Retournez uniquement du JSON.
Texte : "Elon Musk a 53 ans et vit à Austin."
Schéma : { "name": string, "age": int, "city": string }
"""
response = ollama.chat(model="qwen3", messages=[{"role": "user", "content": prompt}])
output = response['message']['content']
# Analyser le JSON
try:
data = json.loads(output)
print(data)
except Exception as e:
print("Erreur lors de l’analyse du JSON :", e)
Sortie :
{"name": "Elon Musk", "age": 53, "city": "Austin"}
Imposer la validation du schéma avec Pydantic
Pour éviter les sorties malformées, vous pouvez valider contre un schéma Pydantic en Python.
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
city: str
# Supposons que 'output' est la chaîne JSON provenant de Qwen3
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)
Cela garantit que la sortie correspond à la structure attendue.
La sortie structurée d’Ollama
Ollama permet désormais de passer un schéma dans le paramètre format
. Le modèle est alors contraint de répondre uniquement en JSON qui correspond au schéma (docs).
En Python, vous définissez généralement votre schéma avec Pydantic et laissez Ollama l’utiliser comme un schéma JSON.
Exemple 2 : Extraire les métadonnées des fonctionnalités d’un LLM
Supposons que vous ayez un extrait de texte décrivant les capacités d’un LLM :
« Qwen3 a un bon support multilingue (anglais, chinois, français, espagnol, arabe). Il permet des étapes de raisonnement (chaîne de pensée). La fenêtre de contexte est de 128K tokens. »
Vous souhaitez des données structurées :
from pydantic import BaseModel
from typing import List
from ollama import chat
class LLMFeatures(BaseModel):
name: str
supports_thinking: bool
max_context_tokens: int
languages: List[str]
prompt = """
Analysez la description suivante et retournez les fonctionnalités du modèle en JSON uniquement.
Description du modèle :
'Qwen3 a un bon support multilingue (anglais, chinois, français, espagnol, arabe).
Il permet des étapes de raisonnement (chaîne de pensée).
La fenêtre de contexte est de 128K tokens.'
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format=LLMFeatures.model_json_schema(),
options={"temperature": 0},
)
print(resp.message.content)
Sortie possible :
{
"name": "Qwen3",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}
Exemple 3 : Comparer plusieurs modèles
Fournissez des descriptions de plusieurs modèles et extrayez-les sous forme structurée :
from typing import List
class ModelComparison(BaseModel):
models: List[LLMFeatures]
prompt = """
Extrayez les fonctionnalités de chaque modèle en JSON.
1. Llama 3.1 prend en charge le raisonnement. Fenêtre de contexte de 128K. Langues : anglais uniquement.
2. GPT-4 Turbo prend en charge le raisonnement. Fenêtre de contexte de 128K. Langues : anglais, japonais.
3. Qwen3 prend en charge le raisonnement. Fenêtre de contexte de 128K. Langues : anglais, chinois, français, espagnol, arabe.
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format=ModelComparison.model_json_schema(),
options={"temperature": 0},
)
print(resp.message.content)
Sortie :
{
"models": [
{
"name": "Llama 3.1",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English"]
},
{
"name": "GPT-4 Turbo",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English", "Japanese"]
},
{
"name": "Qwen3",
"supports_thinking": true,
"max_context_tokens": 128000,
"languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}
]
}
Cela rend trivial le benchmarking, la visualisation ou le filtrage des modèles selon leurs fonctionnalités.
Exemple 4 : Détecter automatiquement les lacunes
Vous pouvez même permettre des valeurs null
lorsqu’un champ est absent :
from typing import Optional
class FlexibleLLMFeatures(BaseModel):
name: str
supports_thinking: Optional[bool]
max_context_tokens: Optional[int]
languages: Optional[List[str]]
Cela garantit que votre schéma reste valide même si certaines informations sont inconnues.
Avantages, limites et bonnes pratiques
L’utilisation de la sortie structurée via Ollama (ou tout autre système qui le prend en charge) offre de nombreux avantages — mais présente également certaines limites.
Avantages
- Garanties plus fortes : Le modèle est demandé de se conformer à un schéma JSON plutôt que du texte libre.
- Parsing plus facile : Vous pouvez directement utiliser
json.loads
ou valider avec Pydantic / Zod, plutôt que des expressions régulières ou des heuristiques. - Évolution basée sur le schéma : Vous pouvez versionner votre schéma, ajouter des champs (avec des valeurs par défaut) et maintenir la compatibilité descendante.
- Interopérabilité : Les systèmes en aval attendent des données structurées.
- Déterminisme (meilleur avec une température basse) : Lorsque la température est basse (par exemple, 0), le modèle est plus susceptible de respecter rigoureusement le schéma. Les documents d’Ollama recommandent cela.
Limites et pièges
- Incohérence du schéma : Le modèle pourrait toujours dévier — par exemple, manquer une propriété obligatoire, réordonner les clés ou inclure des champs supplémentaires. Une validation est nécessaire.
- Schémas complexes : Des schémas JSON très profonds ou récursifs pourraient troubler le modèle ou entraîner des échecs.
- Ambiguïté dans le prompt : Si votre prompt est vague, le modèle pourrait deviner des champs ou des unités incorrectement.
- Incohérence entre les modèles : Certains modèles pourraient être meilleurs ou pires pour respecter les contraintes structurées.
- Limites de tokens : Le schéma lui-même ajoute un coût en tokens au prompt ou à l’appel d’API.
Bonnes pratiques et conseils (tirés du blog d’Ollama + expérience)
- Utilisez Pydantic (Python) ou Zod (JavaScript) pour définir vos schémas et générer automatiquement des schémas JSON. Cela évite les erreurs manuelles.
- Incluez toujours des instructions comme « répondez uniquement en JSON » ou « n’incluez aucun commentaire ou texte supplémentaire » dans votre prompt.
- Utilisez temperature = 0 (ou très bas) pour minimiser le hasard et maximiser la conformité au schéma. Ollama recommande le déterminisme.
- Validez et potentiellement reculez (par exemple, réessayer ou nettoyer) lorsqu’un échec de parsing JSON ou de validation du schéma se produit.
- Commencez par un schéma plus simple, puis étendez-le progressivement. Ne compliquez pas trop initialement.
- Incluez des instructions d’erreur utiles mais contrôlées : par exemple, si le modèle ne peut pas remplir un champ obligatoire, répondez avec
null
plutôt que de l’omettre (si votre schéma le permet).
Exemple Go 1 : Extraction des fonctionnalités des LLM
Voici un simple programme Go qui demande à Qwen3 une sortie structurée concernant les fonctionnalités d’un LLM.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ollama/ollama/api"
)
type LLMFeatures struct {
Name string `json:"name"`
SupportsThinking bool `json:"supports_thinking"`
MaxContextTokens int `json:"max_context_tokens"`
Languages []string `json:"languages"`
}
func main() {
client, err := api.ClientFromEnvironment()
if err != nil {
log.Fatal(err)
}
prompt := `
Analysez la description suivante et retournez les fonctionnalités du modèle en JSON uniquement.
Description :
"Qwen3 a un bon support multilingue (anglais, chinois, français, espagnol, arabe).
Il permet des étapes de raisonnement (chaîne de pensée).
La fenêtre de contexte est de 128K tokens."
`
// Définir le schéma JSON pour la sortie structurée
formatSchema := map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]string{
"type": "string",
},
"supports_thinking": map[string]string{
"type": "boolean",
},
"max_context_tokens": map[string]string{
"type": "integer",
},
"languages": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
}
// Convertir le schéma en JSON
formatJSON, err := json.Marshal(formatSchema)
if err != nil {
log.Fatal("Échec de la conversion du schéma de format :", err)
}
req := &api.GenerateRequest{
Model: "qwen3:8b",
Prompt: prompt,
Format: formatJSON,
Options: map[string]any{"temperature": 0},
}
var features LLMFeatures
var rawResponse string
err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
// Accumuler le contenu à mesure qu’il est transmis
rawResponse += response.Response
// Ne parser que lorsqu’il est complet
if response.Done {
if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
return fmt.Errorf("erreur de parsing JSON : %v", err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Structuré analysé : %+v\n", features)
}
Pour compiler et exécuter ce programme Go - supposons que nous ayons ce fichier main.go dans un dossier ollama-struct
,
Nous devons exécuter à l’intérieur de ce dossier :
# initialiser le module
go mod init ollama-struct
# récupérer toutes les dépendances
go mod tidy
# compiler et exécuter
go build -o ollama-struct main.go
./ollama-struct
Exemple de sortie
Structuré analysé : {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}
Exemple Go 2 : Comparer plusieurs modèles
Vous pouvez étendre cela pour extraire une liste de modèles pour une comparaison.
type ModelComparison struct {
Models []LLMFeatures `json:"models"`
}
prompt = `
Extraire les fonctionnalités à partir des descriptions suivantes des modèles et retourner en tant que JSON :
1. PaLM 2 : Ce modèle a des capacités de raisonnement limitées et se concentre sur la compréhension de base du langage. Il prend en charge une fenêtre de contexte de 8 000 tokens. Il prend principalement en charge l’anglais.
2. LLaMA 2 : Ce modèle a des capacités de raisonnement modérées et peut gérer certaines tâches logiques. Il peut traiter jusqu’à 4 000 tokens dans son contexte. Il prend en charge les langues anglaise, espagnole et italienne.
3. Codex : Ce modèle a des capacités de raisonnement fortes spécifiquement pour la programmation et l’analyse de code. Il a une fenêtre de contexte de 16 000 tokens. Il prend en charge les langues anglaise, Python, JavaScript et Java.
Retourner un objet JSON avec un tableau "models" contenant tous les modèles.
`
// Définir le schéma JSON pour la comparaison des modèles
comparisonSchema := map[string]any{
"type": "object",
"properties": map[string]any{
"models": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]string{
"type": "string",
},
"supports_thinking": map[string]string{
"type": "boolean",
},
"max_context_tokens": map[string]string{
"type": "integer",
},
"languages": map[string]any{
"type": "array",
"items": map[string]string{
"type": "string",
},
},
},
"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
},
},
},
"required": []string{"models"},
}
// Convertir le schéma en JSON
comparisonFormatJSON, err := json.Marshal(comparisonSchema)
if err != nil {
log.Fatal("Échec de la conversion du schéma de comparaison :", err)
}
req = &api.GenerateRequest{
Model: "qwen3:8b",
Prompt: prompt,
Format: comparisonFormatJSON,
Options: map[string]any{"temperature": 0},
}
var comp ModelComparison
var comparisonResponse string
err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
// Accumuler le contenu à mesure qu’il est transmis
comparisonResponse += response.Response
// Ne parser que lorsqu’il est complet
if response.Done {
if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
return fmt.Errorf("erreur de parsing JSON : %v", err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
for _, m := range comp.Models {
fmt.Printf("%s : Context=%d, Languages=%v\n", m.Name, m.MaxContextTokens, m.Languages)
}
Exemple de sortie
PaLM 2 : Context=8000, Languages=[English]
LLaMA 2 : Context=4000, Languages=[English Spanish Italian]
Codex : Context=16000, Languages=[English Python JavaScript Java]
Par ailleurs, qwen3:4b fonctionne bien sur ces exemples, tout comme qwen3:8b.
Bonnes pratiques pour les développeurs Go
- Fixer la température à 0 pour une adhésion maximale au schéma.
- Valider avec
json.Unmarshal
et reculer si l’analyse échoue. - Garder les schémas simples — les structures JSON profondément imbriquées ou récursives peuvent causer des problèmes.
- Autoriser les champs optionnels (utiliser
omitempty
dans les balises des structs Go) si vous attendez des données manquantes. - Ajouter des tentatives si le modèle émet parfois du JSON invalide.
Exemple complet - Créer un graphique avec les spécifications des LLM (étapes : du JSON structuré aux tableaux de comparaison)
- Définir un schéma pour les données que vous souhaitez
Utilisez Pydantic afin de générer à la fois (a) un schéma JSON pour Ollama et (b) valider la réponse du modèle.
from pydantic import BaseModel
from typing import List, Optional
class LLMFeatures(BaseModel):
name: str
supports_thinking: bool
max_context_tokens: int
languages: List[str]
- Demandez à Ollama de retourner uniquement du JSON dans cette forme
Transmettez le schéma dans format=
et réduisez la température pour le déterminisme.
from ollama import chat
prompt = """
Extrayez les fonctionnalités de chaque modèle. Retournez uniquement du JSON correspondant au schéma.
1) Qwen3 prend en charge la chaîne de pensée ; 128K contexte ; anglais, chinois, français, espagnol, arabe.
2) Llama 3.1 prend en charge la chaîne de pensée ; 128K contexte ; anglais.
3) GPT-4 Turbo prend en charge la chaîne de pensée ; 128K contexte ; anglais, japonais.
"""
resp = chat(
model="qwen3",
messages=[{"role": "user", "content": prompt}],
format={"type": "array", "items": LLMFeatures.model_json_schema()},
options={"temperature": 0}
)
raw_json = resp.message.content # Liste JSON de LLMFeatures
- Valider et normaliser
Validez toujours avant d’utiliser en production.
from pydantic import TypeAdapter
adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json) # -> list[LLMFeatures]
- Créer un tableau de comparaison (pandas)
Transformez vos objets validés en DataFrame que vous pouvez trier, filtrer et exporter.
import pandas as pd
df = pd.DataFrame([m.model_dump() for m in models])
df["languages_count"] = df["languages"].apply(len)
df["languages"] = df["languages"].apply(lambda xs: ", ".join(xs))
# Réordonner les colonnes pour une meilleure lisibilité
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]
# Enregistrer en tant que CSV pour une utilisation ultérieure
df.to_csv("llm_feature_comparison.csv", index=False)
- (Optionnel) Visualisation rapide
Des graphiques simples vous aident à visualiser rapidement les différences entre les modèles.
import matplotlib.pyplot as plt
plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("Fenêtre de contexte maximale par modèle (tokens)")
plt.xlabel("Modèle")
plt.ylabel("Tokens de contexte maximum")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")
TL;DR
Avec le nouveau support de sortie structurée d’Ollama, vous pouvez traiter les LLM non seulement comme des chatbots mais aussi comme des moteurs d’extraction de données.
Les exemples ci-dessus ont montré comment extraire automatiquement des métadonnées structurées sur les fonctionnalités des LLM comme le support de la pensée, la taille de la fenêtre de contexte et les langues prises en charge — des tâches qui nécessiteraient autrement un parsing fragile.
Qu’il s’agisse de construire un catalogue de modèles LLM, un tableau de bord d’évaluation ou un assistant de recherche alimenté par l’IA, les sorties structurées rendent l’intégration fluide, fiable et prête pour la production.
Liens utiles
- https://ollama.com/blog/structured-outputs
- Fiche de référence Ollama
- Fiche de référence Python
- AWS SAM + AWS SQS + Python PowerTools
- Fiche de référence Go
- Comparaison des ORMs Go pour PostgreSQL : GORM vs Ent vs Bun vs sqlc
- Reranking des documents textuels avec Ollama et le modèle d’embedding Qwen3 - en Go
- Performance d’AWS Lambda : JavaScript vs Python vs Go