Skip to content

LLM Basics

Notes de fond pour DeepLearning.AI "Generative AI with LLMs" (Phase 1), réécrites en visant la profondeur "staff engineer". Objectif : comprendre ce qui se passe sous l'appel API, pas juste savoir l'appeler. Un dev qui sait raisonner sur les tokens, l'attention et le sampling débogue des problèmes que les autres attribuent à la magie.


0. Le modèle mental à garder en tête

Un LLM est une fonction déterministe-jusqu'au-sampling qui prend une séquence de tokens et renvoie une distribution de probabilité sur le prochain token. Tout le reste — chat, agents, RAG, tool use — n'est que de l'ingénierie autour de cette boucle :

tokens_in → [transformer] → logits (un score par token du vocabulaire)
          → softmax(logits / temperature) → distribution
          → sampling (top-p / top-k) → 1 token
          → on l'ajoute à la séquence et on recommence (autoregressif)

Trois conséquences qu'un senior internalise :

  1. Tout est tokens. Le prompt, le contexte RAG, les définitions d'outils, la sortie : tout coûte des tokens, donc de l'argent et de la latence. "Réduire le prompt" est une optimisation de coût et de latence et parfois de qualité (moins de bruit).
  2. La génération est séquentielle. Le time-to-first-token (TTFT) dépend de la taille du prompt (prefill, parallélisable). Le débit ensuite (tokens/s, decode) dépend de la taille de la sortie (séquentiel, non parallélisable). C'est pourquoi on streame les longues sorties : la latence perçue est dominée par le decode.
  3. Le modèle n'a pas de mémoire. L'API est stateless : à chaque tour on renvoie tout l'historique. La "conversation" est une illusion reconstruite côté client. Le context window est la seule mémoire de travail.

1. Tokenization — pourquoi la longueur du prompt coûte de l'argent

  • BPE (Byte-Pair Encoding) : le vocabulaire est construit en fusionnant itérativement les paires de bytes les plus fréquentes. Conséquence : les mots anglais courants = 1 token ; le code, les langues non-latines, les UUID, le JSON verbeux = beaucoup plus de tokens par caractère.
  • Règle de poche (anglais) : ~4 caractères ≈ 1 token, soit ~0,75 token par mot. Le français consomme ~15-20 % de tokens en plus, le code et le JSON souvent 2×.
  • Ne jamais estimer les tokens Claude avec tiktoken : c'est le tokenizer d'OpenAI, il sous-compte de 15-20 % sur du texte courant et bien plus sur du code. Pour Claude, utiliser l'endpoint officiel client.messages.count_tokens(model=..., messages=...) — le compte est spécifique au modèle.
python
from anthropic import Anthropic

client = Anthropic()
resp = client.messages.count_tokens(
    model="claude-opus-4-8",
    messages=[{"role": "user", "content": open("prompt.txt").read()}],
)
print(resp.input_tokens)

⚠️ Détail qui mord en prod : changer de famille de modèle change le tokenizer. Le tokenizer de Opus 4.7/4.8 produit ~1× à 1,35× plus de tokens que celui de Opus 4.6 sur le même texte. Si tu portes un budget de contexte ou un seuil de compaction d'un modèle à l'autre, re-mesure avec count_tokens sur chaque modèle — n'applique pas un multiplicateur en aveugle.


2. Context window — ce qui rentre, et le "lost in the middle"

  • Le context window est la longueur maximale (prompt + sortie) que le modèle traite en un appel. Pour la génération actuelle de Claude, c'est 1M tokens (Opus 4.6/4.7/4.8, Sonnet 4.6), 200K pour Haiku 4.5.
  • max_tokens borne la sortie, pas l'entrée. Si la sortie atteint max_tokens, stop_reason == "max_tokens" et la réponse est tronquée en plein milieu — il faut relancer ou augmenter le plafond.
  • "Lost in the middle" : l'attention n'est pas uniforme sur la fenêtre. Les modèles récupèrent mieux l'information placée au début et à la fin du contexte que celle enfouie au milieu. Implications pratiques :
    • Mets les instructions critiques et la question à la fin du prompt.
    • En RAG, ne balance pas 200 chunks "au cas où" : le recall baisse quand le contexte est saturé de bruit. Re-ranke et garde le top-k pertinent.
    • Un context window géant n'est pas un substitut au retrieval. Remplir 1M tokens coûte cher, augmente la latence de prefill, et dilue le signal.
