Applied AI · Avancé 🔴 · Session 1
✏️ Exercices
← Retour au programme 📄 Source .md

Exercices — Niveau Avancé, Session 1

« Claude API : plongée en profondeur »

Programme : Applied AI — Yann Isola Public : Architectes solutions (préparation Claude Certified Architect) Durée totale estimée : 2 h 30 – 3 h (hors classe ou en atelier encadré) Prérequis : Python 3.10+, SDK (SDK = Software Development Kit) anthropic installé, clé API (API = Application Programming Interface) de test.

⚠ Tous les noms de modèles, tarifs et limites cités sont volatils : vérifiez la documentation officielle (docs.anthropic.com) avant de coder en dur quoi que ce soit.


Exercice 1 — Construction de requête API : le client robuste (60 min)

Contexte

Vous êtes architecte chez un courtier en instruments tokenisés. L’équipe produit veut un module Python claude_client.py réutilisable par tous les services internes. Votre mission : concevoir la fonction d’appel de référence, avec tous les paramètres maîtrisés et tous les stop_reason gérés.

Consignes

Partie A — La requête complète (20 min)

Écrivez une fonction appeler_claude() qui :

  1. Accepte : question: str, persona: str, format_json: bool = False, max_tokens: int = 1024.
  2. Construit la requête avec :
    • un system prompt (canal privilégié : persona + règles de sortie — jamais dans un message user) ;
    • temperature=0.2 (tâche technique) ;
    • si format_json=True : un prefilling assistant avec "{" pour forcer l’ouverture JSON (JSON = JavaScript Object Notation), et une stop_sequences=["```"] de sécurité.
  3. Retourne un objet structuré ReponseClaude(texte, stop_reason, input_tokens, output_tokens, cout_estime).

Squelette de départ :

from dataclasses import dataclass
import anthropic

PRIX_INPUT_PAR_MTOK = 3.00    # $ / Mtok ⚠ volatil
PRIX_OUTPUT_PAR_MTOK = 15.00  # $ / Mtok ⚠ volatil

@dataclass
class ReponseClaude:
    texte: str
    stop_reason: str
    input_tokens: int
    output_tokens: int
    cout_estime: float

def appeler_claude(question: str, persona: str,
                   format_json: bool = False,
                   max_tokens: int = 1024) -> ReponseClaude:
    client = anthropic.Anthropic()
    # ... à compléter ...

Partie B — La garde stop_reason (20 min)

Complétez la fonction pour traiter chacun des quatre stop_reason :

N’oubliez pas : si format_json=True, re-préfixez le "{" du prefill (il n’est pas inclus dans la réponse).

Partie C — Le pré-comptage (20 min)

Avant l’appel réel, utilisez l’endpoint client.messages.count_tokens(...) pour :

  1. Compter les tokens d’entrée de la requête complète (system + messages).
  2. Si input_tokens + max_tokens > 200_000 ⚠ (fenêtre de contexte), lever ContexteDebordeError sans consommer d’appel de génération.
  3. Logger l’estimation de coût avant l’appel.

Livrables

Critères d’évaluation

Critère Points
Requête complète et paramètres justifiés (commentaires) /6
Les 4 stop_reason traités correctement /6
Prefill JSON re-préfixé côté client /3
Pré-comptage + garde de fenêtre de contexte /3
Qualité du code (typage, dataclass, logs) /2
Total /20

Piège à éviter (indice)

stop_reason: "max_tokens" arrive avec un HTTP 200. Si votre gestion d’erreur ne regarde que les exceptions HTTP, vous livrerez des JSON tronqués en production.


Exercice 2 — Implémentation du streaming SSE (50 min)

Contexte

Le front-end de votre plateforme affiche les réponses de Claude en temps réel. Vous devez implémenter le consommateur de flux côté serveur, en traitant les événements bruts SSE (SSE = Server-Sent Events) — pas seulement le helper haut niveau — car la certification l’exige et votre équipe front a besoin des métadonnées fines.

Consignes

Partie A — Le consommateur d’événements (25 min)

Implémentez streamer_reponse() qui consomme le flux bas niveau et maintient un état complet :

def streamer_reponse(client, question: str) -> dict:
    """Consomme le flux SSE et retourne l'état final :
    {texte, stop_reason, output_tokens, evenements_recus (liste des types), ttft_ms}
    """
    import time
    etat = {"texte": "", "stop_reason": None, "output_tokens": None,
            "evenements_recus": [], "ttft_ms": None}
    debut = time.monotonic()

    with client.messages.stream(
        model="claude-sonnet-4-5",   # ⚠
        max_tokens=800,
        messages=[{"role": "user", "content": question}],
    ) as stream:
        for event in stream:
            etat["evenements_recus"].append(event.type)
            # ... à compléter : traiter chaque type d'événement ...
    return etat

Exigences :

  1. message_start → capturer l’id du message.
  2. content_block_delta (sous-type text_delta) → accumuler le texte ; au premier delta, enregistrer le TTFT (TTFT = Time To First Token) en millisecondes.
  3. message_delta → capturer stop_reason et output_tokens (rappel : ils n’arrivent QUE dans cet événement).
  4. message_stop → clore proprement.
  5. Ignorer les ping sans planter ; sur un événement error, lever une exception avec le détail.

