Aller au contenu
Formation Claude

Tapez au moins deux lettres.

Commencer

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.

12 min de lecturePartie 6 — L’API et les agentsTechnique

À 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
  1. Le premier appel
  2. Les champs de la requête
  3. La conversation
  4. Le streaming
  5. Les raisons d’arrêt
  6. Les erreurs
  7. Réflexion et effort
  8. Ce qu’il faut retenir

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 :

bash
export ANTHROPIC_API_KEY="sk-ant-..."
pip install anthropic          # ou : npm install @anthropic-ai/sdk
python
import 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)
typescript
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

ChampRôle
modelL’identifiant exact du modèle. Pas d’approximation : une faute donne un 404
max_tokensPlafond de sortie, obligatoire. Ne limite pas l’entrée
systemL’instruction permanente. Séparée des messages, et cachable
messagesL’historique, alternant user et assistant
thinkingRéflexion approfondie
output_configEffort 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.

python
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.

python
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 fini

Deux 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.

ValeurSignificationCe qu’il faut faire
end_turnRéponse terminée normalementTraiter
max_tokensCoupée par le plafondRelancer avec un plafond plus haut, ou découper
tool_useAttend le résultat d’un outilChapitre 30
stop_sequenceSéquence d’arrêt atteinteTraiter
pause_turnOutil serveur en pauseRenvoyer la conversation pour reprendre
refusalRefus des filtres de sécuritéNe pas relancer à l’identique
python
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.

python
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éseau

La 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.

python
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

python
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.

Question 1

L’API est sans état. Qu’est-ce que cela implique pour une conversation ?

Voir la réponse

A — Vous renvoyez tout l’historique à chaque appel, et vous le payez à chaque fois

Chaque appel est indépendant. Vous reconstruisez la liste des messages et l’envoyez en entier. C’est ce qui explique qu’une conversation longue coûte de plus en plus cher à chaque tour — et c’est ce que la mise en cache du chapitre 32 corrige.

Question 2

Que faut-il vérifier avant de lire le contenu d’une réponse ?

Voir la réponse

A — Le champ stop_reason : la réponse peut être tronquée, en attente d’un outil, ou refusée

Lire content[0].text sans regarder stop_reason est le bug le plus répandu en production. Une réponse coupée sur max_tokens est incomplète mais paraît normale ; une réponse en tool_use ne contient pas de texte du tout.

Question 3

Quand le streaming devient-il nécessaire plutôt que confortable ?

Voir la réponse

A — Dès que la requête peut générer beaucoup de sortie : une requête non diffusée trop longue expire

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 à fort max_tokens plutôt que de vous laisser échouer en production.

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.