SymptômeCause probableRéflexe staff
Le modèle "oublie" une consigne donnée tôtConsigne enfouie en milieu de contexteDéplacer en fin de prompt, ou la répéter
Réponse tronquée brutalementmax_tokens atteintVérifier stop_reason, streamer, augmenter le plafond
RAG qui régresse quand on ajoute des docsSaturation + bruitRe-rank, réduire le top-k, mesurer le recall

3. Mécanisme d'attention — Q / K / V

L'attention est le cœur du transformer. Pour chaque token, le modèle calcule trois projections :

  • Query (Q) : "qu'est-ce que je cherche ?"
  • Key (K) : "qu'est-ce que j'offre comme information ?"
  • Value (V) : "voici le contenu que je transporte."

Le score d'attention = softmax(Q·Kᵀ / √d_k) · V. Intuitivement : chaque token regarde tous les autres, pondère leur importance par la similarité Q·K, et agrège leurs valeurs.

  • Self-attention : Q, K, V viennent de la même séquence (le token regarde son propre contexte).
  • Multi-head : on fait tourner plusieurs attentions en parallèle, chacune apprenant un type de relation (syntaxe, coréférence, position…), puis on concatène.
  • Coût quadratique : l'attention est en O(n²) sur la longueur de séquence n. C'est la raison pour laquelle le contexte long est cher et lent : doubler le prompt quadruple (en théorie naïve) le coût d'attention. Les optimisations modernes (FlashAttention, KV-cache, attention par blocs) atténuent ça mais la tendance reste.
  • KV-cache : pendant la génération autoregressive, on met en cache les K et V des tokens déjà traités pour ne pas les recalculer à chaque nouveau token. C'est ce qui rend le prompt caching côté API possible et rentable : le préfixe stable est précalculé une fois.

🧠 Pourquoi ça compte pour toi : quand un senior voit une latence de TTFT qui explose, il pense "prefill O(n²) sur un gros prompt" et regarde s'il peut cacher le préfixe. Quand le débit de decode est lent, il pense "le modèle, pas le prompt" et envisage un modèle plus petit ou du streaming.

Le pont entre KV-cache et prompt caching (à internaliser). Le KV-cache est une optimisation intra-requête : pendant le decode d'une même génération, on ne recalcule pas les K/V des tokens déjà émis. Le prompt caching côté API est une optimisation inter-requêtes : Anthropic persiste les K/V d'un préfixe stable (système + outils + début des messages) pour ne pas refaire le prefill à chaque appel qui partage ce préfixe. Conséquences directes que tu dois savoir énoncer :

  • C'est un prefix match exact : un seul byte qui change dans le préfixe (un datetime.now() dans le système, un JSON d'outils non trié, un set d'outils variable) invalide tout ce qui suit. Le cache hit retombe à 0 silencieusement.
  • La lecture cache coûte ~0,1× le prix input ; l'écriture ~1,25× (TTL 5 min) ou (TTL 1 h). Donc le caching est rentable dès ~2 requêtes partageant le préfixe en TTL court, ~3 en TTL long.
  • Le préfixe minimum cacheable dépend du modèle : 4096 tokens sur Opus 4.8 / Haiku 4.5, 2048 sur Sonnet 4.6. En dessous, ça ne cache pas — sans erreur, juste cache_creation_input_tokens: 0.
  • Ordre de rendu : toolssystemmessages. Donc le contenu volatil (timestamp, question du tour) va après le dernier breakpoint, jamais avant.

