Aller au contenu

Lab 017 : Sortie structurée & mode JSON

Niveau : L100 Parcours : Tous les parcours Durée : ~25 min 💰 Coût : GitHub Free — Compte GitHub gratuit, sans carte de crédit

Ce que vous apprendrez

  • Pourquoi la sortie non structurée des LLM est fragile dans les systèmes d'agents
  • Comment utiliser le mode JSON pour forcer une sortie JSON valide
  • Comment définir des schémas avec Pydantic (Python) et des classes C#
  • Comment utiliser la sortie structurée avec l'API OpenAI
  • Modèles pratiques : extraction, classification, sortie de fonction

Introduction

Dans les systèmes d'agents en production, vous affichez rarement le texte du LLM directement aux utilisateurs. Vous l'analysez, le stockez dans des bases de données, le transmettez à d'autres services ou déclenchez des actions en fonction de celui-ci.

Le problème : les LLM sont bavards. Demandez du JSON et vous pourriez obtenir :

Sure! Here's the JSON you asked for:
```json
{"name": "hiking boots", "price": 129.99}
I hope that helps!
Maintenant votre analyseur JSON plante à cause du texte supplémentaire. C'est un vrai problème que la sortie structurée résout complètement.

---

## Prérequis et configuration

```bash
pip install openai pydantic

Définissez GITHUB_TOKEN depuis le Lab 013.


Exercice du lab

Étape 1 : Le problème — analyser une sortie non structurée

import os, json
from openai import OpenAI

client = OpenAI(
    base_url="https://models.inference.ai.azure.com",
    api_key=os.environ["GITHUB_TOKEN"],
)

# BAD approach - asking for JSON in the prompt
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{
        "role": "user",
        "content": "Extract the product info as JSON: 'The ProTrek X200 hiking boots cost $189.99 and come in black.'"
    }],
)

text = response.choices[0].message.content
print(text)  # May include "Sure! Here's the JSON: ```json ... ```"

# This will often FAIL:
try:
    data = json.loads(text)
except json.JSONDecodeError:
    print("Parse failed! LLM added extra text.")

Étape 2 : Mode JSON — JSON valide garanti

Le mode JSON force le modèle à produire uniquement du JSON valide, rien d'autre.

response = client.chat.completions.create(
    model="gpt-4o-mini",
    response_format={"type": "json_object"},  # ← Enable JSON mode
    messages=[
        {
            "role": "system",
            "content": "You are a data extractor. Always respond with valid JSON only."
        },
        {
            "role": "user",
            "content": "Extract product info from: 'The ProTrek X200 hiking boots cost $189.99 and come in black.'"
        }
    ],
)

text = response.choices[0].message.content
data = json.loads(text)  # Always succeeds now
print(data)
# {"name": "ProTrek X200", "type": "hiking boots", "price": 189.99, "colors": ["black"]}

Exigence du mode JSON

Lors de l'utilisation du mode json_object, votre message système ou utilisateur doit mentionner le mot « JSON » — sinon l'API renvoie une erreur.

Étape 3 : Sortie structurée avec schéma Pydantic

Le mode JSON vous donne du JSON valide, mais pas nécessairement la forme souhaitée. La sortie structurée avec un schéma impose les champs et types exacts.

from pydantic import BaseModel
from openai import OpenAI
import os

client = OpenAI(
    base_url="https://models.inference.ai.azure.com",
    api_key=os.environ["GITHUB_TOKEN"],
)

# Define the schema
class ProductInfo(BaseModel):
    name: str
    category: str
    price: float
    colors: list[str]
    in_stock: bool

# Parse with structured output
response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "Extract product information accurately."},
        {"role": "user", "content": "The ProTrek X200 hiking boots cost $189.99, come in black and brown, and are currently available."},
    ],
    response_format=ProductInfo,  # ← Pass the Pydantic model
)

product = response.choices[0].message.parsed  # Already a ProductInfo object!
print(product.name)      # "ProTrek X200"
print(product.price)     # 189.99
print(product.colors)    # ["black", "brown"]
print(product.in_stock)  # True
using OpenAI.Chat;
using System.Text.Json;

// Define the schema as a C# class
public class ProductInfo
{
    public string Name { get; set; } = "";
    public string Category { get; set; } = "";
    public decimal Price { get; set; }
    public List<string> Colors { get; set; } = new();
    public bool InStock { get; set; }
}

// Use JSON mode + deserialize
var client = new ChatClient(
    model: "gpt-4o-mini",
    apiKey: Environment.GetEnvironmentVariable("GITHUB_TOKEN"),
    options: new OpenAIClientOptions { Endpoint = new Uri("https://models.inference.ai.azure.com") }
);

var completion = await client.CompleteChatAsync(
    new SystemChatMessage("Extract product information. Respond with JSON only."),
    new UserChatMessage("The ProTrek X200 hiking boots cost $189.99, come in black and brown, and are available.")
);

var product = JsonSerializer.Deserialize<ProductInfo>(completion.Value.Content[0].Text);
Console.WriteLine($"{product.Name}: ${product.Price}");

Étape 4 : Modèles pratiques

Modèle 1 — Classification

from pydantic import BaseModel
from typing import Literal

class SupportTicket(BaseModel):
    category: Literal["billing", "shipping", "returns", "technical", "other"]
    priority: Literal["low", "medium", "high", "urgent"]
    summary: str
    requires_human: bool

response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "Classify support tickets accurately."},
        {"role": "user", "content": "My order arrived broken and I need a replacement ASAP for my daughter's birthday tomorrow."},
    ],
    response_format=SupportTicket,
)

ticket = response.choices[0].message.parsed
print(f"Category: {ticket.category}")       # "shipping" or "returns"
print(f"Priority: {ticket.priority}")       # "urgent"
print(f"Human needed: {ticket.requires_human}")  # True

Modèle 2 — Extraction avec objets imbriqués

from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    state: str
    zip_code: str

class OrderDetails(BaseModel):
    order_id: str
    customer_name: str
    shipping_address: Address
    items: list[str]
    total: float

# Extract structured data from unstructured text
text = """
Hi, I'm John Smith, order #ORD-2024-1234.
I ordered a tent and sleeping bag. Total was $289.98.
Ship to 123 Main St, Seattle, WA 98101.
"""

Modèle 3 — Sortie d'outil d'agent

Utilisez la sortie structurée pour les valeurs de retour des outils MCP afin de rendre l'analyse fiable :

class SearchResult(BaseModel):
    products: list[dict]
    total_found: int
    has_more: bool
    suggested_query: str | None = None

@mcp.tool()
def search_products(query: str) -> dict:
    """Search the product catalog."""
    # ... do the search ...
    result = SearchResult(
        products=found_products,
        total_found=len(all_matches),
        has_more=len(all_matches) > 10,
    )
    return result.model_dump()  # Pydantic → dict → JSON

Étape 5 : Temperature = 0 pour les tâches structurées

Lors de l'extraction de données structurées, utilisez toujours temperature=0 :

response = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    temperature=0,          # ← Deterministic for extraction
    messages=[...],
    response_format=MySchema,
)

L'extraction est une tâche factuelle — vous voulez la même réponse à chaque fois, pas une variation créative.


Résumé

Approche Quand l'utiliser Python
Prompt uniquement Jamais en production ❌ Fragile
Mode JSON JSON simple, pas de schéma strict response_format={"type": "json_object"}
Sortie structurée Schéma exact requis response_format=MyPydanticModel

La règle d'or : toute sortie de LLM que votre code analysera devrait utiliser la sortie structurée.


Prochaines étapes