---
title: "Claude API : plongée en profondeur"
subtitle: "Applied AI — Niveau Avancé · Session 1"
author: "Yann Isola"
theme: "ink #1A2230 / teal #0F7A6C / copper #B4612A / light-teal #E9F6F3 / bg #F4F7F6"
---

# Slide 1 — Titre

## Claude API : plongée en profondeur
**Applied AI — Niveau Avancé · Session 1**
Yann Isola · Préparation *Claude Certified Architect*

> Notes : Cadrer d'emblée — cette session n'est pas du prompting, c'est de l'architecture. Contrat d'API, économie de tokens, modes de défaillance.

---

# Slide 2 — Objectifs de la session

- Construire une requête API complète et justifier **chaque** paramètre
- Interpréter les 4 valeurs de `stop_reason` → logique applicative
- Raisonner sur la fenêtre de contexte comme **mémoire de travail**
- Maîtriser le prompt caching (`cache_control`) et son ROI
- Implémenter le streaming SSE (SSE = Server-Sent Events)
- Arbitrer synchrone / streaming / **Batches API**
- Concevoir la gestion d'erreurs : 429, backoff, `retry-after`

> Notes : 7 objectifs = 7 blocs. Tout est évalué en QCM certification.

---

# Slide 3 — Le contrat minimal

Trois champs **obligatoires** : `model`, `max_tokens`, `messages`

```bash
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Bonjour"}]
  }'
```

⚠ Noms de modèles volatils — toujours vérifier docs.anthropic.com

> Notes : `anthropic-version` épingle le contrat d'API. Sans lui → erreur. `max_tokens` n'a PAS de défaut (piège Q1 du quiz).

---

# Slide 4 — max_tokens : un plafond, pas une cible

- Le modèle peut s'arrêter **avant** le plafond
- S'il l'atteint → réponse **tronquée** avec HTTP **200** ✅😱
- Facturation = tokens d'entrée + tokens de sortie (tarifs différents, sortie ≈ 5× ⚠)

```json
"usage": {"input_tokens": 58, "output_tokens": 1024}
```
`output_tokens == max_tokens` → suspicion de troncature

> Notes : Anecdote terrain : systèmes en prod livrant des JSON tronqués pendant des semaines faute de tester stop_reason. Transition vers slide 8.

---

# Slide 5 — L'appel via SDK Python

```python
import anthropic
client = anthropic.Anthropic()  # lit ANTHROPIC_API_KEY

response = client.messages.create(
    model="claude-sonnet-4-5",        # ⚠
    max_tokens=1024,
    temperature=0.2,                   # tâche technique
    stop_sequences=["FIN_RAPPORT"],
    system="Analyste financier. Français, concis.",
    messages=[{"role": "user", "content": "..."}],
)
print(response.content[0].text)
print(response.usage, response.stop_reason)
```

> Notes : SDK = Software Development Kit. Insister : logger `usage` systématiquement (observabilité des coûts). `content` est un TABLEAU de blocs.

---

# Slide 6 — Paramètres d'échantillonnage

| Paramètre | Rôle | Reco architecte |
|-----------|------|-----------------|
| `temperature` 0–1 | aléa | 0–0.3 extraction · 0.7–1 créatif |
| `top_p` | nucleus sampling | ne pas combiner avec temperature |
| `top_k` | k tokens les + probables | cas avancés uniquement |
| `stop_sequences` | arrêts personnalisés | sorties délimitées |

**Piège certif :** `temperature: 0` ≠ déterminisme parfait

> Notes : Formulation correcte : « réduit fortement la variabilité ». Non-déterminisme numérique résiduel.

---

# Slide 7 — Trois rôles, trois privilèges

1. **`system`** — canal **privilégié** (hors du tableau messages)
   → persona, règles, politique de sortie. Pas « un premier message déguisé »
2. **`user`** — l'appelant : questions, documents, `tool_result`
3. **`assistant`** — le modèle… et le **prefilling**

Règle : alternance stricte, premier message = `user`
Deux rôles identiques consécutifs → **400**

> Notes : Le system prompt est traité avec une priorité particulière par le modèle — résistance accrue aux contournements dans les tours user. API sans état : tout est renvoyé à chaque appel.

---