4. Architecture transformer — encoder / decoder / encoder-decoder

ArchitectureExemplesUsage
Encoder-onlyBERTCompréhension : classification, embeddings, NER. Voit tout le contexte d'un coup (bidirectionnel).
Decoder-onlyGPT, Claude, Llama, MistralGénération autoregressive. C'est l'archi des LLM "chat" actuels. Attention causale (un token ne voit que le passé).
Encoder-decoderT5, modèles de traductionSeq2seq : l'encoder lit l'entrée, le decoder génère. Utile quand entrée et sortie ont des structures distinctes.

Les LLM génératifs modernes (dont Claude) sont decoder-only avec attention causale : à la position t, le token ne peut regarder que les positions ≤ t. C'est ce qui permet la génération token-par-token cohérente.


5. Pretraining vs fine-tuning vs RLHF

ÉtapeCe qu'elle faitQuand ça compte pour toi
PretrainingPrédire le prochain token sur des téraoctets de texte. Le modèle apprend langue, faits, raisonnement implicite. Coûte des millions $.Tu ne le feras jamais. C'est le "knowledge cutoff".
Fine-tuning (SFT)Ré-entraîner sur des exemples (input, output désiré) pour spécialiser.Rarement nécessaire : le prompting + few-shot + RAG résout 90 % des cas pour moins cher et sans MLOps.
RLHF / RLAIFAligner sur les préférences humaines (ou d'un modèle juge) : utilité, honnêteté, innocuité.Explique pourquoi le modèle refuse, hedge, ou répond "comme un assistant".

Réflexe staff : avant de proposer un fine-tuning, épuise prompting → few-shot → RAG → tool use. Le fine-tuning ajoute un pipeline de données, un cycle de ré-entraînement, et un risque de régression. On y va seulement pour un style/format très spécifique répété à grande échelle, ou pour compresser un long prompt système coûteux.

🧠 L'échelle d'escalade qu'un staff a en tête. Chaque marche augmente le coût d'ingénierie et le couplage opérationnel ; ne monte d'une marche qu'après avoir épuisé la précédente :

LevierCe que ça changeCoût d'ingénierieQuand y aller
Prompt (instructions, rôle)Comportement immédiat~0Toujours en premier
Few-shotFormat/style par l'exemple~0Quand le zero-shot dérive
RAG / retrievalConnaissance fraîche, factualitéMoyen (index, embeddings)Connaissance hors cutoff ou propriétaire
Tool use / agentsActions, calcul, vérité externeMoyen-élevé (orchestration)Le modèle doit agir, pas juste répondre
Fine-tuning (SFT)Style/format figé à grande échelleÉlevé (data + MLOps + régressions)Tout le reste a échoué et le volume justifie

La faute classique du junior : sauter directement au fine-tuning parce que c'est "le vrai ML". Le staff sait que 90 % des problèmes meurent sur les deux premières marches, pour un centième du coût.


6. Sampling — temperature, top-p, top-k

Le modèle sort des logits. Comment on en tire un token :

  • Temperature : divise les logits avant softmax. T → 0 : quasi-déterministe (greedy, prend le plus probable). T = 1 : distribution brute. T > 1 : aplatit, plus de hasard et de créativité… et d'hallucinations.
  • Top-k : ne garde que les k tokens les plus probables, renormalise, échantillonne.
  • Top-p (nucleus) : garde le plus petit ensemble de tokens dont la proba cumulée ≥ p. S'adapte à la forme de la distribution (mieux que top-k fixe).
  • Repetition / frequency penalty : pénalise les tokens déjà émis pour casser les boucles.

Heuristique : T≈0 pour extraction/classification/code (on veut de la reproductibilité), T≈0,7 pour de la rédaction, T≈1+ pour du brainstorming créatif. Mais T=0 ne garantit pas des sorties identiques bit-à-bit (non-déterminisme matériel, batching).

⚠️ Spécifique Claude (canonique 2026) : sur Opus 4.7 / Opus 4.8 / Fable 5, les paramètres temperature, top_p, top_k sont retirés et renvoient HTTP 400. On ne pilote plus le sampling par ces leviers : on guide le comportement par le prompt et on règle la profondeur de raisonnement via output_config.effort (low/medium/high/xhigh/max). Si l'intention était le déterminisme → effort: "low" + prompt serré. Si c'était la variance créative → demande-la explicitement dans le prompt. Sonnet 4.6 et Haiku 4.5 acceptent encore le sampling classique.


7. "Extended thinking" et effort — la syntaxe canonique 2026

C'est le piège le plus courant sur du code Claude récent. La forme thinking: {type: "enabled", budget_tokens: N} est SUPPRIMÉE sur Opus 4.7/4.8 et Fable 5 — elle renvoie un HTTP 400. Le concept de "budget de tokens fixe pour réfléchir" est mort.

À la place :

  • Adaptive thinking : thinking: {type: "adaptive"} — le modèle décide quand et combien réfléchir, et entrelace automatiquement le raisonnement entre les appels d'outils.
  • Effort : output_config: {effort: "low" | "medium" | "high" | "xhigh" | "max"} — pilote la profondeur de raisonnement et la dépense globale de tokens. Défaut = high. xhigh est le sweet spot pour le coding/agentique.
  • Affichage : sur Opus 4.7/4.8 et Fable 5, display vaut "omitted" par défaut (les blocs thinking arrivent avec un texte vide). Pour montrer un résumé à l'utilisateur : thinking: {type: "adaptive", display: "summarized"}.
python
from anthropic import Anthropic

client = Anthropic()
resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=16000,
    thinking={"type": "adaptive", "display": "summarized"},
    output_config={"effort": "high"},
    messages=[{"role": "user", "content": "Résous ce problème étape par étape…"}],
)

