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 :
- 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).
- 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. - 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 windowest 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 officielclient.messages.count_tokens(model=..., messages=...)— le compte est spécifique au modèle.
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_tokenssur 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_tokensborne la sortie, pas l'entrée. Si la sortie atteintmax_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ôme | Cause probable | Réflexe staff |
|---|---|---|
| Le modèle "oublie" une consigne donnée tôt | Consigne enfouie en milieu de contexte | Déplacer en fin de prompt, ou la répéter |
| Réponse tronquée brutalement | max_tokens atteint | Vérifier stop_reason, streamer, augmenter le plafond |
| RAG qui régresse quand on ajoute des docs | Saturation + bruit | Re-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 cachingcô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 2× (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 :
tools→system→messages. Donc le contenu volatil (timestamp, question du tour) va après le dernier breakpoint, jamais avant.
4. Architecture transformer — encoder / decoder / encoder-decoder
| Architecture | Exemples | Usage |
|---|---|---|
| Encoder-only | BERT | Compréhension : classification, embeddings, NER. Voit tout le contexte d'un coup (bidirectionnel). |
| Decoder-only | GPT, Claude, Llama, Mistral | Génération autoregressive. C'est l'archi des LLM "chat" actuels. Attention causale (un token ne voit que le passé). |
| Encoder-decoder | T5, modèles de traduction | Seq2seq : 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
| Étape | Ce qu'elle fait | Quand ça compte pour toi |
|---|---|---|
| Pretraining | Pré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 / RLAIF | Aligner 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 :
Levier Ce que ça change Coût d'ingénierie Quand y aller Prompt (instructions, rôle) Comportement immédiat ~0 Toujours en premier Few-shot Format/style par l'exemple ~0 Quand le zero-shot dérive RAG / retrieval Connaissance fraîche, factualité Moyen (index, embeddings) Connaissance hors cutoff ou propriétaire Tool use / agents Actions, calcul, vérité externe Moyen-é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
ktokens 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_ksont 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 viaoutput_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.xhighest le sweet spot pour le coding/agentique. - Affichage : sur Opus 4.7/4.8 et Fable 5,
displayvaut"omitted"par défaut (les blocsthinkingarrivent avec un texte vide). Pour montrer un résumé à l'utilisateur :thinking: {type: "adaptive", display: "summarized"}.
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èle | ID exact | Context | Input $ / 1M tok | Output $ / 1M tok | Sweet spot |
|---|---|---|---|---|---|
| Claude Opus 4.8 | claude-opus-4-8 | 1M | $5 | $25 | Flagship : raisonnement le plus dur, agentique long-horizon |
| Claude Sonnet 4.6 | claude-sonnet-4-6 | 1M | $3 | $15 | Le cheval de trait par défaut (vitesse/intelligence) |
| Claude Haiku 4.5 | claude-haiku-4-5 | 200K | $1 | $5 | Haut volume, rapide, pas cher |
- Flagship =
claude-opus-4-8(Opus 4.8). Pas Opus 4.7, pasclaude-opus-4-7comme 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.usagepar 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_000Exemple 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érifierusage.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.
effortplus 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
effortplus 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, ououtput_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 cassenpm run docs:build.
// 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 :
AsyncAnthropiccôté serveur ;asyncio.gatherpour paralléliser des appels indépendants.- Retries + exceptions typées :
max_retriesdu SDK, et on attrapeRateLimitError/APIStatusError/OverloadedError/APITimeoutError— jamais du string-matching sur le message d'erreur. - Timeout par appel + streaming pour les longues sorties.
- Prompt caching via
cache_controlsur 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.
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 :
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_tokenset le context window, et que se passe-t-il si on dépasse ?" →max_tokensborne la sortie ; le context window borne entrée + sortie. Dépassermax_tokenstronque (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_tokenssur des prompts représentatifs (pastiktoken), × 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
temperaturene marche-t-il plus sur les Claude récents, et comment contrôler le déterminisme alors ?" →temperature/top_p/top_ksont retirés sur Opus 4.7/4.8 et Fable 5 (HTTP 400) ; on pilote par le prompt et paroutput_config.effort(lowpour du déterministe/serré,high/xhighpour le raisonnement profond) — et de toute façonT=0n'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_tokensreste à 0 alors que je passecache_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
effortetmax_tokensplutôt que d'espérer.
Ressources
- Cours : DL.AI Generative AI with LLMs
- Visualisation : 3Blue1Brown — Transformers expliqués
- Deep dive : nanoGPT de Karpathy — construire un transformer from scratch
- Paper fondateur : Attention Is All You Need (Vaswani et al. 2017)
- Lost in the middle : Liu et al. 2023 — Lost in the Middle: How Language Models Use Long Contexts