L’API : premiers appels
Intégrer Claude dans son propre logiciel : le premier appel, la structure d’une requête, la conversation, le streaming, la gestion des erreurs et des raisons d’arrêt.
À la fin de ce chapitre, vous saurez
- Faire un premier appel et comprendre chaque champ de la requête
- Gérer une conversation, l’API étant sans état
- Utiliser le streaming et savoir quand il devient obligatoire
- Traiter les erreurs et les raisons d’arrêt sans casser en production
Dans ce chapitre
Tout ce qui précède passait par une interface. L’API est la porte d’entrée directe : votre programme envoie du texte, reçoit du texte, et vous construisez ce que vous voulez autour.
C’est aussi le moment où les notions de la partie 1 cessent d’être théoriques : la fenêtre de contexte devient une ligne de facture, les jetons deviennent une métrique, et les raisons d’arrêt deviennent des cas à traiter.
Le premier appel
L’authentification se fait par une clé d’API, créée dans la console, jamais écrite dans le code :
export ANTHROPIC_API_KEY="sk-ant-..."
pip install anthropic # ou : npm install @anthropic-ai/sdkimport anthropic
client = anthropic.Anthropic() # lit ANTHROPIC_API_KEY dans l'environnement
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system="Tu réponds en français, de façon concise et factuelle.",
messages=[
{"role": "user", "content": "Résume ce texte en trois puces : ..."}
],
)
for bloc in reponse.content:
if bloc.type == "text":
print(bloc.text)import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const reponse = await client.messages.create({
model: 'claude-opus-5',
max_tokens: 16000,
system: 'Tu réponds en français, de façon concise et factuelle.',
messages: [{ role: 'user', content: 'Résume ce texte en trois puces : ...' }],
});
for (const bloc of reponse.content) {
if (bloc.type === 'text') console.log(bloc.text);
}Notez for bloc in reponse.content plutôt que content[0].text. Le contenu est une liste de blocs, pas une chaîne. Selon la requête, on y trouve du texte, des blocs de réflexion, des demandes d’outil. Indexer le premier élément fonctionne jusqu’au jour où ce n’est plus du texte.
Les champs de la requête
| Champ | Rôle |
|---|---|
model | L’identifiant exact du modèle. Pas d’approximation : une faute donne un 404 |
max_tokens | Plafond de sortie, obligatoire. Ne limite pas l’entrée |
system | L’instruction permanente. Séparée des messages, et cachable |
messages | L’historique, alternant user et assistant |
thinking | Réflexion approfondie |
output_config | Effort et format de sortie |
Deux remarques qui évitent des erreurs.
max_tokens est un plafond, pas un objectif. Une valeur trop basse tronque la réponse au milieu d’une phrase, et stop_reason vaut alors max_tokens. Une valeur généreuse ne coûte rien de plus : vous payez la sortie réellement produite.
Le system n’est pas un message. C’est un champ à part, ce qui a une conséquence pratique importante : il est en tête du prompt, donc c’est le premier candidat à la mise en cache — chapitre 32.
La conversation
L’API est sans état. Le serveur ne garde rien : à chaque appel, vous renvoyez tout l’historique.
messages = []
def echanger(message_utilisateur: str) -> str:
messages.append({"role": "user", "content": message_utilisateur})
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
system=SYSTEME,
messages=messages,
)
# On rejoue le contenu complet, pas seulement le texte : les blocs de
# réflexion et les demandes d'outil doivent être conservés tels quels.
messages.append({"role": "assistant", "content": reponse.content})
return next((b.text for b in reponse.content if b.type == "text"), "")Deux points comptent.
On rejoue reponse.content, pas une chaîne. Extraire le texte et n’ajouter que lui casse les conversations qui comportent des blocs de réflexion ou des appels d’outil.
Chaque tour coûte l’historique entier. Une conversation de quarante échanges fait payer, au quarantième appel, la relecture des trente-neuf précédents. C’est arithmétique, et c’est exactement ce que la mise en cache corrige.
Le streaming
Sans streaming, vous attendez la réponse complète. Avec, les fragments arrivent au fur et à mesure.
with client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Rédige la note complète..."}],
) as flux:
for fragment in flux.text_stream:
print(fragment, end="", flush=True)
message = flux.get_final_message() # l'objet complet, une fois finiDeux raisons de l’utiliser, et la seconde n’est pas facultative.
L’expérience. Dans une interface, voir le texte arriver change la perception du délai.
Le délai d’expiration. Au-delà d’un certain volume de sortie, une requête non diffusée risque d’excéder le délai HTTP. Les bibliothèques officielles refusent d’ailleurs certaines requêtes non diffusées à max_tokens élevé plutôt que de vous laisser échouer.
La règle pratique : en dessous d’environ 16 000 jetons de sortie, le non-diffusé va bien. Au-dessus, diffusez — quitte à utiliser get_final_message() et à ignorer les fragments.
Les raisons d’arrêt
Le champ stop_reason doit être lu avant le contenu. C’est le bug le plus répandu en production.
| Valeur | Signification | Ce qu’il faut faire |
|---|---|---|
end_turn | Réponse terminée normalement | Traiter |
max_tokens | Coupée par le plafond | Relancer avec un plafond plus haut, ou découper |
tool_use | Attend le résultat d’un outil | Chapitre 30 |
stop_sequence | Séquence d’arrêt atteinte | Traiter |
pause_turn | Outil serveur en pause | Renvoyer la conversation pour reprendre |
refusal | Refus des filtres de sécurité | Ne pas relancer à l’identique |
reponse = client.messages.create(...)
if reponse.stop_reason == "refusal":
journaliser_refus(reponse)
return message_utilisateur_de_repli()
if reponse.stop_reason == "max_tokens":
# La réponse est syntaxiquement plausible mais incomplète.
# Ne l'exploitez pas telle quelle.
raise ReponseTronquee(reponse)
texte = next((b.text for b in reponse.content if b.type == "text"), "")Le cas max_tokens mérite une attention particulière : la réponse paraît normale. Un JSON tronqué, un tableau à moitié rempli, une liste coupée au cinquième élément passeront sans bruit si vous ne vérifiez pas.
Les erreurs
Les bibliothèques officielles exposent des exceptions typées. Utilisez-les — jamais de comparaison sur le texte du message.
import anthropic
try:
reponse = client.messages.create(...)
except anthropic.NotFoundError:
# Identifiant de modèle invalide : c'est une erreur de code
raise
except anthropic.RateLimitError as e:
delai = int(e.response.headers.get("retry-after", "60"))
replanifier(delai)
except anthropic.APIStatusError as e:
if e.status_code >= 500:
replanifier(30) # problème côté service, réessayable
else:
journaliser(e) # requête invalide : ne pas réessayer tel quel
raise
except anthropic.APIConnectionError:
replanifier(10) # réseauLa bibliothèque réessaie déjà automatiquement les erreurs 429 et 5xx, avec un recul exponentiel. Ne réimplémentez pas ce comportement : réglez max_retries si le défaut ne convient pas.
Réflexion et effort
Les deux réglages du chapitre 2 se pilotent ici explicitement.
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=32000,
thinking={"type": "adaptive", "display": "summarized"},
output_config={"effort": "high"}, # low | medium | high | xhigh | max
messages=[...],
)
for bloc in reponse.content:
if bloc.type == "thinking":
journaliser_raisonnement(bloc.thinking)
elif bloc.type == "text":
afficher(bloc.text)Trois choses à savoir.
display par défaut n’affiche pas le raisonnement. Les blocs de réflexion sont présents mais leur texte est vide. Si vous voulez le résumé, demandez-le explicitement.
La réflexion consomme le max_tokens. C’est un plafond global sur la sortie, réflexion comprise. Un max_tokens calculé au plus juste pour la réponse tronquera dès que la réflexion s’allonge.
L’effort a un effet réel sur le coût. Un effort élevé sur une tâche simple consomme davantage pour un résultat identique. Sur une tâche longue et agentique, l’inverse est souvent vrai : mieux réfléchir au départ évite des allers-retours.
Un service de classification de tickets
La situation
Vous recevez trois cents tickets de support par jour. Vous voulez les classer automatiquement par catégorie et par urgence, avec un traitement fiable en production.
Le squelette
import anthropic
client = anthropic.Anthropic()
SYSTEME = """Tu classes des tickets de support.
Catégories : bug, question, demande_evolution, facturation, autre.
Urgence : critique, elevee, normale, basse.
Règles :
- « critique » suppose un service inutilisable pour plusieurs
utilisateurs. Un utilisateur bloqué seul est « elevee ».
- Si le ticket ne permet pas de trancher, urgence « normale » et
signale-le dans le champ prévu.
- Ne déduis jamais l'urgence du ton employé par l'utilisateur.
"""
class ReponseInexploitable(Exception):
pass
def classer(ticket: str) -> dict:
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
system=SYSTEME,
output_config={
"effort": "low", # tâche courte et bien cadrée
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"categorie": {
"type": "string",
"enum": ["bug", "question", "demande_evolution",
"facturation", "autre"],
},
"urgence": {
"type": "string",
"enum": ["critique", "elevee", "normale", "basse"],
},
"incertain": {"type": "boolean"},
"justification": {"type": "string"},
},
"required": ["categorie", "urgence", "incertain",
"justification"],
"additionalProperties": False,
},
},
},
messages=[{"role": "user", "content": ticket}],
)
if reponse.stop_reason == "refusal":
raise ReponseInexploitable("refus des filtres")
if reponse.stop_reason == "max_tokens":
raise ReponseInexploitable("réponse tronquée")
import json
texte = next(b.text for b in reponse.content if b.type == "text")
return json.loads(texte)Les trois décisions qui comptent
effort: "low". La tâche est courte et bien cadrée. Un effort élevé coûterait davantage pour un résultat identique.
Le champ incertain. Sans lui, le modèle doit trancher, et il tranche — c’est le mécanisme du chapitre 4. Avec lui, les cas douteux remontent au lieu de se déguiser en certitudes.
Les exceptions sur stop_reason. Une réponse tronquée produirait un JSON invalide, ou pire, un JSON valide mais incomplet.
Ce qui reste à faire
La suite est au chapitre 32 : mettre en cache l’instruction système, et traiter les trois cents tickets par lots plutôt qu’un par un.
Ce qu’il faut retenir
Vérifier que c’est passé
3 questions. Rien n’est enregistré, rien n’est envoyé : c’est pour vous.
Le squelette qui tient en production
Écrivez une fonction qui appelle Claude et qui traite explicitement les quatre cas : réponse normale, réponse tronquée sur max_tokens, refus, erreur réseau ou de quota. Testez-la en provoquant chacun des cas — un max_tokens à 10 pour la troncature, une clé invalide pour l’authentification. C’est le squelette que vous réutiliserez partout.
Un indice, si vous bloquez
Provoquer une erreur de quota est difficile à la demande ; simulez-la en interceptant l’exception correspondante dans un test.