Sonnet 4.6 / Haiku 4.5 ne prennent pas de budget de thinking. Sonnet 4.6 supporte l'adaptive thinking ; Haiku non.


8. Familles de modèles — Claude (chiffres à connaître)

Tableau de référence Claude, faits canoniques 2026. Vérifie toujours les prix sur la page officielle avant un calcul de coût engageant, mais ces ID et tarifs sont la base à connaître par cœur.

ModèleID exactContextInput $ / 1M tokOutput $ / 1M tokSweet spot
Claude Opus 4.8claude-opus-4-81M$5$25Flagship : raisonnement le plus dur, agentique long-horizon
Claude Sonnet 4.6claude-sonnet-4-61M$3$15Le cheval de trait par défaut (vitesse/intelligence)
Claude Haiku 4.5claude-haiku-4-5200K$1$5Haut volume, rapide, pas cher
  • Flagship = claude-opus-4-8 (Opus 4.8). Pas Opus 4.7, pas claude-opus-4-7 comme flagship.
  • Jamais de suffixe de date inventé (-20260101, etc.) sur les alias — l'alias nu suffit et un faux suffixe renvoie 404.
  • Les autres familles (GPT/o-series, Gemini, Llama, Mistral) existent et ont leurs forces (Gemini sur le très long contexte, Mistral sur la souveraineté EU/français, Llama en open-weights), mais ce hub se concentre sur Claude — c'est là que tu factureras et débogueras.

Comment un staff choisit un modèle : par défaut Sonnet 4.6 (meilleur ratio coût/intelligence). On monte à Opus 4.8 quand la tâche est dure, longue, agentique, ou que le coût d'erreur est élevé. On descend à Haiku 4.5 pour du volume simple (classification, extraction, sous-agents) — jamais "pour économiser" sur une tâche sensible à l'intelligence. Le choix de descendre est une décision produit, pas un réflexe.

Arbre de décision (à dérouler à voix haute en entretien) :

