0% read

Sortie structurée Gemma 4 : comment obtenir du JSON fiable à chaque fois

avr. 7, 2026

Si vous construisez une application au-dessus de Gemma 4, vous avez besoin d'une sortie structurée — pas de texte libre. Vous avez besoin de JSON que vous pouvez parser, valider et injecter dans votre base de données ou API. À chaque fois, sans exception.

C'est l'une des parties les plus délicates du travail avec les LLM locaux, mais avec les bonnes techniques, Gemma 4 peut être étonnamment fiable. Passons en revue chaque méthode.

Pourquoi la sortie structurée compte

Quand vous utilisez Gemma 4 comme composant d'un système plus large — pas juste pour discuter avec — vous avez besoin d'une sortie prévisible :

# C'est ce que vous voulez :
{"sentiment": "positive", "confidence": 0.92, "topics": ["pricing", "support"]}

# Ce n'est pas ce que vous voulez :
"Le sentiment de ce texte est positif, avec une confiance d'environ 92%..."

Le premier peut être parsé et utilisé de manière programmatique. Le second nécessite un autre round de parsing, ce qui ajoute de la latence, du coût et des points de défaillance.

Méthode 1 : technique du system prompt

L'approche la plus simple — dire au modèle exactement ce que vous voulez dans le system prompt :

import requests
import json

response = requests.post("http://localhost:11434/api/chat", json={
    "model": "gemma4:26b",
    "messages": [
        {
            "role": "system",
            "content": """Tu es une API qui répond UNIQUEMENT en JSON.
Tu DOIS répondre avec du JSON valide et rien d'autre.
Pas de markdown, pas d'explication, pas de blocs de code — juste du JSON brut.

Schéma :
{
  "sentiment": "positive" | "negative" | "neutral",
  "confidence": nombre entre 0 et 1,
  "topics": string[],
  "summary": string (une phrase)
}"""
        },
        {
            "role": "user",
            "content": "Analyse : 'La nouvelle mise à jour est incroyable ! L'interface est tellement plus propre et tout charge plus vite. Seule plainte : le prix a augmenté.'"
        }
    ],
    "stream": False,
})

result = json.loads(response.json()["message"]["content"])
print(result)

Ça fonctionne la plupart du temps. Mais « la plupart du temps » n'est pas suffisant pour la production. Le modèle pourrait occasionnellement ajouter un préambule comme « Voici le JSON : » ou encapsuler la sortie dans des blocs de code markdown.

Méthode 2 : paramètre format d'Ollama

Ollama a un paramètre format intégré qui contraint la sortie à du JSON valide :

response = requests.post("http://localhost:11434/api/chat", json={
    "model": "gemma4:26b",
    "messages": [
        {
            "role": "system",
            "content": "Analyse le sentiment du texte donné. Retourne : sentiment (positive/negative/neutral), confidence (0-1), topics (liste), summary (une phrase)."
        },
        {
            "role": "user",
            "content": "Le service client était terrible mais le produit lui-même est excellent."
        }
    ],
    "format": "json",
    "stream": False,
})

# C'est garanti d'être du JSON valide
result = response.json()["message"]["content"]
parsed = json.loads(result)

Le flag format: "json" indique à Ollama de contraindre la génération de tokens pour ne produire que du JSON valide. C'est beaucoup plus fiable que le prompt engineering seul.

Limitation : Cela garantit une syntaxe JSON valide, mais ne garantit pas le schéma. Le modèle pourrait retourner {"answer": "positive"} au lieu de votre format attendu. Vous avez toujours besoin de validation.

Méthode 3 : définition de schéma avec Pydantic

Pour le code de production, définissez votre schéma attendu avec Pydantic et validez contre lui :

from pydantic import BaseModel, Field
from typing import Literal
import json
import requests

class SentimentResult(BaseModel):
    sentiment: Literal["positive", "negative", "neutral"]
    confidence: float = Field(ge=0, le=1)
    topics: list[str]
    summary: str

def analyze_sentiment(text: str) -> SentimentResult:
    schema_str = json.dumps(SentimentResult.model_json_schema(), indent=2)
    
    response = requests.post("http://localhost:11434/api/chat", json={
        "model": "gemma4:26b",
        "messages": [
            {
                "role": "system",
                "content": f"""Réponds avec du JSON correspondant exactement à ce schéma :
{schema_str}

Aucun autre texte. Juste du JSON valide."""
            },
            {
                "role": "user",
                "content": f"Analyse ce texte : {text}"
            }
        ],
        "format": "json",
        "stream": False,
    })
    
    raw = json.loads(response.json()["message"]["content"])
    return SentimentResult.model_validate(raw)

