Aller au contenu principal

Bonnes pratiques de production

Mis à jour le 29 juillet 2026

Bonnes pratiques de production

Cette leçon rassemble les bonnes pratiques essentielles pour déployer une application basée sur l’API OpenAI en production. C’est votre checklist avant de mettre en service. Les leçons précédentes vous ont donné les briques — erreurs, rate limits, résilience, observabilité, coûts ; il s’agit maintenant de les assembler dans une application qu’un collègue pourra reprendre et exploiter sans vous.

1. Architecture du client

Singleton avec configuration centralisée

Instancier un client OpenAI à chaque appel gaspille la connexion HTTP sous-jacente et, plus grave, disperse la configuration : vous vous retrouvez avec un timeout de trente secondes ici et le défaut ailleurs, sans savoir lequel s’applique en production. Le décorateur lru_cache garantit qu’une seule instance existe, configurée en un seul endroit.

from openai import OpenAI
from functools import lru_cache

@lru_cache(maxsize=1)
def get_client() -> OpenAI:
    """Client singleton avec configuration de production."""
    return OpenAI(
        max_retries=3,
        timeout=30.0,
    )

# Partout dans votre code :
client = get_client()

Variables d’environnement

Le même principe s’applique aux réglages qui varient d’un environnement à l’autre. Vérifiez la présence des variables obligatoires au démarrage : mieux vaut un service qui refuse de se lancer avec un message clair qu’un service qui démarre et échoue sur le premier appel utilisateur. Modèle, plafond de tokens et température deviennent alors ajustables sans redéploiement.

import os

# OBLIGATOIRE : ne jamais mettre de clés en dur
REQUIRED_ENV = ["OPENAI_API_KEY"]

for var in REQUIRED_ENV:
    if not os.environ.get(var):
        raise RuntimeError(f"Variable {var} manquante")

# Configuration par environnement
MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.6-terra")
MAX_TOKENS = int(os.environ.get("OPENAI_MAX_TOKENS", "4096"))
TEMPERATURE = float(os.environ.get("OPENAI_TEMPERATURE", "0.7"))

2. Gestion des prompts

Séparer les prompts du code

Un prompt est un actif métier, au même titre qu’une règle de gestion. Le noyer dans une f-string au milieu d’une fonction le rend impossible à relire pour la personne qui connaît le métier mais pas Python. Isolez-le sous forme de gabarit paramétré : la fonction se réduit alors au formatage et à l’appel, et la température à 0.0 rend la classification reproductible.

# prompts/classification.txt
PROMPT_CLASSIFICATION = """Vous êtes un classifieur de texte.
Classifiez le texte suivant dans une des catégories :
{categories}

Texte : {texte}

Répondez uniquement avec le nom de la catégorie."""

def classifier(texte: str, categories: list[str]) -> str:
    prompt = PROMPT_CLASSIFICATION.format(
        categories=", ".join(categories),
        texte=texte
    )
    response = get_client().responses.create(
        model=MODEL,
        input=prompt,
        temperature=0.0
    )
    return response.output_text.strip()

Versionner les prompts

Un prompt modifié change le comportement du système aussi sûrement qu’un changement de code, mais sans qu’aucun test unitaire ne s’en aperçoive. Conservez donc les versions successives et choisissez celle qui est active par configuration. Vous pouvez ainsi comparer deux formulations sur du trafic réel, et revenir à la précédente en une variable d’environnement si la nouvelle dégrade les résultats.

PROMPTS = {
    "classification_v1": "Classifiez : {texte}",
    "classification_v2": "Analysez et classifiez précisément : {texte}",
}

# En production, utilisez une version spécifique
ACTIVE_PROMPT = os.environ.get("PROMPT_VERSION", "classification_v2")

3. Timeouts et limites

Toute entrée venant de l’extérieur doit être bornée avant d’atteindre l’API. Un utilisateur qui colle un PDF de deux cents pages dans votre champ de saisie ne doit pas pouvoir déclencher un appel à plusieurs euros ni faire échouer la requête sur un dépassement de contexte. La troncature préventive, associée au plafond de sortie et à la température issus de la configuration, rend le coût et la latence de chaque appel prévisibles.

def appel_avec_garde_fous(prompt: str) -> str:
    """Appel API avec toutes les protections."""
    # Limite la taille du prompt
    MAX_PROMPT_CHARS = 50000
    if len(prompt) > MAX_PROMPT_CHARS:
        prompt = prompt[:MAX_PROMPT_CHARS]
        print(f"Prompt tronqué a {MAX_PROMPT_CHARS} caractères")

    response = get_client().responses.create(
        model=MODEL,
        input=prompt,
        max_output_tokens=MAX_TOKENS,
        temperature=TEMPERATURE,
    )

    return response.output_text

4. Fallback entre modèles

Quand un modèle est saturé ou momentanément indisponible, rendre une erreur n’est pas la seule option. Basculer vers un modèle secondaire dégrade la qualité mais préserve le service — un arbitrage souvent préférable pour un assistant de support, moins pour une analyse contractuelle. La boucle parcourt les modèles dans l’ordre de préférence et ne lève une exception que si tous ont échoué.

