Contrainte des LLMs avec des sorties structurées : Ollama, Qwen3 et Python ou Go

Quelques méthodes pour obtenir des sorties structurées avec Ollama

Sommaire

Les grands modèles de langage (LLM) sont puissants, mais en production, nous ne souhaitons rarement des paragraphes libres. Au lieu de cela, nous voulons des données prédictibles : des attributs, des faits ou des objets structurés que vous pouvez injecter dans une application. C’est ce qu’on appelle la sortie structurée des LLM.

L’application stricte du schéma réduit la fréquence à laquelle des logits erronés se transforment en JSON invalide, mais la température et les pénalités restent importantes pour les tempêtes de tentatives ; consultez les paramètres d’inférence agencique pour Qwen et Gemma lorsque vous combinez les contraintes de format avec des agents.

Il y a quelque temps, Ollama a introduit le support des sorties structurées (annonce), rendant possible la contrainte des réponses d’un modèle pour qu’elles correspondent à un schéma JSON. Cela débloque des pipelines d’extraction de données cohérents pour des tâches telles que le catalogage des fonctionnalités des LLM, le benchmarking des modèles ou l’automatisation de l’intégration système.

canards en rangée

Dans cet article, nous couvrirons :

  • Ce qu’est la sortie structurée et pourquoi elle est importante
  • Une méthode simple pour obtenir des sorties structurées des LLM
  • Comment la nouvelle fonctionnalité d’Ollama fonctionne
  • Des 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 le raisonnement avec la chaîne de pensée, a une fenêtre de contexte de 200K et parle anglais, chinois et espagnol. »

C’est lisible, mais difficile à analyser.

Au lieu de cela, avec la 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 une interface utilisateur.


Méthode simple pour obtenir une sortie structurée d’un LLM

Les LLM comprennent parfois ce qu’est le schéma et nous pouvons demander au LLM de retourner la sortie en JSON en utilisant un schéma particulier. Le modèle Qwen3 d’Alibaba est optimisé pour le raisonnement et les réponses structurées. Vous pouvez lui demander explicitement de répondre en JSON.

Exemple 1 : Utilisation de Qwen3 avec ollama en Python, demande de JSON avec 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']

# Analyse du JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("Erreur d'analyse du JSON :", e)

Sortie :

{"name": "Elon Musk", "age": 53, "city": "Austin"}

Application de la validation de schéma avec Pydantic

Pour éviter les sorties mal formé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' soit la chaîne JSON de Qwen3
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

Cela garantit que la sortie conforme à la structure attendue.


La sortie structurée d’Ollama

Ollama vous permet maintenant de passer un schéma dans le paramètre format. Le modèle est alors contraint de répondre uniquement en JSON qui conforme au schéma (docs).

En Python, vous définissez généralement votre schéma avec Pydantic et laissez Ollama l’utiliser comme schéma JSON.


Exemple 2 : Extraire les métadonnées des fonctionnalités des LLM

Supposons que vous ayez un extrait de texte décrivant les capacités d’un LLM :

« Qwen3 a un fort 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 voulez 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 fort 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

Alimentez 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. La fenêtre de contexte est de 128K. Langues : Anglais uniquement.
2. GPT-4 Turbo prend en charge le raisonnement. La fenêtre de contexte est de 128K. Langues : Anglais, Japonais.
3. Qwen3 prend en charge le raisonnement. La fenêtre de contexte est 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 de faire le benchmark, visualiser ou filtrer les modèles par leurs fonctionnalités.


Exemple 4 : Détecter automatiquement les lacunes

Vous pouvez même autoriser les valeurs null lorsqu’un champ est manquant :

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, mises en garde et meilleures pratiques

L’utilisation de la sortie structurée via Ollama (ou tout autre système qui le supporte) offre de nombreux avantages — mais comporte également quelques mises en garde.

Avantages

  • Garanties plus fortes : Le modèle est demandé de conformer à un schéma JSON plutôt qu’à du texte libre.
  • Analyse plus facile : Vous pouvez directement utiliser json.loads ou valider avec Pydantic / Zod, plutôt que des regex 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é ascendante.
  • Interopérabilité : Les systèmes en aval attendent des données structurées.
  • Déterminisme (meilleur avec une température faible) : Lorsque la température est faible (ex. 0), le modèle est plus susceptible de respecter rigoureusement le schéma. La documentation d’Ollama recommande cela.

Mises en garde et pièges

  • Incompatibilité de schéma : Le modèle peut toujours dévier — par exemple, manquer une propriété requise, réorganiser les clés ou inclure des champs supplémentaires. Vous avez besoin de validation.
  • Schémas complexes : Les schémas JSON très profonds ou récursifs peuvent embrouiller le modèle ou entraîner des échecs.
  • Ambiguïté dans l’invite : Si votre invite est vague, le modèle peut deviner des champs ou des unités incorrectement.
  • Incohérence entre les modèles : Certains modèles peuvent ê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 à l’invite ou à l’appel API.

