Contrainte des LLM avec une sortie structurée : Ollama, Qwen3 & Python ou Go

Quelques façons d'obtenir une sortie structurée d'Ollama

Sommaire

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.

ducks in a row

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)

llm-chart

  1. 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]
  1. 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
  1. 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]
  1. 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)
  1. (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