from openai import RateLimitError, InternalServerError

def appel_avec_fallback(prompt: str) -> str:
    """Essaie Terra puis bascule sur Luna si erreur."""
    modeles = ["gpt-5.6-terra", "gpt-5.6-luna"]

    for modele in modeles:
        try:
            response = get_client().responses.create(
                model=modele,
                input=prompt
            )
            return response.output_text
        except (RateLimitError, InternalServerError) as e:
            print(f"{modele} indisponible : {e}")
            continue

    raise RuntimeError("Tous les modèles sont indisponibles")

5. Traitement par lots (batch)

Pour traiter mille documents, l’appel séquentiel est trop lent et l’appel simultané fait exploser vos rate limits. Le sémaphore fixe le juste milieu. Deux détails font la solidité de cette implémentation : return_exceptions=True empêche qu’un seul échec annule tout le lot, et le tri par index restitue l’ordre d’origine, qu’asyncio.gather ne garantit pas dans le temps d’exécution.

import asyncio
from openai import AsyncOpenAI

async def traiter_batch(prompts: list[str], max_concurrent: int = 5) -> list:
    """Traite un lot de prompts avec concurrence limitée."""
    client = AsyncOpenAI()
    semaphore = asyncio.Semaphore(max_concurrent)
    resultats = []

    async def traiter_un(index: int, prompt: str):
        async with semaphore:
            response = await client.responses.create(
                model="gpt-5.6-terra",
                input=prompt
            )
            return index, response.output_text

    tasks = [traiter_un(i, p) for i, p in enumerate(prompts)]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    # Trier par index pour maintenir l'ordre
    sorted_results = sorted(
        [(idx, res) for idx, res in results if not isinstance(res, Exception)],
        key=lambda x: x[0]
    )
    return [res for _, res in sorted_results]

# prompts = ["Question 1", "Question 2", "Question 3"]
# résultats = asyncio.run(traiter_batch(prompts))

6. Tests et validation

Deux tests différents sont nécessaires. Le premier vérifie la plomberie — la clé fonctionne, le réseau passe, le modèle répond — et se place utilement dans un health check de démarrage. Le second, plus original, teste la qualité du prompt lui-même : on soumet un cas dont on connaît la bonne réponse et on vérifie qu’elle apparaît. Ces assertions constituent votre filet de sécurité contre les régressions le jour où vous reformulerez un gabarit.

def test_api_disponible():
    """Vérifie que l'API est accessible."""
    try:
        response = get_client().responses.create(
            model="gpt-5.6-terra",
            input="test",
            max_output_tokens=5
        )
        assert response.output_text is not None
        print("API OK")
        return True
    except Exception as e:
        print(f"API KO : {e}")
        return False

def test_prompt_quality(prompt: str, attendu: str) -> bool:
    """Vérifie qu'un prompt retourne un résultat cohérent."""
    response = get_client().responses.create(
        model="gpt-5.6-terra",
        input=prompt,
        temperature=0.0
    )
    resultat = response.output_text.lower()
    return attendu.lower() in resultat

# Tests de régression des prompts
assert test_prompt_quality(
    "Classifiez 'excellent produit' : positif ou négatif ?",
    "positif"
)

Constituez-vous une dizaine de cas de ce type, représentatifs de vos usages réels, et exécutez-les à chaque modification de prompt. Un jeu de tests de dix minutes vous évitera des semaines de doute sur la qualité perçue.

7. Checklist de mise en production

Avant de déployer, vérifiez chaque point :

Infrastructure

  • Variables d’environnement configurées (OPENAI_API_KEY, etc.)
  • Timeouts et max_retries configurés
  • Logging structuré en place
  • Monitoring des coûts activé

Résilience

  • Gestion d’erreurs complète (tous les codes HTTP)
  • Backoff exponentiel pour les retries
  • Circuit breaker pour les pannes prolongées
  • Fallback vers un modèle alternatif

Sécurité

  • Clé API jamais exposée dans le code ou les logs
  • Validation des inputs utilisateur
  • Rate limiting côté client
  • Pas de données sensibles dans les prompts

Performance

  • Streaming activé pour les interfaces utilisateur
  • Batch processing pour les traitements en volume
  • Cache pour les requêtes répétitives
  • Choix du modèle adapté à chaque tâche

Coûts

  • Budget quotidien défini
  • Alertes configurées
  • max_output_tokens fixé quand possible
  • Modèle le plus économique sélectionné par tâche

Points clés à retenir

  • Centralisez la configuration du client avec un singleton
  • Séparez et versionnez vos prompts hors du code
  • Implémentez un fallback entre modèles pour la haute disponibilité
  • Utilisez le traitement par lots avec concurrence limitée pour le volume
  • Testez vos prompts avec des assertions de qualité
  • Suivez la checklist complète avant chaque mise en production