La tâche est-elle sensible à l'intelligence (raisonnement, code, coût d'erreur élevé) ?
├── Oui → est-elle longue / agentique / long-horizon ?
│        ├── Oui → Opus 4.8 (xhigh effort)
│        └── Non → Sonnet 4.6 (defaut), monter en effort si besoin
└── Non (volume, format simple, extraction, classification, sous-agent) → Haiku 4.5

⚠️ Le piège du "downgrade pour économiser". Remplacer Opus par Haiku sur une tâche qui dépend de l'intelligence ne réduit pas le coût : ça réduit la qualité, et le coût réapparaît en retries, en escalades humaines, ou en bugs en prod. Le vrai levier de coût sur une tâche intelligente, c'est le prompt caching + le bon effort, pas le sous-dimensionnement du modèle. On descend de modèle uniquement quand la tâche tolère l'intelligence d'un Haiku — c'est une propriété de la tâche, pas du budget.

🔭 Observabilité du choix. Tu ne sais pas si ton choix de modèle est bon tant que tu ne logges pas resp.usage par appel (input / output / cache_read_input_tokens / cache_creation_input_tokens) et que tu ne le rattaches pas à une métrique de qualité (taux de retry, taux de validation humaine, taux de parse échoué sur les structured outputs). Sans ce couplage coût↔qualité, "Sonnet suffit" est une opinion, pas une décision.


9. Estimer coût et latence

Coût d'un appel (modèle de base, sans cache) :

coût = (input_tokens × prix_input + output_tokens × prix_output) / 1_000_000

Exemple concret, Opus 4.8, prompt de 20K tokens d'entrée + 2K de sortie :

input :  20_000 × $5  / 1M = $0,10
output:   2_000 × $25 / 1M = $0,05
total ≈ $0,15 par appel

À 100 000 appels/jour, ça fait $15 000/jour. C'est pourquoi le prompt caching et le choix du modèle ne sont pas des détails : ils dominent la facture.