Meilleures 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 les schémas JSON. Cela évite les erreurs manuelles.
  • Incluez toujours des instructions comme « répondez en JSON uniquement » ou « n’incluez pas de commentaires ou de texte supplémentaire » dans votre invite.
  • Utilisez temperature = 0 (ou très faible) pour minimiser l’aléatoire et maximiser l’adhésion au schéma. Ollama recommande le déterminisme.
  • Validez et envisagez potentiellement une fallback (ex. réessayer ou nettoyer) lorsque l’analyse du JSON échoue ou lorsque la validation du schéma échoue.
  • Commencez par un schéma plus simple, puis étendez-le progressivement. Ne compliquez pas trop au début.
  • Incluez des instructions d’erreur utiles mais contraintes : par exemple, si le modèle ne peut pas remplir un champ requis, 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 programme Go simple qui demande à Qwen3 une sortie structurée sur 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 fort 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 sérialisation 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 pendant le streaming
		rawResponse += response.Response

		// Analyser uniquement lorsque la réponse est complète
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("Erreur d'analyse JSON : %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Struct analysée : %+v\n", features)
}

Pour compiler et exécuter cet exemple de 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
# construire et exécuter
go build -o ollama-struct main.go
./ollama-struct

Exemple de sortie

Struct analysée : {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}

Exemple Go 2 : Comparaison de plusieurs modèles

Vous pouvez étendre cela pour extraire une liste de modèles pour comparaison.

  type ModelComparison struct {
		Models []LLMFeatures `json:"models"`
	}

	prompt = `
	Extrayez les fonctionnalités des descriptions de modèles suivantes et retournez-les en JSON :

	1. PaLM 2 : Ce modèle a des capacités de raisonnement limitées et se concentre sur la compréhension de base de la langue. Il prend en charge une fenêtre de contexte de 8 000 tokens. Il prend principalement en charge uniquement la langue anglaise.
	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 de fortes capacités de raisonnement 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.

	Retournez un objet JSON avec un tableau "models" contenant tous les modèles.
	`

	// Définir le schéma JSON pour la comparaison de 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 sérialisation 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 pendant le streaming
		comparisonResponse += response.Response

		// Analyser uniquement lorsque la réponse est complète
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("Erreur d'analyse JSON : %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, m := range comp.Models {
		fmt.Printf("%s: Contexte=%d, Langues=%v\n", m.Name, m.MaxContextTokens, m.Languages)
	}

Exemple de sortie

PaLM 2: Contexte=8000, Langues=[English]
LLaMA 2: Contexte=4000, Langues=[English Spanish Italian]
Codex: Contexte=16000, Langues=[English Python JavaScript Java]

Soit dit en passant, qwen3:4b fonctionne bien sur ces exemples, tout comme qwen3:8b.

Meilleures pratiques pour les développeurs Go

  • Définir la température à 0 pour une adhérence maximale au schéma.
  • Valider avec json.Unmarshal et faire une fallback 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 tags de structure Go) si vous attendez des données manquantes.
  • Ajouter des tentatives si le modèle émet occasionnellement du JSON invalide.

Exemple complet - Dessiner un graphique avec les spécifications des LLM (Étape par étape : 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 pouvoir (a) générer 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. Demander à 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 pour chaque modèle. Retournez uniquement du JSON correspondant au schéma.
1) Qwen3 prend en charge la chaîne de pensée ; contexte 128K ; Anglais, Chinois, Français, Espagnol, Arabe.
2) Llama 3.1 prend en charge la chaîne de pensée ; contexte 128K ; Anglais.
3) GPT-4 Turbo prend en charge la chaîne de pensée ; contexte 128K ; 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

Toujours valider avant d’utiliser en production.

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. Construire 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éorganiser les colonnes pour la lisibilité
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# Sauvegarder en CSV pour une utilisation ultérieure
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (Facultatif) Visualisations rapides

Des graphiques simples vous aident à évaluer visuellement les différences entre les modèles rapidement.

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 max")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

En résumé

Avec le nouveau support des sorties structurées d’Ollama, vous pouvez traiter les LLM non seulement comme des chatbots mais 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 du raisonnement, la taille de la fenêtre de contexte et les langues prises en charge — des tâches qui nécessiteraient autrement une analyse fragile.

Que vous construisiez 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

S'abonner

Recevez de nouveaux articles sur les systèmes, l'infrastructure et l'ingénierie IA.