# Slide 8 — Prefilling : mettre des mots dans la bouche du modèle

```python
messages=[
    {"role": "user", "content": "Liste 3 risques en JSON."},
    {"role": "assistant", "content": "{"}   # ← prefill
]
```

- Le modèle **continue** à partir de `{`
- Supprime les préambules (« Bien sûr ! Voici… »)
- ⚠ Le prefill n'est **pas** dans la réponse → re-préfixer côté client

> Notes : Démo live : même question avec/sans prefill. JSON = JavaScript Object Notation. Autre usage : forcer un choix de QCM avec prefill « La réponse est ( ».

---

# Slide 9 — stop_reason : les 4 signaux

| `stop_reason` | Signification | Réaction |
|---------------|---------------|----------|
| `end_turn` | fin naturelle | ✅ nominal |
| `max_tokens` | **tronqué** (HTTP 200 !) | alerter / relancer |
| `stop_sequence` | séquence rencontrée | lire `response.stop_sequence` |
| `tool_use` | outil demandé | exécuter → `tool_result` → boucler |

> Notes : LE slide certification. Un architecte écrit une branche par valeur. max_tokens n'est PAS une erreur HTTP — état métier à détecter soi-même.

---

# Slide 10 — Le garde-fou canonique

```python
match response.stop_reason:
    case "end_turn":
        return extract_text(response)
    case "max_tokens":
        raise TruncatedResponseError(partial=...)
    case "stop_sequence":
        return parse_delimited(..., response.stop_sequence)
    case "tool_use":
        return handle_tool_loop(response)
    case _:
        raise UnexpectedStopReason(...)
```

> Notes : Le `case _` protège contre les futures valeurs de l'API. À faire écrire en exercice 1.

---

# Slide 11 — La fenêtre de contexte = mémoire de travail

- ⚠ ~200 000 tokens (certains modèles : 1M en bêta)
- Contient TOUT : system + historique + docs + outils + résultats
- Ce n'est pas « une limite » — c'est ce que le modèle **sait** pour cette requête

**3 conséquences :** coût linéaire · lost in the middle · latence (TTFT)

> Notes : TTFT = Time To First Token. Changement de mental model : de « contrainte » à « ressource à architecturer ».

---

# Slide 12 — Coût : la conversation qui devient quadratique

API **sans état** → l'historique complet est refacturé à chaque tour

```python
# 20 tours, 30k tokens d'historique moyen, 500 tokens de sortie
total = sum(cout(30_000, 500) for _ in range(20))
# ≈ 1,95 $ pour UNE conversation ⚠
```

× 10 000 utilisateurs/jour → **sujet de direction technique**

Parades : résumé · troncature glissante · **prompt caching**

> Notes : Somme des préfixes = croissance quadratique. Transition naturelle vers le caching.

---

# Slide 13 — « Lost in the middle »

Rappel de l'information selon sa position dans le contexte :

```
DÉBUT  ████████████  excellent
MILIEU ██████        dégradé   ← ne rien enterrer ici
FIN    ███████████   excellent
```

**Placement stratégique :**
- Documents volumineux → en tête
- Instructions critiques + question → en fin
- Consigne clé au milieu de 80k tokens de logs → ❌

> Notes : Question au groupe : « RAG avec 40 chunks — où va la question utilisateur ? » Réponse : après les docs, éventuellement répétée.

---

# Slide 14 — Prompt caching : le principe

Réutiliser un **préfixe déjà traité** via `cache_control`

⚠ Économie (volatile) :
- **Lecture (hit) : ~10 % du tarif → −90 %**
- **Écriture : surcoût ~25 %**
- TTL ~5 min ⚠, rafraîchi à chaque hit (option 1 h ⚠)

Rentable dès **2 requêtes** : 1,25 + 0,10 = 1,35 < 2,00

> Notes : TTL = Time To Live. Faire faire le calcul de rentabilité au groupe avant de donner la réponse.

---

# Slide 15 — cache_control : syntaxe

```python
system=[{
    "type": "text",
    "text": GROS_CONTEXTE,          # 50k tokens stables
    "cache_control": {"type": "ephemeral"}   # ← breakpoint
}]
```

```python
u = response.usage
u.cache_creation_input_tokens   # écrits (×1,25 ⚠)
u.cache_read_input_tokens       # lus (×0,10 ⚠)
u.input_tokens                  # non cachés, plein tarif
```

> Notes : Trois compteurs distincts dans usage — les montrer, c'est ce qui permet de vérifier que le cache fonctionne réellement en prod.

---

# Slide 16 — Règles d'invalidation du cache

- Préfixe **exact** : un octet modifié en amont → invalidé
- Ordre : `tools` → `system` → `messages`
- Minimum cachable : ⚠ 1 024 tokens (sinon ignoré **silencieusement**)
- Max **4 breakpoints** ⚠ par requête

**Design cible :** stable en tête, variable en queue

> Notes : Piège Q7 du quiz : modifier le dernier message user N'invalide PAS le cache (il est après le breakpoint).

---

# Slide 17 — Anti-pattern : le cache qui coûte plus cher

```python
system = f"[{datetime.now()}] Tu es un assistant..."  # ❌
```

- Horodatage en tête → préfixe jamais identique
- 0 % de hit + surcoût d'écriture 25 % **à chaque appel**
- Résultat : on paie **plus cher** que sans cache

> Notes : Cas réel fréquent. Idem avec un ID de session, un nonce, un compteur. Tout ce qui varie va APRÈS le breakpoint.

---

# Slide 18 — Compter avant de payer : count_tokens

```python
count = client.messages.count_tokens(
    model="claude-sonnet-4-5",   # ⚠ tokenizer par modèle
    system="...",
    messages=[{"role": "user", "content": doc}],
)
print(count.input_tokens)   # sans génération
```

- Chaque **famille de modèles a son tokenizer**
- ❌ Ne jamais réutiliser un comptage tiktoken (OpenAI) pour Claude
- Ordres de grandeur : ~3,5–4 car./token (anglais), un peu plus dense en français

> Notes : Usages : valider la fenêtre AVANT de payer, dimensionner max_tokens, chargeback interne. Passer la requête COMPLÈTE (system+messages+tools).

---

# Slide 19 — Streaming : pourquoi

- **UX** : premier token en ~1 s au lieu d'attendre 30 s
- Longues générations : indispensable (timeouts HTTP)
- SSE = **Server-Sent Events** : flux HTTP unidirectionnel `text/event-stream`

```
event: content_block_delta
data: {"delta":{"type":"text_delta","text":"Le"}}
```

> Notes : UX = User eXperience. SSE ≠ WebSocket : unidirectionnel, simple HTTP, compatible proxys (sauf buffering — voir exercice 2C).

---

# Slide 20 — Le cycle d'événements SSE

```
message_start          ← id, model, usage d'ENTRÉE
content_block_start    ← ouverture bloc 0
content_block_delta ×n ← les morceaux de texte
content_block_stop     ← fermeture bloc 0
message_delta          ← stop_reason + output_tokens ★
message_stop           ← fin du flux
```

(+ `ping` keep-alive, + `error` possible en cours de flux)

★ **Piège certif : les métadonnées finales sont dans `message_delta`**

> Notes : Démo : visualiseur de la page web de session (offline). Lancer la version « troncature » pour montrer stop_reason=max_tokens en streaming.

---

# Slide 21 — Streaming : implémentation

```python
with client.messages.stream(
    model="claude-sonnet-4-5", max_tokens=1024,   # ⚠
    messages=[...],
) as stream:
    for text in stream.text_stream:      # helper haut niveau
        print(text, end="", flush=True)
    final = stream.get_final_message()
    print(final.stop_reason)             # toujours vérifier !
```

Version bas niveau : `stream=True` + boucle sur `event.type`
→ exigée en certification (exercice 2)

> Notes : Même en streaming, la troncature max_tokens existe. Un front qui affiche joliment un texte tronqué reste un bug.

---

# Slide 22 — Batches API : le 3e mode d'exécution

| Mode | Latence | Coût | Usage |
|------|---------|------|-------|
| Synchrone | secondes | 100 % | interactif |
| Streaming | TTFT ~1 s | 100 % | interactif long |
| **Batch** | **SLA 24 h** ⚠ | **−50 %** ⚠ | masse, non urgent |

- Jusqu'à **100 000 requêtes** / ~256 Mo ⚠ par batch
- En pratique : souvent < 1 h
- Résultats **non ordonnés** → corrélation par `custom_id` obligatoire

> Notes : SLA = Service Level Agreement. Cas d'usage à faire trouver : classification nocturne, enrichissement CRM, éval massive de prompts.

---

# Slide 23 — Batch : code

```python
batch = client.messages.batches.create(requests=[
    {"custom_id": f"doc-{i}",            # clé de corrélation
     "params": {"model": "claude-haiku-4-5",   # ⚠ petit modèle
                "max_tokens": 512,
                "messages": [...]}}
    for i, doc in enumerate(documents)
])

for r in client.messages.batches.results(batch.id):
    if r.result.type == "succeeded": traiter(r.custom_id, ...)
    else: rejouer(r.custom_id, r.result)  # errored/expired/canceled
```

> Notes : États finaux : succeeded / errored / expired / canceled — une politique de rejeu pour chacun. Bonus : batch + caching se CUMULENT (hits non garantis).

---

# Slide 24 — Gestion d'erreurs : taxonomie

| Code | Cause | Retry ? |
|------|-------|---------|
| 400 | requête malformée | ❌ corriger |
| 401 / 403 | clé invalide / sans accès | ❌ |
| 413 | requête trop grosse | ❌ réduire |
| **429** | **rate limit** (RPM/ITPM/OTPM) | ✅ backoff |
| 500 / 529 | interne / surcharge | ✅ backoff |

RPM = Requests Per Minute · ITPM/OTPM = Input/Output Tokens Per Minute

> Notes : Trois compteurs de rate limit indépendants — on peut être limité en tokens de sortie tout en étant sous la limite de requêtes.

---

# Slide 25 — Backoff exponentiel avec jitter

```python
except anthropic.RateLimitError as e:
    retry_after = e.response.headers.get("retry-after")
    if retry_after:
        delai = float(retry_after)        # le serveur PRIME
    else:
        delai = min(60, 2**tentative) * random.uniform(0.5, 1.5)
    time.sleep(delai)
```

1. `retry-after` **prime** sur votre formule
2. **Jitter** obligatoire (effet « thundering herd »)
3. Le SDK retry déjà 2× par défaut → attention aux couches multiplicatives

> Notes : Bonus : en-têtes `anthropic-ratelimit-*-remaining` pour throttling PROACTIF. Rappeler la question d'ouverture du cours (qui a vécu un 429 ?).

---

# Slide 26 — Arbre de décision de l'architecte

```
Besoin temps réel ?
├── OUI → réponse longue ? → streaming SSE
│         réponse courte ? → synchrone
└── NON → volume massif ? → Batches API (−50 % ⚠)
                            + petit modèle
                            + prompt caching si préfixe commun
```

Toujours : garde `stop_reason` + backoff 429 + log de `usage`

> Notes : Synthèse opérationnelle. Les trois réflexes du bas s'appliquent aux TROIS modes.

---

# Slide 27 — Les 7 pièges de certification vus aujourd'hui

1. `max_tokens` obligatoire, sans valeur par défaut
2. Troncature = HTTP **200** + `stop_reason: "max_tokens"`
3. Le prefill n'est pas inclus dans la réponse
4. `temperature: 0` ≠ déterminisme parfait
5. Cache : préfixe exact, `tools → system → messages`
6. `stop_reason` final en streaming → **`message_delta`**
7. Résultats batch non ordonnés → `custom_id`

> Notes : Faire relire cette liste à voix haute par un participant. C'est la check-list de révision.

---

# Slide 28 — Prochaines étapes

- **Exercices :** client robuste · streaming bas niveau · pipeline batch 80k emails
- **Quiz :** 10 QCM certification (seuil 7/10)
- **Page interactive :** constructeur de requête + visualiseur SSE + compteur de tokens (offline)
- **Session 2 :** tool use & agents — la boucle `tool_use` → `tool_result` en profondeur

⚠ Réflexe permanent : revalider tarifs, limites et noms de modèles sur docs.anthropic.com

> Notes : Distribuer les exit tickets (5 questions, 3 min). Ramasser à la sortie — ils calibrent l'ouverture de la session 2.
