Cours
En août 2024, OpenAI a annoncé une nouvelle fonctionnalité puissante dans son API — les Structured Outputs. Comme son nom l’indique, cette fonctionnalité permet de forcer les LLM à générer des réponses uniquement dans le format que vous spécifiez. Elle simplifie grandement la création d’applications nécessitant un formatage de données précis.
Dans ce tutoriel, vous apprendrez à prendre en main les Structured Outputs d’OpenAI, à comprendre leur nouvelle syntaxe et à explorer leurs principaux usages.
Développer des applications d'IA
L’importance des Structured Outputs dans les applications d’IA
Des réponses déterministes, ou autrement dit, un formatage cohérent, sont essentielles pour de nombreuses tâches comme la saisie de données, la recherche d’informations, les questions-réponses, les workflows multi-étapes, etc. Vous avez peut-être déjà constaté que les LLM peuvent produire des sorties dans des formats très variés, même avec le même prompt.
Par exemple, considérez cette simple fonction classify_sentiment alimentée par GPT-4o :
# List of hotel reviews
reviews = [
"The room was clean and the staff was friendly.",
"The location was terrible and the service was slow.",
"The food was amazing but the room was too small.",
]
# Classify sentiment for each review and print the results
for review in reviews:
sentiment = classify_sentiment(review)
print(f"Review: {review}\nSentiment: {sentiment}\n")
Sortie :
Review: The room was clean and the staff was friendly.
Sentiment: Positive
Review: The location was terrible and the service was slow.
Sentiment: Negative
Review: The food was amazing but the room was too small.
Sentiment: The sentiment of the review is neutral.
Même si les deux premières réponses partagent un format en un seul mot, la dernière est une phrase complète. Si une autre application en aval dépendait de cette sortie, elle se serait arrêtée, car elle attendait un seul mot.
On peut corriger ce problème avec du prompt engineering, mais c’est un processus itératif et chronophage. Même avec un prompt très abouti, rien ne garantit à 100 % que les réponses resteront conformes à notre format à l’avenir. Sauf si, bien sûr, nous utilisons les Structured Outputs :
def classify_sentiment_with_structured_outputs(review):
"""Sentiment classifier with Structured Outputs"""
...
# Classify sentiment for each review with Structured Outputs
for review in reviews:
sentiment = classify_sentiment_with_structured_outputs(review)
print(f"Review: {review}\nSentiment: {sentiment}\n")
Sortie :
Review: The room was clean and the staff was friendly.
Sentiment: {"sentiment":"positive"}
Review: The location was terrible and the service was slow.
Sentiment: {"sentiment":"negative"}
Review: The food was amazing but the room was too small.
Sentiment: {"sentiment":"neutral"}
Avec la nouvelle fonction classify_sentiment_with_structured_outputs, les réponses partagent toutes le même format.
Cette capacité à imposer un format strict aux modèles de langage est déterminante : elle vous fait gagner d’innombrables heures de prompt engineering ou l’intégration d’outils open source tiers.
Bien démarrer avec les Structured Outputs d’OpenAI
Dans cette section, nous allons décortiquer les Structured Outputs avec l’exemple du classificateur de sentiments.
Configuration de l’environnement
Prérequis
Avant de commencer, assurez-vous d’avoir :
- Python 3.7 ou version ultérieure installé sur votre système.
- Une clé d’API OpenAI. Vous pouvez l’obtenir en créant un compte sur le site d’OpenAI.
Mise en place de l’API OpenAI
1. Installez le package Python OpenAI : ouvrez votre terminal et exécutez la commande suivante pour installer ou mettre à jour le package OpenAI à la dernière version :
$ pip install -U openai
2. Configurez votre clé d’API : vous pouvez définir votre clé d’API en variable d’environnement ou directement dans votre code. Pour la définir en variable d’environnement, exécutez :
$ export OPENAI_API_KEY='your-api-key'
3. Vérifiez l’installation : créez un petit script Python pour valider l’installation :
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Say hello!"}
],
max_tokens=5
)
>>> print(response.choices[0].message.content.strip())
Hello! How can I
Exécutez le script pour vous assurer que tout est correctement configuré. Vous devriez voir la réponse du modèle s’afficher dans le terminal.
En plus du package OpenAI, vous aurez besoin de la bibliothèque Pydantic pour définir et valider des schémas JSON pour les Structured Outputs. Installez-la avec pip :
$ pip install pydantic
Avec ces étapes, votre environnement est prêt pour utiliser la fonctionnalité Structured Outputs d’OpenAI.
Définir un schéma de sortie avec Pydantic
Pour utiliser les Structured Outputs, vous devez définir la structure attendue via des modèles Pydantic. Pydantic est une bibliothèque de validation de données et de gestion de paramètres pour Python, qui permet de définir des modèles à l’aide d’annotations de types. Ces modèles servent ensuite à imposer la structure des sorties générées par les modèles d’OpenAI.
Voici un exemple de modèle Pydantic pour spécifier le format de notre classificateur de sentiments de critiques :
from pydantic import BaseModel
from typing import Literal
class SentimentResponse(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
Dans cet exemple :
SentimentResponseest un modèle Pydantic qui définit la structure attendue de la sortie.- Le modèle possède un seul champ
sentiment, qui ne peut prendre que l’une des trois valeurs littérales : "positive", "negative" ou "neutral".
En passant ce modèle dans nos requêtes à l’API d’OpenAI, les sorties seront limitées aux mots fournis.
Voyons comment.
Utiliser le helper parse
Pour imposer notre schéma Pydantic dans les requêtes OpenAI, il suffit de le transmettre au paramètre response_format de l’API Chat Completions. En gros, cela ressemble à :
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[...],
response_format=SentimentResponse
)
Comme vous le remarquez, au lieu d’utiliser client.chat.completions.create, nous utilisons la méthode client.beta.chat.completions.parse. .parse() est une nouvelle méthode de l’API Chat Completions spécifiquement conçue pour les Structured Outputs.
Maintenant, regroupons tout en réécrivant le classificateur de sentiments avec des Structured Outputs. D’abord, nous effectuons les imports nécessaires, définissons le modèle Pydantic, le prompt système et un modèle de prompt :
from openai import OpenAI
from pydantic import BaseModel
from typing import Literal
class SentimentResponse(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
client = OpenAI()
MODEL = "gpt-4o-mini"
SYSTEM_PROMPT = "You are a sentiment classifier assistant."
PROMPT_TEMPLATE = """
Classify the sentiment of the following hotel review as positive, negative, or neutral:\n\n{review}
"""
Ensuite, nous écrivons une nouvelle fonction qui utilise le helper .parse() :
# Function to classify sentiment using OpenAI's chat completions API with structured outputs
def classify_sentiment_with_structured_outputs(review):
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": PROMPT_TEMPLATE.format(review=review)},
],
response_format=SentimentResponse
)
return response.choices[0].message
La ligne clé de la fonction est response_format=SentimentResponse, qui active effectivement les Structured Outputs.
Testons-la sur l’une des critiques :
# List of hotel reviews
reviews = [
"The room was clean and the staff was friendly.",
"The location was terrible and the service was slow.",
"The food was amazing but the room was too small.",
]
result = classify_sentiment_with_structured_outputs(reviews[0])
>>> print(result.content)
{"sentiment":"positive"}
Ici, result est un objet message :
>>> type(result)
openai.types.chat.parsed_chat_completion.ParsedChatCompletionMessage[SentimentResponse]
Outre son attribut .content, qui récupère la réponse, il possède un attribut .parsed qui renvoie les informations parsées sous forme de classe :
>>> result.parsed
SentimentResponse(sentiment='positive')
Comme vous le voyez, nous obtenons une instance de la classe SentimentResponse. Nous pouvons donc accéder au sentiment comme à une chaîne (et non un dictionnaire) via l’attribut .sentiment :
>>> result.parsed.sentiment
'positive'
Imbriquer des modèles Pydantic pour définir des schémas complexes
Dans certains cas, vous devez définir des structures de sortie plus complexes impliquant des données imbriquées. Pydantic permet d’imbriquer des modèles les uns dans les autres, ce qui vous aide à créer des schémas élaborés pour de nombreux cas d’usage. C’est particulièrement utile avec des données hiérarchiques ou lorsque vous devez imposer une structure spécifique à des sorties complexes.
Prenons un exemple où l’on doit extraire des informations détaillées sur un utilisateur : son nom, ses coordonnées et une liste d’adresses. Chaque adresse doit inclure la rue, la ville, l’État et le code postal. Cela requiert plusieurs modèles Pydantic pour construire le bon schéma.
Étape 1 : définir les modèles Pydantic
Commençons par définir les modèles Pydantic pour l’adresse et les informations utilisateur :
from pydantic import BaseModel
from typing import List
# Define the Pydantic model for an address
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str
# Define the Pydantic model for user information
class UserInfo(BaseModel):
name: str
email: str
phone: str
addresses: List[Address]
Dans cet exemple :
Addressest un modèle Pydantic qui définit la structure d’une adresse.UserInfoest un modèle Pydantic qui inclut une liste d’objetsAddress, ainsi que des champs pour le nom, l’email et le numéro de téléphone.
Étape 2 : utiliser les modèles imbriqués dans les appels d’API
Ensuite, utilisons ces modèles imbriqués pour imposer la structure de sortie dans un appel à l’API d’OpenAI :
SYSTEM_PROMPT = "You are a user information extraction assistant."
PROMPT_TEMPLATE = """ Extract the user information from the following text:\n\n{text}"""
# Function to extract user information using OpenAI's chat completions API with structured outputs
def extract_user_info(text):
response = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": PROMPT_TEMPLATE.format(text=text)},
],
response_format=UserInfo
)
return response.choices[0].message
# Example text containing user information
text = """John DoeEmail: john.doe@example.comPhone: 123-456-7890Addresses:- 123 Main St, Springfield, IL, 62701- 456 Elm St, Shelbyville, IL, 62702"""
# Extract user information and print the results
user_info = extract_user_info(text)
Le texte d’exemple est illisible et ne comporte aucun espace entre des éléments clés. Voyons si le modèle s’en sort. Nous allons utiliser la bibliothèque json pour afficher proprement la réponse :
import json
data = json.loads(user_info.content)
pretty_response = json.dumps(data, indent=2)
print(pretty_response)
{
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "123-456-7890",
"addresses": [
{
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"zip_code": "62701"
},
{
"street": "456 Elm St",
"city": "Shelbyville",
"state": "IL",
"zip_code": "62702"
}
]
}
Comme vous pouvez le voir, le modèle a correctement extrait les informations d’un utilisateur et ses deux adresses distinctes selon le schéma fourni.
En bref, en imbriquant des modèles Pydantic, vous pouvez définir des schémas complexes qui gèrent des données hiérarchiques et imposent des structures précises à des sorties élaborées.
Function calling avec Structured Outputs
L’une des fonctionnalités largement adoptées des modèles récents est le function calling (également appelé tool calling). Cette capacité permet de connecter les modèles de langage à des fonctions définies par l’utilisateur, leur donnant effectivement un accès au monde extérieur.
Parmi les exemples courants :
- Récupérer des données en temps réel (météo, cours boursiers, scores sportifs, etc.)
- Effectuer des calculs ou des analyses de données
- Interroger des bases de données ou des API
- Générer des images ou d’autres médias
- Traduire du texte entre langues
- Contrôler des objets connectés ou systèmes IoT
- Exécuter une logique ou des workflows métiers personnalisés
Nous n’entrerons pas ici dans le détail du function calling, mais vous pouvez consulter notre tutoriel OpenAI Function Calling.
Ce qu’il faut savoir, c’est qu’avec les Structured Outputs, le function calling avec les modèles OpenAI devient bien plus simple. Auparavant, les fonctions transmises aux modèles OpenAI exigeaient d’écrire à la main des schémas JSON complexes, détaillant chaque paramètre avec ses types. Voici un exemple :
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the users location.",
},
},
"required": ["location", "format"],
},
}
}
Même si la fonction get_current_weather n’a que deux paramètres, son schéma JSON devient énorme et source d’erreurs lorsqu’il est rédigé manuellement.
Avec les Structured Outputs, on résout cela en réutilisant des modèles Pydantic :
from pydantic import BaseModel
from typing import Literal
def get_weather(location: str, unit: str, condition: str):
# Implementation details...
pass
class WeatherData(BaseModel):
location: str
unit: Literal["celsius", "fahrenheit"]
condition: Literal["sunny", "cloudy", "rainy", "snowy"]
D’abord, vous écrivez la fonction et sa logique. Puis vous la redéfinissez via un modèle Pydantic en spécifiant les paramètres d’entrée attendus.
Ensuite, pour convertir le modèle Pydantic en schéma JSON compatible, appelez pydantic_function_tool :
>>> import openai
>>> openai.pydantic_function_tool(WeatherData)
{'type': 'function',
'function': {'name': 'WeatherData',
'strict': True,
'parameters': {'properties': {'location': {'title': 'Location',
'type': 'string'},
'unit': {'enum': ['celsius', 'fahrenheit'],
'title': 'Unit',
'type': 'string'},
'condition': {'enum': ['sunny', 'cloudy', 'rainy', 'snowy'],
'title': 'Condition',
'type': 'string'}},
'required': ['location', 'unit', 'condition'],
'title': 'WeatherData',
'type': 'object',
'additionalProperties': False}}}
Voici comment utiliser cet outil dans une requête :
import openai
client = OpenAI()
tools = [openai.pydantic_function_tool(WeatherData)]
messages = [
{
"role": "system",
"content": "You are a helpful customer support assistant. Use the supplied tools to assist the user.",
},
{
"role": "user",
"content": "What is the weather in Tokyo?",
}
]
response = client.chat.completions.create(
model=MODEL, messages=messages, tools=tools
)
tool_call = response.choices[0].message.tool_calls[0]
>>> tool_call
ChatCompletionMessageToolCall(id='call_QnZZ0DmNN2cxw3bN433JQNIC', function=Function(arguments='{"location":"Tokyo","unit":"celsius","condition":"sunny"}', name='WeatherData'), type='function')
Nous passons le modèle Pydantic au format JSON compatible dans le paramètre tools de l’API Chat Completions. Ensuite, selon la requête, le modèle décide d’appeler l’outil ou non.
Étant donné notre question « What is the weather in Tokyo? », on observe un appel dans tool_calls de l’objet message retourné.
Rappelez-vous : le modèle n’appelle pas la fonction get_weather, mais génère les arguments pour celle-ci à partir du schéma Pydantic fourni :
arguments = json.loads(tool_call.function.arguments)
>>> arguments
{'location': 'Tokyo', 'unit': 'celsius', 'condition': 'sunny'}
Il nous revient ensuite d’appeler la fonction avec les arguments fournis :
some_result = get_weather(**arguments)
Si vous souhaitez que le modèle génère les arguments et appelle la fonction dans la foulée, vous cherchez un agent d’IA.
Nous avons un tutoriel LangChain Agents dédié si cela vous intéresse.
Bonnes pratiques avec les Structured Outputs d’OpenAI
Lorsque vous utilisez les Structured Outputs, gardez à l’esprit plusieurs bonnes pratiques et recommandations. Nous en listons quelques-unes ci-dessous.
- Utilisez des modèles Pydantic pour définir les schémas de sortie : c’est une manière claire et typée de spécifier les structures attendues.
- Gardez des schémas simples et spécifiques pour obtenir des résultats plus précis.
- Employez des types adaptés (
str,int,float,bool,List,Dict) pour représenter fidèlement vos données. - Utilisez des types
Literalpour les enums afin de définir des valeurs autorisées spécifiques. - Gérez les refus du modèle. Avec la nouvelle méthode
.parse(), les objets message disposent d’un nouvel attribut.refusalpour indiquer un refus :
text = """John DoeEmail: john.doe@example.comPhone: 123-456-7890Addresses:- 123 Main St, Springfield, IL, 62701- 456 Elm St, Shelbyville, IL, 62702"""
user_info = extract_user_info(text)
if user_info.refusal:
print(user_info.refusal)
else:
print(user_info.content)
Sortie :
{"name":"John Doe","email":"john.doe@example.com","phone":"123-456-7890","addresses":[{"street":"123 Main St","city":"Springfield","state":"IL","zip_code":"62701"},{"street":"456 Elm St","city":"Shelbyville","state":"IL","zip_code":"62702"}]}
6. Fournissez des descriptions claires et concises pour chaque champ de vos modèles Pydantic afin d’améliorer la précision des sorties :
from pydantic import BaseModel, Field
class Person(BaseModel):
name: str = Field(..., description="The person's full name")
age: int = Field(..., description="The person's age in years")
occupation: str = Field(..., description="The person's current job or profession")
Ces pratiques vous aideront à tirer le meilleur parti des Structured Outputs dans vos applications.
Conclusion
Dans ce tutoriel, nous avons découvert une nouvelle fonctionnalité de l’API OpenAI : les Structured Outputs. Nous avons vu comment elle force les modèles de langage à produire des sorties dans le format que nous spécifions. Nous avons appris à l’utiliser avec le function calling et passé en revue quelques bonnes pratiques pour en tirer le maximum.
Voici quelques ressources complémentaires pour approfondir :
Obtenez une certification de haut niveau en matière d'IA
FAQ sur les Structured Outputs
Comment les modèles Pydantic fonctionnent-ils avec les Structured Outputs ?
Les modèles Pydantic servent à définir le schéma de la structure de sortie souhaitée, qui est ensuite transmis à l’API d’OpenAI pour imposer le format de réponse.
Peut-on utiliser les Structured Outputs avec le function calling ?
Oui, les Structured Outputs peuvent être utilisés avec le function calling pour simplifier la définition des paramètres et des sorties attendues.
Quels sont les avantages des Structured Outputs ?
Les avantages incluent des formats de réponse cohérents, moins de post-traitements, une fiabilité accrue des applications d’IA et une intégration facilitée aux systèmes existants.
Existe-t-il des limitations à l’usage des Structured Outputs ?
Bien que puissants, les Structured Outputs peuvent réduire la flexibilité des réponses du modèle et nécessitent une conception de schéma soignée pour équilibrer structure et niveau de détail souhaité.
Je suis créateur de contenu en science des données avec plus de 2 ans d’expérience et l’une des plus grandes audiences sur Medium. J’aime écrire des articles détaillés sur l’IA et le ML avec une pointe de sarcasme, histoire de les rendre un peu moins austères. J’ai publié plus de 130 articles et un cours DataCamp, avec un autre en préparation. Mes contenus ont été vus par plus de 5 millions de personnes, dont 20 000 sont devenues abonnées sur Medium et LinkedIn.