Partie B — L’assertion de séquence (15 min)

Écrivez verifier_sequence(evenements: list[str]) -> bool qui valide l’ordre canonique :

message_start
  → (content_block_start → content_block_delta* → content_block_stop)+
  → message_delta
  → message_stop

Testez-la sur la liste evenements_recus de la partie A. Cette fonction sert de test d’intégration : si Anthropic change le protocole ou si votre parseur perd des événements, elle échoue bruyamment.

Partie C — Question d’architecture (10 min, rédigé)

En 10 lignes max : votre front-end est derrière un proxy inverse (nginx) qui bufferise les réponses HTTP. Quel est l’impact sur votre streaming, quel symptôme observera l’utilisateur, et quelles directives de configuration corrigent le problème ? (Indice : proxy_buffering, X-Accel-Buffering, text/event-stream.)

Critères d’évaluation

Critère Points
Tous les types d’événements traités (y compris ping/error) /7
TTFT mesuré au bon endroit (premier text_delta) /3
stop_reason + output_tokens extraits de message_delta /4
Validateur de séquence correct /4
Question proxy : buffering identifié + correctifs /2
Total /20

Piège à éviter (indice)

Chercher stop_reason dans message_start ou message_stop = 0 point sur le critère 3. Relisez la séquence.


Exercice 3 — Conception d’un traitement par lots (Batches API) (60 min)

Contexte

Votre société doit classifier 80 000 courriels clients archivés (conformité) : catégorie, sentiment, présence de réclamation réglementaire. Pas d’exigence de latence — le rapport est mensuel. Budget serré. C’est un cas d’école pour la Batches API : traitement asynchrone, −50 % ⚠ sur les coûts, SLA (SLA = Service Level Agreement) de 24 h ⚠, jusqu’à 100 000 requêtes ⚠ par batch.

Consignes

Partie A — Document de conception (25 min, rédigé)

Produisez une note d’architecture (1–2 pages) couvrant :

  1. Découpage : un seul batch de 80k ou plusieurs ? Justifiez (limites ⚠ : 100k requêtes / ~256 Mo ⚠ par batch ; granularité de reprise sur erreur).
  2. Schéma de custom_id : proposez un format traçable (ex. mail-{lot}-{id_source}) et expliquez pourquoi la corrélation par custom_id est obligatoire (les résultats ne reviennent pas dans l’ordre).
  3. Choix de modèle : quel modèle pour de la classification simple à 80k exemplaires, et pourquoi ? (coût vs capacité)
  4. Cumul caching + batch : votre prompt de classification contient 3 000 tokens de taxonomie identiques pour les 80k requêtes. Expliquez comment cache_control se combine avec le batch et estimez l’économie supplémentaire (hits non garantis ⚠).
  5. Gestion des états finaux : succeeded, errored, expired, canceled — politique de rejeu pour chacun.
  6. Estimation de coût complète : avec entrée ~3 300 tokens/requête et sortie ~150 tokens/requête, chiffrez le coût total avec et sans batch, avec et sans cache (tarifs ⚠ de la grille du jour, montrez vos calculs).

Partie B — Implémentation du pipeline (35 min)

Codez pipeline_batch.py avec quatre fonctions :

def construire_requetes(emails: list[dict]) -> list[dict]:
    """Génère les requêtes batch avec custom_id traçables,
    system prompt de taxonomie marqué cache_control,
    et prefill assistant '{' pour forcer le JSON."""

def soumettre(client, requetes: list[dict]) -> str:
    """Crée le batch, retourne son id. Découpe en plusieurs
    batches si > 100_000 requêtes."""  # ⚠

def surveiller(client, batch_id: str, intervalle_s: int = 60) -> None:
    """Polling de processing_status jusqu'à 'ended'.
    Logge request_counts à chaque itération.
    Backoff : ne pas marteler l'API."""

def recolter(client, batch_id: str) -> tuple[list, list]:
    """Itère les résultats JSONL (JSONL = JSON Lines).
    Retourne (succes, echecs). Les echecs incluent custom_id
    + type d'erreur pour rejeu ciblé."""

Exigences :

  1. Les résultats errored sont écrits dans rejeu.jsonl, prêts à être resoumis dans un batch correctif.
  2. Chaque réponse succeeded est validée : JSON parsable ET stop_reason == "end_turn" (une classification tronquée par max_tokens est un échec silencieux à intercepter).
  3. Le polling utilise un intervalle croissant (60 s → 120 s → 300 s max) : un batch peut durer des heures, inutile d’interroger chaque seconde.

Critères d’évaluation

Critère Points
Note d’architecture : les 6 points traités avec chiffres /8
custom_id traçable + corrélation correcte /3
cache_control combiné au batch /3
Validation stop_reason sur chaque résultat /3
Pipeline de rejeu des échecs /3
Total /20

Piège à éviter (indice)

Deux pièges classiques : (1) supposer que les résultats reviennent dans l’ordre de soumission ; (2) compter uniquement les erreurs HTTP et laisser passer les classifications tronquées (stop_reason: "max_tokens") comme des succès.


Barème global

Exercice Poids
1 — Client robuste 35 %
2 — Streaming SSE 30 %
3 — Conception batch 35 %

Seuil de validation session : 60 %. Les corrigés types sont fournis par le formateur après remise.