Chapitre 721 minDébutant
Appeler un LLM par API : SDK Python et Ollama
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- envoyer une requête à un LLM hébergé avec le SDK Python officiel ;
- garder une clé d'API hors du code ;
- gérer l'historique d'une conversation ;
- interroger un modèle local par l'API HTTP d'Ollama ;
- installer Ollama sur un serveur sans l'ouvrir à tout le réseau.
4 exercices corrigés1 schéma
Appeler un LLM par API, c'est envoyer depuis ton programme une requête HTTP qui contient le modèle choisi, les consignes et les messages, puis recevoir en JSON la réponse et le nombre de tokens consommés. Ce chapitre le fait deux fois : avec le SDK Python officiel d'Anthropic pour un modèle hébergé, puis avec l'API HTTP d'Ollama pour un modèle qui tourne sur ta machine ou sur un serveur de l'entreprise. Tu termines par l'installation d'Ollama sur un serveur du labo de Sorbier, sans l'ouvrir à tout le réseau.
Ce que contient une requête
Toutes les API de LLM suivent le même schéma, à quelques noms de champs près.
| Dans la requête | Rôle |
|---|---|
model | Le modèle à utiliser : claude-opus-5-5, llama3.2:3b… |
system | Le prompt système : les consignes de l'application (chapitre 4) |
messages | La conversation : une liste de messages, chacun avec un role (user pour l'utilisateur, assistant pour les réponses précédentes du modèle) et un content |
max_tokens | Le nombre maximal de tokens que la réponse peut produire, réflexion comprise : un plafond, pas une cible |
| La clé d'API | Dans un en-tête HTTP, jamais dans l'URL ni dans le corps |
| Dans la réponse | Rôle |
|---|---|
content | Le texte de la réponse, découpé en blocs typés (text, et selon les cas thinking ou tool_use) |
stop_reason | Pourquoi la génération s'est arrêtée : end_turn (terminé), max_tokens (plafond atteint, réponse tronquée), tool_use (le modèle demande un outil, chapitre 8), refusal (refus) |
usage | Les tokens consommés en entrée et en sortie : c'est ce qui est facturé |
Le cours API REST détaille les méthodes HTTP, les en-têtes et les codes de statut que tu croises ici.
Créer une clé et la garder hors du code
Chez Anthropic, la clé se crée dans la console développeur, platform.claude.com ; l'API ne répond qu'une fois des crédits achetés. Profites-en pour fixer une limite de dépense mensuelle : une boucle qui s'emballe peut coûter cher.
Une clé d'API vaut un mot de passe et une carte bancaire à la fois. Elle ne va jamais dans le code : un dépôt Git se partage, se pousse sur GitHub, se clone, et des robots parcourent en permanence les dépôts publics pour y récupérer les clés. Range-la dans une variable d'environnement, que le SDK lit tout seul.
# PowerShell, pour la session en cours
$env:ANTHROPIC_API_KEY = "colle-ta-clé-ici"
# PowerShell, de façon permanente pour ton compte (ouvre ensuite un nouveau terminal)
setx ANTHROPIC_API_KEY "colle-ta-clé-ici"
# Linux ou macOS, pour la session en cours ; ajoute la ligne à ~/.bashrc pour la garder
export ANTHROPIC_API_KEY="colle-ta-clé-ici"
Sur un serveur, la variable se définit dans l'unité systemd ou le gestionnaire de secrets de la plateforme, avec des droits de lecture limités au compte de l'application.
Premier appel avec le SDK Python
Le SDK officiel d'Anthropic pour Python s'appelle anthropic (version 2.6.0 en novembre 2026, Python 3.10 ou plus récent). Installe-le dans un environnement virtuel :
python -m venv .venv
# Windows : .venv\Scripts\activate Linux et macOS : source .venv/bin/activate
python -m pip install anthropic
Le programme assistant_support.py pose une question au modèle avec un prompt système :
import anthropic
client = anthropic.Anthropic() # lit la clé dans la variable d'environnement ANTHROPIC_API_KEY
reponse = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
system=(
"Tu es l'assistant du support informatique de Sorbier Logistique. "
"Réponds en français, en cinq étapes au plus, à un salarié qui n'est pas informaticien."
),
messages=[
{"role": "user", "content": "Mon imprimante affiche « bourrage papier » mais aucune feuille n'est coincée."},
],
)
for bloc in reponse.content:
if bloc.type == "text":
print(bloc.text)
print("Fin :", reponse.stop_reason)
print("Tokens :", reponse.usage.input_tokens, "en entrée,", reponse.usage.output_tokens, "en sortie")
Trois remarques :
- Le contenu est une liste de blocs. Claude Opus 5.5 réfléchit toujours avant de répondre ; sa réflexion revient dans un bloc de type
thinking, vide par défaut. Le programme n'affiche donc que les blocs de typetext. max_tokensest un plafond. Tu ne paies que les tokens réellement produits. Trop bas, la réponse est coupée etstop_reasonvautmax_tokens.usagete dit ce que tu paies. Le calcul du coût est au chapitre 11.
Pour une question simple, tu peux réduire la réflexion, donc le coût et le temps de réponse, avec le paramètre output_config={"effort": "low"}.
Pour comprendre ce que le SDK fait pour toi, voici la même requête en HTTP brut avec curl (dans un terminal Bash) :
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "content-type: application/json" \
-d '{"model": "claude-opus-5-5", "max_tokens": 16000,
"messages": [{"role": "user", "content": "Explique le rôle de DHCP en deux phrases."}]}'
L'en-tête anthropic-version fixe la version de l'API, donc la forme des requêtes et des réponses. Le SDK l'envoie à ta place, ainsi que la clé, et ajoute les nouvelles tentatives automatiques (deux par défaut) en cas d'erreur réseau ou de surcharge.
Tenir une conversation et gérer les erreurs
L'API est sans état (chapitre 3) : pour que le modèle se souvienne de ce qui a été dit, ton programme garde l'historique et le renvoie à chaque tour. conversation.py le fait, et traite les erreurs les plus courantes, de la plus précise à la plus générale :
import anthropic
client = anthropic.Anthropic()
CONSIGNES = "Tu es l'assistant du support informatique de Sorbier Logistique. Réponds en français."
historique = []
while True:
question = input("Toi : ").strip()
if question in ("", "quitter"):
break
historique.append({"role": "user", "content": question})
try:
reponse = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
system=CONSIGNES,
messages=historique,
)
except anthropic.AuthenticationError:
print("Clé d'API absente ou refusée : vérifie ANTHROPIC_API_KEY.")
break
except anthropic.RateLimitError:
print("Trop de requêtes : attends un peu avant de réessayer.")
historique.pop()
continue
except anthropic.APIConnectionError:
print("Impossible de joindre l'API : vérifie la connexion ou le proxy.")
historique.pop()
continue
except anthropic.APIStatusError as erreur:
print("Erreur", erreur.status_code, ":", erreur.message)
historique.pop()
continue
texte = "".join(bloc.text for bloc in reponse.content if bloc.type == "text")
if reponse.stop_reason == "refusal" or not texte:
print("Le modèle n'a pas répondu à cette demande.")
historique.pop()
continue
historique.append({"role": "assistant", "content": texte})
print("Assistant :", texte)
print(f"({reponse.usage.input_tokens} tokens envoyés pour ce tour)")
Lance-le et regarde la dernière ligne grandir à chaque tour : tout l'historique repart à chaque question. En cas d'erreur, historique.pop() retire la question qui n'a pas eu de réponse, pour que la conversation reste une alternance propre de messages user et assistant. Même chose si le modèle refuse de répondre (stop_reason vaut refusal) ou ne renvoie aucun texte : un message assistant vide dans l'historique ferait rejeter toutes les requêtes suivantes.
| Exception | Code HTTP | Cause probable |
|---|---|---|
AuthenticationError | 401 | Clé absente, mal copiée ou révoquée |
PermissionDeniedError | 403 | La clé n'a pas accès à ce modèle ou à cette fonction |
RateLimitError | 429 | Trop de requêtes ou de tokens par minute |
APIStatusError | 400, 500… | Requête mal formée (400), erreur du fournisseur (5xx) |
APIConnectionError | — | Pas de réseau, proxy, pare-feu sortant |
Un modèle local avec Ollama
Ollama télécharge des modèles ouverts et les sert sur une API HTTP locale, par défaut sur 127.0.0.1:11434 (version 0.40 en novembre 2026). Aucune clé, aucune facture, et rien ne sort de la machine. Attention, la bibliothèque d'Ollama propose aussi des modèles « cloud » (suffixe :cloud, comme gemma4:cloud) : ceux-là tournent chez Ollama, avec un compte, et tes requêtes quittent la machine. Sur un serveur qui doit rester interne, n'utilise que des modèles téléchargés, ou désactive les fonctions cloud.
Installation. Sous Windows et macOS, télécharge l'installateur sur ollama.com. Sous Linux :
curl -fsSL https://ollama.com/install.sh | sh
Premiers pas.
ollama pull llama3.2:3b # télécharge le modèle (2,0 Go), une seule fois
ollama run llama3.2:3b # discute dans le terminal ; /bye pour sortir
ollama list # les modèles téléchargés
ollama ps # les modèles chargés en mémoire en ce moment
Choisis la taille du modèle selon la mémoire de ta machine :
| Modèle (nom Ollama) | Éditeur | Taille du téléchargement | Pour quoi |
|---|---|---|---|
llama3.2:3b | Meta | 2,0 Go | Un PC portable avec 8 Go de RAM ; réponses simples |
qwen3:4b | Alibaba | 2,5 Go | Petit modèle qui raisonne avant de répondre |
gemma4:e2b | 4,6 Go et plus | Modèle récent, pour une machine plus confortable | |
embeddinggemma | 622 Mo | Calcule des plongements pour le RAG (chapitre 8) ; ne converse pas |
Interroger l'API. Avec curl, "stream": false demande une réponse complète en un seul objet JSON, plutôt qu'une suite de morceaux envoyés au fil de la génération :
curl http://localhost:11434/api/chat -d '{
"model": "llama3.2:3b",
"messages": [{"role": "user", "content": "Explique le rôle de DHCP en deux phrases."}],
"stream": false
}'
La réponse contient message.content (le texte), done et done_reason, et les compteurs prompt_eval_count (tokens lus) et eval_count (tokens générés).
En Python, la bibliothèque standard suffit : pas de SDK à installer. assistant_local.py reprend la conversation avec historique :
import json
import urllib.request
OLLAMA = "http://localhost:11434"
MODELE = "llama3.2:3b"
def discuter(messages):
corps = json.dumps({
"model": MODELE,
"messages": messages,
"stream": False,
"options": {"temperature": 0.2, "num_ctx": 8192},
}).encode("utf-8")
requete = urllib.request.Request(
OLLAMA + "/api/chat",
data=corps,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(requete, timeout=300) as reponse:
return json.load(reponse)
historique = [{
"role": "system",
"content": "Tu es l'assistant du support informatique de Sorbier Logistique. Réponds en français, brièvement.",
}]
while True:
question = input("Toi : ").strip()
if question in ("", "quitter"):
break
historique.append({"role": "user", "content": question})
donnees = discuter(historique)
texte = donnees["message"]["content"]
historique.append({"role": "assistant", "content": texte})
print("Assistant :", texte)
print(f"({donnees.get('prompt_eval_count')} tokens lus, {donnees.get('eval_count')} générés)")
Chez Ollama, le prompt système est simplement un message de rôle system en tête de liste. Le dictionnaire options règle la température (chapitre 3) et num_ctx, la taille du contexte : sans lui, Ollama se limite à 4 096 tokens sur une machine sans grosse carte graphique. Plus de contexte demande plus de mémoire vive. Le délai de 300 secondes laisse le temps au premier appel de charger le modèle en mémoire ; Ollama le décharge ensuite après 5 minutes d'inactivité.
Installer Ollama sur un serveur de l'entreprise
Sorbier veut un modèle interne pour résumer les tickets, sans envoyer leurs données à l'extérieur. Moussa crée la VM srv-ia sous Debian 13 dans le VLAN 21 APPLIS, à l'adresse 192.168.21.40 (passerelle 192.168.21.254), avec 4 vCPU et 16 Go de RAM : de quoi faire tourner llama3.2:3b et embeddinggemma sans carte graphique, lentement mais correctement pour quelques utilisateurs. Le cours Virtualisation montre comment créer la VM sous Proxmox VE.
Par défaut, Ollama n'écoute que sur 127.0.0.1 : l'application d'assistance, hébergée sur le serveur de l'intranet (192.168.21.10), ne peut pas le joindre. Pour l'ouvrir au réseau, on définit OLLAMA_HOST dans le service systemd :
sudo systemctl edit ollama
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
sudo systemctl daemon-reload
sudo systemctl restart ollama
sudo systemctl status ollama # le service tourne-t-il ?
journalctl -e -u ollama # ses journaux
Deux barrières, comme pour tout service interne :
-
Le pare-feu local de
srv-ian'accepte le port 11434 que de l'application et du réseau d'administration. C'est indispensable : l'intranet etsrv-iasont dans le même VLAN, et le trafic entre eux ne passe pas par pfSense. Dans/etc/nftables.conf, sur le modèle du pare-feu local du cours Services Linux :ip saddr 192.168.21.10 tcp dport 11434 accept ip saddr 192.168.10.208/28 tcp dport { 22, 11434 } accept -
pfSense n'autorise aucun autre VLAN à joindre
192.168.21.40:11434: les postes des salariés passent par l'application, jamais directement par le modèle (règles du pare-feu).
Pour exposer le modèle à plus de monde, on place devant lui un proxy inverse Nginx qui ajoute HTTPS et une authentification (reverse proxy), et on journalise les requêtes.
Exercices
Exercice 1 sur 4 Une clé dans le code Réussi À revoir
Le code de Yanis commence par client = anthropic.Anthropic(api_key="sk-ant-api03-…") et il a poussé son dépôt sur GitHub. Que doit-il faire, dans l'ordre ?
Voir la réponseMasquer la réponse de l'exercice 1
- Révoquer la clé dans la console du fournisseur, tout de suite : elle a pu être copiée par un robot qui surveille les dépôts publics.
- Créer une nouvelle clé et la placer dans la variable d'environnement
ANTHROPIC_API_KEY. - Remplacer la ligne par
client = anthropic.Anthropic(), qui lit la variable. - Vérifier la consommation récente dans la console pour repérer un usage frauduleux.
Supprimer la ligne et commiter ne suffit pas : la clé reste dans l'historique Git. La révocation est la seule mesure qui la rend inutilisable.
Tu avais trouvé ?
Exercice 2 sur 4 L'assistant qui oublie Réussi À revoir
Chloé a écrit ce code. Elle tape « Je m'appelle Chloé », puis « Comment je m'appelle ? » : le modèle répond qu'il ne sait pas. Explique pourquoi et corrige.
while True:
question = input("Toi : ")
reponse = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
messages=[{"role": "user", "content": question}],
)
print("".join(b.text for b in reponse.content if b.type == "text"))
Voir la réponseMasquer la réponse de l'exercice 2
L'API est sans état : chaque requête ne contient que la dernière question, donc le modèle ne voit jamais « Je m'appelle Chloé ». Le programme doit garder l'historique et l'envoyer à chaque tour :
historique = []
while True:
question = input("Toi : ")
historique.append({"role": "user", "content": question})
reponse = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
messages=historique,
)
texte = "".join(b.text for b in reponse.content if b.type == "text")
historique.append({"role": "assistant", "content": texte})
print(texte)
Tu avais trouvé ?
Exercice 3 sur 4 Lire usage Réussi À revoir
Une réponse de Claude Opus 5.5 indique input_tokens = 1200 et output_tokens = 450. Le modèle coûte 4 dollars par million de tokens en entrée et 20 dollars par million en sortie. Combien coûte la requête ? Quelle part vient de la sortie ?
Voir la réponseMasquer la réponse de l'exercice 3
Entrée : 1 200 × 4 / 1 000 000 = 0,0048 $. Sortie : 450 × 20 / 1 000 000 = 0,009 $. Total : 0,0138 $, soit environ 1,4 centime de dollar.
La sortie représente 0,009 / 0,0138 ≈ 65 % du coût, alors qu'elle compte trois fois moins de tokens : un token de sortie coûte cinq fois plus cher qu'un token d'entrée. Les tokens de réflexion sont comptés dans la sortie.
Tu avais trouvé ?
Exercice 4 sur 4 Ouvrir Ollama, mais pas trop Réussi À revoir
Après le passage à OLLAMA_HOST=0.0.0.0:11434, Moussa constate depuis son poste du VLAN POSTES (192.168.10.64/26) que curl http://192.168.21.40:11434/api/tags répond.
- Pourquoi est-ce un problème ?
- Quelles règles corrigent la situation, sur pfSense et sur
srv-ia?
Voir la réponseMasquer la réponse de l'exercice 4
- L'API d'Ollama n'a pas d'authentification : n'importe quel poste peut utiliser le modèle, en télécharger d'autres jusqu'à remplir le disque, ou supprimer les modèles en place. Un poste compromis l'atteint aussi.
- Sur pfSense, l'interface POSTES ne doit avoir aucune règle qui autorise
192.168.21.40port 11434 ; si une règle trop large existe (vers tout le VLAN APPLIS, tous ports), Moussa la restreint aux seuls services prévus, comme HTTPS vers l'intranet. Sursrv-ia, le pare-feu local nftables n'accepte le port 11434 que de192.168.21.10(l'application) et de192.168.10.208/28(l'administration), et refuse le reste. Moussa refait ensuite le test depuis son poste : la connexion doit échouer.
Tu avais trouvé ?