Lab 017 : Sortie structurée & mode JSON¶
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 :
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¶
- Utiliser la sortie structurée dans un outil MCP : → Lab 020 — Serveur MCP en Python
- Utiliser avec les résultats de fonctions Semantic Kernel : → Lab 023 — Plugins, mémoire et planificateurs SK