# Utilisation
result = analyze_sentiment("Super produit, temps de livraison horrible.")
print(f"Sentiment : {result.sentiment} ({result.confidence:.0%})")
print(f"Sujets : {', '.join(result.topics)}")

Cela vous donne la sécurité de type et la validation. Si le modèle retourne quelque chose d'inattendu, Pydantic lève une erreur claire au lieu de corrompre silencieusement vos données.

Méthode 4 : pattern de validation et retry

Pour une fiabilité maximale, ajoutez une boucle de retry :

from pydantic import BaseModel, ValidationError
import json
import requests
import time

def get_structured_output(
    prompt: str,
    schema_class: type[BaseModel],
    model: str = "gemma4:26b",
    max_retries: int = 3,
) -> BaseModel:
    schema_str = json.dumps(schema_class.model_json_schema(), indent=2)
    
    for attempt in range(max_retries):
        try:
            response = requests.post("http://localhost:11434/api/chat", json={
                "model": model,
                "messages": [
                    {
                        "role": "system",
                        "content": f"Réponds UNIQUEMENT avec du JSON correspondant à ce schéma :\n{schema_str}"
                    },
                    {"role": "user", "content": prompt}
                ],
                "format": "json",
                "stream": False,
                "options": {
                    "temperature": 0.1 if attempt == 0 else 0.3,
                },
            })
            
            raw = json.loads(response.json()["message"]["content"])
            return schema_class.model_validate(raw)
            
        except (json.JSONDecodeError, ValidationError) as e:
            if attempt == max_retries - 1:
                raise ValueError(
                    f"Échec d'obtention d'une sortie valide après {max_retries} tentatives : {e}"
                )
            time.sleep(0.5)
    
    raise ValueError("Inaccessible")

# Utilisation
class ProductReview(BaseModel):
    rating: int = Field(ge=1, le=5)
    pros: list[str]
    cons: list[str]
    recommendation: bool

review = get_structured_output(
    "Avis : 'Laptop solide, super clavier, la batterie pourrait être meilleure. 4/5, je rachèterais.'",
    ProductReview,
)

Choix de conception clés :

  • Commencez avec une température basse (0,1) pour la cohérence, augmentez lors des retries pour la variété
  • Utilisez format: "json" pour garantir une syntaxe JSON valide
  • Validez avec Pydantic pour la correction du schéma
  • Limitez les retries à 3 — si ça échoue 3 fois, le prompt a probablement besoin de travail

Échecs courants et solutions

Le modèle encapsule le JSON dans du markdown :

```json
{"key": "value"}

Solution : Utilisez `format: "json"` dans Ollama. Si ce n'est pas disponible, retirez le markdown :

```python
def clean_json(text: str) -> str:
    text = text.strip()
    if text.startswith("```"):
        text = text.split("\n", 1)[1]  # Retirer la première ligne
        text = text.rsplit("```", 1)[0]  # Retirer le dernier ```
    return text.strip()

Le modèle ajoute des champs supplémentaires :

Le modèle pourrait retourner des champs que vous n'avez pas demandés. Pydantic gère cela — par défaut il ignore les champs supplémentaires. Ou définissez model_config = ConfigDict(extra="forbid") pour les rejeter.

Le modèle utilise de mauvais types :

Parfois le modèle retourne "0.92" (string) au lieu de 0.92 (nombre). model_validate de Pydantic gère automatiquement la plupart des coercitions de type.

Champs vides ou nuls :

Rendez les champs optionnels quand ils pourraient être vides :

class Result(BaseModel):
    name: str
    email: str | None = None  # Le modèle pourrait ne pas trouver d'email
    topics: list[str] = []    # Par défaut liste vide

Objets imbriqués :

Gemma 4 gère bien le JSON imbriqué, mais gardez l'imbrication à 2-3 niveaux max :

class Address(BaseModel):
    city: str
    country: str

class Person(BaseModel):
    name: str
    age: int
    address: Address  # Un niveau d'imbrication — OK

Conseils de performance

  • Température plus basse (0,1-0,3) produit un JSON plus cohérent
  • Schémas plus courts obtiennent une meilleure conformité — ne demandez pas 20 champs d'un coup
  • Exemples few-shot dans le system prompt améliorent dramatiquement la fiabilité
  • Le modèle 26B est significativement meilleur pour le JSON que E4B — voir comparaison des modèles
  • Le mode thinking aide avec les schémas complexes — voir guide du mode thinking

Prochaines étapes

gemma4 — interact

Stop reading. Start building.

~/gemma4 $ Get hands-on with the models discussed in this guide. No deployment, no friction, 100% free playground.

Launch Playground />
Gemma 4 AI

Gemma 4 AI

Related Guides

Sortie structurée Gemma 4 : comment obtenir du JSON fiable à chaque fois | Blog