Leviers de coût qu'un senior actionne :

  • Prompt caching : cache_control: {type: "ephemeral"} sur le préfixe stable (système + outils). Lecture cache ≈ 0,1× du prix input ; écriture ≈ 1,25×. Rentable dès 2 requêtes partageant le préfixe. Vérifier usage.cache_read_input_tokens — s'il reste à 0, un invalidateur silencieux est à l'œuvre (datetime.now() dans le prompt système, JSON non trié, set d'outils variable).
  • Batch API : -50 % pour les traitements non temps-réel.
  • Modèle adapté : Haiku pour le volume, Opus pour le dur.
  • effort plus bas : moins de tokens de raisonnement.

Latence : latence ≈ TTFT (prefill, fonction de l'input) + N_output × temps_par_token (decode). On streame dès que la sortie est longue, à la fois pour la latence perçue et pour éviter les timeouts HTTP du SDK (au-delà de ~16K max_tokens, le streaming est requis).

🧠 Le découplage latence/débit qu'un staff énonce sans hésiter. Le TTFT (prefill) est parallélisable sur le GPU et borné par la taille de l'input ; un gros prompt caché transforme un prefill cher en lecture cache quasi gratuite, donc le caching attaque le TTFT autant que le coût. Le débit de decode est séquentiel et borné par la taille de l'output et par le modèle ; ni le caching ni un prompt plus court n'y changent rien — seuls un modèle plus petit, un effort plus bas, ou une sortie plus courte le réduisent. Quand on te dit "c'est lent", la première question est : lent à démarrer (TTFT) ou lent à débiter (decode) ? — ce sont deux problèmes différents avec deux remèdes différents.


10. Patterns de prompting à maîtriser

  • Zero-shot / Few-shot : avec ou sans exemples dans le prompt. Le few-shot est souvent le levier de qualité le moins cher.
  • Chain-of-Thought (CoT) : "raisonne étape par étape". Sur Claude récent, l'adaptive thinking remplace le CoT manuel pour le raisonnement interne.
  • Self-consistency : échantillonner N raisonnements, voter sur la réponse majoritaire. Coûteux mais robuste.
  • Tree-of-Thought (ToT) : explorer plusieurs branches de raisonnement avec backtracking.
  • ReAct : alterner raisonnement et action (tool use) — la base des agents.
  • Reflection / self-critique : faire critiquer/réviser sa propre sortie par le modèle.
  • Prompt chaining : découper une tâche complexe en étapes API chaînées (chaque étape simple et vérifiable).
  • Role / system prompts : poser le contexte et les contraintes en amont.
  • Output structuré : préférer les structured outputs natifs (client.messages.parse() + schéma Pydantic/zod, ou output_config.format) au prompting XML/JSON artisanal. C'est validé, parsable, et plus fiable.

⚠️ Build-safety VitePress : si tu montres un template Angular/Vue/Handlebars avec des accolades doubles, mets-le toujours dans un bloc de code fencé (ts / html), jamais en inline — sinon Vue tente de l'interpréter et casse npm run docs:build.

ts
// Exemple de structured output natif (préféré au prompting JSON artisanal)
const ContactInfo = z.object({ name: z.string(), email: z.string() });
const resp = await client.messages.parse({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Extrais: Jane Doe ([email protected])" }],
  output_config: { format: zodOutputFormat(ContactInfo) },
});

11. Préoccupations de production (le réflexe staff)

Un appel LLM en prod n'est jamais un client.messages.create() nu. Ce qu'un senior attend dans le code :

  • AsyncAnthropic côté serveur ; asyncio.gather pour paralléliser des appels indépendants.
  • Retries + exceptions typées : max_retries du SDK, et on attrape RateLimitError / APIStatusError / OverloadedError / APITimeoutError — jamais du string-matching sur le message d'erreur.
  • Timeout par appel + streaming pour les longues sorties.
  • Prompt caching via cache_control sur le préfixe stable.
  • Observabilité : logguer resp.usage (input/output/cache tokens) à chaque appel pour suivre coût et dérive. Sans ça, tu pilotes à l'aveugle.
  • Sécurité : pas de secret dans le prompt système (il persiste dans l'historique/les logs) ; valider les entrées des outils à effet de bord ; human-in-the-loop sur les actions irréversibles.
python
import asyncio
from anthropic import AsyncAnthropic, RateLimitError, APIStatusError

client = AsyncAnthropic(max_retries=3, timeout=30.0)

async def ask(prompt: str) -> str:
    try:
        resp = await client.messages.create(
            model="claude-opus-4-8",
            max_tokens=4096,
            messages=[{"role": "user", "content": prompt}],
        )
    except RateLimitError:
        # backoff déjà géré par le SDK ; ici on log/alerte
        raise
    except APIStatusError as e:
        # classer finement via e.type ("overloaded_error", "billing_error"…)
        raise
    # observabilité : ne jamais jeter usage
    print(resp.usage.input_tokens, resp.usage.output_tokens)
    return resp.content[0].text

# appels parallèles indépendants
async def fan_out(prompts):
    return await asyncio.gather(*(ask(p) for p in prompts))

🏋️ Exercices

Des exercices progressifs et exigeants — pas "change cette constante". L'objectif est de te forcer à raisonner sur les tokens, le coût, et les modes de défaillance.

Exercice 1 — Tokenizer et coût réel

Objectif : prouver, chiffres à l'appui, que tiktoken est faux pour Claude et estimer un coût mensuel. Prends 5 prompts réalistes (un en anglais, un en français, un en JSON, un bloc de code, un texte mixte). Compte les tokens avec count_tokens pour claude-opus-4-8 et claude-haiku-4-5, puis avec tiktoken. Calcule l'écart en %. Déduis le coût d'1M d'appels par mois sur chaque modèle. Indice : tu devrais voir tiktoken sous-compter le plus sur le code et le JSON. Le coût Opus vs Haiku doit différer d'un facteur ~5 en input.

Exercice 2 — Démontrer le "lost in the middle"

Objectif : reproduire empiriquement la dégradation du recall en milieu de contexte. Construis un prompt avec 50 "faits" numérotés (Le fait 23 est : XYZ) noyés dans du texte de remplissage. Pose une question sur le fait n°2, n°25, puis n°49. Répète 10× par position. Mesure le taux de réponse correcte par position. Indice : attends-toi à un creux au milieu. Solution de remédiation à tester : déplacer le fait cible en fin de prompt et re-mesurer.

Exercice 3 — Migrer du code legacy qui renvoie 400

Objectif : corriger un appel Claude écrit pour un ancien modèle qui plante sur Opus 4.8. On te donne ce snippet :

python
resp = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=2000,
    temperature=0.7,
    thinking={"type": "enabled", "budget_tokens": 1500},
    messages=[{"role": "user", "content": "…"}],
)

Liste chaque ligne qui renvoie un 400 sur Opus 4.8 et corrige-les. Explique pourquoi chacune casse. Indice : trois problèmes — temperature retiré, budget_tokens retiré, et le modèle n'est pas le flagship. La cible : claude-opus-4-8, thinking: {type: "adaptive"}, output_config: {effort: ...}, pas de temperature.

Exercice 4 — Casser puis réparer le prompt caching

Objectif : diagnostiquer un cache hit rate de 0 % et le corriger. Écris un appel avec un gros préfixe système (>4096 tokens) + cache_control, mais glisse-y un invalidateur silencieux (datetime.now() dans le système). Lance la même requête 5×, observe usage.cache_read_input_tokens rester à 0. Puis répare en sortant le timestamp du préfixe et vérifie que le cache hit. Calcule l'économie réelle. Indice : le cache est un prefix match — un seul byte qui change invalide tout ce qui suit. Le timestamp doit aller après le dernier breakpoint, ou disparaître.

Exercice 5 — Rendre l'appel production-grade

Objectif : transformer un messages.create() nu en client robuste. Pars d'un appel synchrone basique. Ajoute : AsyncAnthropic + max_retries, gestion typée de RateLimitError/OverloadedError/APITimeoutError, timeout par appel, streaming si max_tokens > 16000, logging de resp.usage, et asyncio.gather pour 10 appels parallèles. Mesure le débit avant/après parallélisation. Indice : asyncio.gather ne doit jamais avaler une exception silencieusement — utilise return_exceptions=True puis filtre, ou laisse remonter.

Exercice 6 — Défendre le nombre (le plus dur)

Objectif : produire et défendre une estimation de coût/latence pour un service réel. On veut un service de résumé qui traite 500 000 documents/jour, ~8K tokens d'entrée chacun, ~500 tokens de sortie. Estime le coût quotidien sur Opus 4.8, Sonnet 4.6, et Haiku 4.5. Puis : quel modèle choisis-tu et pourquoi ? Combien le prompt caching économise-t-il si 6K des 8K tokens d'entrée sont un préfixe partagé ? Le Batch API change-t-il la donne ? Présente le calcul comme si tu le défendais devant un VP Eng sceptique. Indice : le caching ne s'applique qu'au préfixe stable ; calcule input caché (0,1×) vs non-caché (1×). Le Batch API (-50 %) est applicable car le résumé n'est pas temps-réel. La réponse "Haiku partout" est probablement fausse si la qualité de résumé compte — argumente le trade-off.

Exercice 7 — Le harnais qui ne casse pas (le boss final)

Objectif : transformer le client de l'exercice 5 en service qui survit à une vraie panne API, et le prouver. Pars du client async de l'exercice 5. Injecte des pannes réalistes via un proxy ou un mock qui renvoie, dans le désordre : 529 overloaded, 429 rate_limit avec un header retry-after, un 500, un APITimeoutError, et un 400 (erreur permanente). Exigences : (a) les erreurs retryables (429/500/529/timeout) sont retriées avec backoff, le 400 ne l'est jamais (sinon boucle infinie sur une erreur permanente) ; (b) tu classes finement via le type d'exception SDK, jamais par string-matching sur le message ; (c) tu logges usage même sur la branche d'erreur partielle d'un stream ; (d) tu mesures le coût réel d'une rafale de 1000 appels avec 20 % d'échecs injectés, retries inclus. Puis défends : pourquoi max_retries du SDK ne suffit-il pas à lui seul, et qu'ajoutes-tu par-dessus ? Indice/Solution : max_retries gère le backoff réseau, mais pas ta logique métier (idempotence, budget de retry global, circuit-breaker quand 529 persiste, alerte). Le 400 doit court-circuiter immédiatement — teste-le explicitement, c'est le bug classique qui fait exploser la facture. Le coût réel > coût nominal à cause des retries : un appel retrié 3× sur 529 est facturé 0 fois côté Anthropic avant sa réussite (les échecs ne sont pas facturés), mais ton prefill caché peut être réécrit si le retry change de nœud — vérifie cache_read_input_tokens sur les retries.


🎤 En entretien

Questions seniors que ce sujet attire, avec la réponse d'une ligne attendue.

  • "Pourquoi un context window plus grand ne remplace-t-il pas le RAG ?" → Le coût/latence de prefill croît avec l'input (attention O(n²)), et le recall baisse en milieu de contexte saturé ("lost in the middle") — remplir 1M tokens dilue le signal et brûle du budget pour rien.

  • "Différence entre max_tokens et le context window, et que se passe-t-il si on dépasse ?"max_tokens borne la sortie ; le context window borne entrée + sortie. Dépasser max_tokens tronque (stop_reason == "max_tokens") ; dépasser le contexte renvoie une erreur (model_context_window_exceeded) — il faut compacter ou découper.

  • "Comment estimes-tu le coût d'une feature LLM avant de l'écrire ?"count_tokens sur des prompts représentatifs (pas tiktoken), × volume attendu × tarif input/output du modèle, en soustrayant le préfixe caché à 0,1× ; je présente Opus/Sonnet/Haiku et je justifie le choix par le coût d'erreur, pas par le prix nu.

  • "Pourquoi temperature ne marche-t-il plus sur les Claude récents, et comment contrôler le déterminisme alors ?"temperature/top_p/top_k sont retirés sur Opus 4.7/4.8 et Fable 5 (HTTP 400) ; on pilote par le prompt et par output_config.effort (low pour du déterministe/serré, high/xhigh pour le raisonnement profond) — et de toute façon T=0 n'a jamais garanti des sorties identiques bit-à-bit.

  • "Pourquoi le contexte long est-il coûteux et lent, mécaniquement ?" → L'attention est en O(n²) sur la longueur de séquence : doubler le prompt quadruple le coût d'attention naïf ; le prefill grossit avec l'input (d'où le TTFT qui explose) et le KV-cache occupe de la mémoire GPU proportionnelle à n. C'est pourquoi on cache le préfixe et on retrieve au lieu de tout balancer dans la fenêtre.

  • "Mon cache_read_input_tokens reste à 0 alors que je passe cache_control. Que cherches-tu en premier ?" → Un invalidateur silencieux dans le préfixe : datetime.now()/UUID dans le système, JSON d'outils non trié, set d'outils variable, ou un préfixe sous le minimum cacheable (4096 tok sur Opus 4.8). Le cache est un prefix-match exact — un byte qui change invalide tout en aval ; je diff les bytes rendus de deux requêtes pour trouver le coupable.

  • "Tu dois estimer la facture d'une feature agentique avant de l'écrire — quelle est la part que les juniors oublient ?" → Le coût des tokens de raisonnement (adaptive thinking) et des tours d'outils, pas juste l'input/output du prompt visible : un agent fait N appels, chacun avec son prefill (cachable) et son decode. On modélise le coût par tour × nombre de tours, on soustrait le préfixe caché à 0,1×, et on borne la dépense via effort et max_tokens plutôt que d'espérer.


Ressources


Mes notes

Bibliothèque tech perso — Achref