écrits/tutorial/2026/07
Tutorial11 juil. 2026·32 min

Créer des agents IA de production avec le Claude Agent SDK pour Python

Apprenez à créer des agents IA autonomes en Python avec le Claude Agent SDK d'Anthropic. Ce tutoriel pratique couvre la boucle query, le streaming avec ClaudeSDKClient, les outils MCP personnalisés, les rappels de permission, les hooks et les patterns de production.

Anthropic n'a pas seulement conçu Claude — l'entreprise a empaqueté le harnais d'agent exact qui alimente Claude Code et l'a publié sous forme de bibliothèque. Le Claude Agent SDK pour Python vous offre la même boucle d'agent, l'exécution d'outils, la gestion du contexte et le système de permissions, sous forme de code importable et composable.

Si vous vivez en Python — pipelines de données, services back-end, scripts d'automatisation, outillage ML — c'est le moyen le plus rapide d'ajouter un agent véritablement autonome à votre stack. Dans ce tutoriel, vous passerez d'un environnement virtuel vierge à un agent qui lit des fichiers, exécute des commandes, appelle vos propres outils personnalisés et applique les règles de sécurité que vous définissez.

L'Agent SDK face à l'API Messages brute. L'API Messages vous donne une seule réponse du modèle. L'Agent SDK vous donne la boucle autour d'elle : il appelle les outils, réinjecte les résultats, gère la fenêtre de contexte, réessaie et s'arrête une fois la tâche terminée. Vous décrivez l'objectif ; le SDK exécute l'agent.

Ce que vous allez apprendre

À la fin de ce tutoriel, vous saurez :

  • Configurer un projet Python avec le Claude Agent SDK
  • Lancer un agent autonome à usage unique avec la fonction query
  • Mener une conversation multi-tours en streaming avec ClaudeSDKClient
  • Exposer vos propres fonctions Python comme outils via le décorateur @tool
  • Restreindre les actions dangereuses avec un rappel de permission can_use_tool
  • Observer et auditer chaque appel d'outil avec les hooks de cycle de vie
  • Appliquer des patterns de production pour le coût, la sécurité et la fiabilité

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Python 3.10 ou plus récent installé (python --version)
  • Node.js 18+ installé — le SDK dialogue avec la CLI Claude Code en arrière-plan
  • Une clé API Anthropic depuis la console, ou un abonnement Claude actif
  • Une familiarité de base avec async/await et asyncio en Python

Étape 1 : Configuration du projet

Créez un environnement isolé et installez le SDK. Nous utiliserons le venv intégré, mais uv ou poetry fonctionnent tout aussi bien.

mkdir claude-agent-python && cd claude-agent-python
python -m venv .venv
source .venv/bin/activate  # Windows : .venv\Scripts\activate
 
pip install claude-agent-sdk

Le SDK pilote la CLI Claude Code, installez-la donc également :

npm install -g @anthropic-ai/claude-code

Enfin, exportez votre clé pour que le SDK puisse s'authentifier :

export ANTHROPIC_API_KEY="sk-ant-..."

Astuce : Ne codez jamais votre clé API en dur dans le code source. Utilisez une variable d'environnement ou un gestionnaire de secrets. Ajoutez un fichier .env à .gitignore et chargez-le avec python-dotenv si vous préférez garder les clés dans un fichier localement.

Étape 2 : Votre premier agent avec query

La fonction query est le point d'entrée le plus simple. Elle exécute une boucle d'agent entièrement autonome et produit les messages au fil de leur streaming. Créez first_agent.py :

import anyio
from claude_agent_sdk import query, ClaudeAgentOptions
 
async def main():
    options = ClaudeAgentOptions(
        system_prompt="You are a concise senior Python engineer.",
        allowed_tools=["Read", "Glob", "Grep"],
        cwd=".",
    )
 
    async for message in query(
        prompt="List the Python files in this folder and summarize what each does.",
        options=options,
    ):
        print(message)
 
anyio.run(main)

Exécutez-le :

python first_agent.py

L'agent décide de lui-même d'appeler Glob pour trouver les fichiers, Read pour les ouvrir, puis rédige un résumé. Vous n'avez écrit aucune orchestration — le SDK a exécuté la boucle. Notez que nous avons passé allowed_tools pour restreindre l'agent aux opérations en lecture seule ; il ne peut ni écrire ni exécuter de commandes shell ici.

Comprendre le flux de messages

query produit des objets message typés. Les plus utiles sont :

  • AssistantMessage — le texte et les blocs d'utilisation d'outils produits par le modèle
  • UserMessage — les résultats d'outils réinjectés dans la boucle
  • ResultMessage — le résumé final, incluant la consommation de tokens et le coût

Filtrons le flux pour n'afficher que le texte de l'assistant et le coût final :

from claude_agent_sdk import (
    query, ClaudeAgentOptions,
    AssistantMessage, TextBlock, ResultMessage,
)
 
async def main():
    options = ClaudeAgentOptions(allowed_tools=["Read", "Glob"])
 
    async for message in query(prompt="Summarize README.md", options=options):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, TextBlock):
                    print(block.text)
        elif isinstance(message, ResultMessage):
            print(f"\n--- done in {message.num_turns} turns, "
                  f"cost ${message.total_cost_usd:.4f} ---")

Étape 3 : Conversations multi-tours avec ClaudeSDKClient

query est sans état — une invite, une exécution. Pour les sessions interactives où l'agent mémorise le contexte d'un tour à l'autre, utilisez ClaudeSDKClient. Il maintient la session active pour envoyer des invites de suivi.

import anyio
from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions,
    AssistantMessage, TextBlock,
)
 
async def main():
    options = ClaudeAgentOptions(
        system_prompt="You are a helpful coding assistant.",
        allowed_tools=["Read", "Write", "Edit", "Bash"],
        permission_mode="acceptEdits",
    )
 
    async with ClaudeSDKClient(options=options) as client:
        # Premier tour
        await client.query("Create a file hello.py that prints today's date.")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
 
        # Tour de suivi — l'agent se souvient de hello.py
        await client.query("Now add a function that returns the date as ISO 8601.")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
 
anyio.run(main)

Le bloc async with gère le cycle de vie de la session. client.query() envoie une invite ; client.receive_response() diffuse les messages jusqu'à la fin de ce tour. Comme le client persiste, la seconde invite opère sur le fichier créé lors de la première.

permission_mode a son importance. Le mode par défaut demande une confirmation avant les modifications. Nous avons choisi acceptEdits ici pour que l'agent puisse écrire des fichiers sans invite — approprié pour un script de confiance, mais à l'étape 5 vous apprendrez à contrôler précisément quelles actions sont autorisées.

Étape 4 : Outils personnalisés avec le décorateur @tool

Les outils intégrés couvrent le système de fichiers et le shell. Pour connecter votre agent à votre monde — une base de données, une API interne, un moteur de tarification — définissez des outils personnalisés. Le SDK les expose via un serveur MCP intra-processus, sans donc aucun saut réseau.

Voici un agent capable de rechercher des commandes et de calculer les frais d'expédition, adossé à de simples fonctions Python :

import anyio
from claude_agent_sdk import (
    tool, create_sdk_mcp_server,
    ClaudeSDKClient, ClaudeAgentOptions,
    AssistantMessage, TextBlock,
)
 
# Un magasin de données fictif pour la démo
ORDERS = {
    "A100": {"customer": "Layla", "weight_kg": 2.5, "country": "TN"},
    "A101": {"customer": "Omar", "weight_kg": 8.0, "country": "SA"},
}
 
@tool("get_order", "Fetch an order by its ID", {"order_id": str})
async def get_order(args):
    order = ORDERS.get(args["order_id"])
    if not order:
        return {"content": [{"type": "text", "text": "Order not found"}]}
    return {"content": [{"type": "text", "text": str(order)}]}
 
@tool("shipping_cost", "Estimate shipping cost in USD",
      {"weight_kg": float, "country": str})
async def shipping_cost(args):
    base = 5.0 if args["country"] == "TN" else 12.0
    cost = base + args["weight_kg"] * 1.5
    return {"content": [{"type": "text", "text": f"${cost:.2f}"}]}
 
async def main():
    server = create_sdk_mcp_server(
        name="fulfillment",
        version="1.0.0",
        tools=[get_order, shipping_cost],
    )
 
    options = ClaudeAgentOptions(
        mcp_servers={"fulfillment": server},
        allowed_tools=[
            "mcp__fulfillment__get_order",
            "mcp__fulfillment__shipping_cost",
        ],
    )
 
    async with ClaudeSDKClient(options=options) as client:
        await client.query("What will it cost to ship order A101?")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
 
anyio.run(main)

Trois points à remarquer :

  1. Le décorateur @tool prend un nom, une description et un schéma associant les noms d'arguments à leurs types. Claude lit la description pour décider quand l'appeler.
  2. create_sdk_mcp_server regroupe vos outils dans un serveur que vous enregistrez sous un espace de noms.
  3. Les outils sont adressés sous la forme mcp__<server>__<tool>. Vous devez les lister dans allowed_tools, sans quoi l'agent ne peut pas les appeler.

L'agent enchaînera les outils de lui-même : récupérer la commande A101, lire le poids et le pays, puis appeler shipping_cost avec ces valeurs — sans aucun câblage manuel.

Étape 5 : Protéger les actions avec un rappel de permission

L'autonomie sans garde-fous est un risque. Le rappel can_use_tool s'exécute avant chaque appel d'outil, vous permettant d'autoriser, de refuser ou de réécrire selon votre propre logique.

from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions,
    PermissionResultAllow, PermissionResultDeny,
    ToolPermissionContext,
)
 
async def gatekeeper(tool_name: str, tool_input: dict,
                     context: ToolPermissionContext):
    # Bloquer d'emblée les commandes shell destructrices
    if tool_name == "Bash":
        command = str(tool_input.get("command", ""))
        for danger in ("rm -rf", "sudo", "mkfs", ":(){"):
            if danger in command:
                return PermissionResultDeny(
                    message=f"Blocked dangerous command: {danger}",
                    interrupt=True,
                )
    # Tout le reste est autorisé
    return PermissionResultAllow()
 
async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Write", "Bash"],
        can_use_tool=gatekeeper,
    )
    async with ClaudeSDKClient(options=options) as client:
        await client.query("Clean up temp files in this directory.")
        async for msg in client.receive_response():
            print(msg)

Si l'agent tente quoi que ce soit correspondant à votre liste de blocage, le rappel renvoie PermissionResultDeny avec interrupt=True, ce qui arrête immédiatement l'exécution. Renvoyez PermissionResultAllow pour continuer. C'est votre dernière ligne de défense — gardez cette logique simple, explicite et bien testée.

Défense en profondeur. Combinez trois couches : allowed_tools pour lister les capacités autorisées, can_use_tool pour inspecter chaque appel à l'exécution, et cwd plus un bac à sable au niveau du système d'exploitation pour limiter le rayon d'action. Ne comptez jamais sur le seul bon comportement du modèle.

Étape 6 : Observabilité avec les hooks

Pour tout ce qui tourne sans surveillance, il vous faut une piste d'audit. Les hooks se déclenchent sur les événements du cycle de vie pour journaliser, mesurer ou opposer un veto aux actions sans toucher au code de vos outils. Enregistrez-les avec HookMatcher :

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
 
async def audit_pre_tool(input_data, tool_use_id, context):
    print(f"[AUDIT] about to run {input_data['tool_name']} "
          f"(id={tool_use_id})")
    return {}  # dict vide = autoriser, continuer normalement
 
async def audit_post_tool(input_data, tool_use_id, context):
    print(f"[AUDIT] finished {input_data['tool_name']}")
    return {}
 
options = ClaudeAgentOptions(
    allowed_tools=["Read", "Bash"],
    hooks={
        "PreToolUse": [HookMatcher(hooks=[audit_pre_tool])],
        "PostToolUse": [HookMatcher(hooks=[audit_post_tool])],
    },
)

L'argument matcher de HookMatcher peut cibler un outil précis (par exemple matcher="Bash") afin qu'un hook ne se déclenche que pour cet outil. En production, remplacez les appels print par votre framework de journalisation et envoyez les enregistrements vers votre stack d'observabilité.

Étape 7 : Un mini-agent complet

Assemblons le tout en un petit agent réaliste : un assistant de dépôt doté d'un outil personnalisé, d'une barrière de permission et de hooks d'audit.

import anyio
from claude_agent_sdk import (
    tool, create_sdk_mcp_server,
    ClaudeSDKClient, ClaudeAgentOptions,
    HookMatcher, PermissionResultAllow, PermissionResultDeny,
    AssistantMessage, TextBlock, ResultMessage,
)
 
@tool("count_todos", "Count TODO comments in a source file",
      {"path": str})
async def count_todos(args):
    try:
        with open(args["path"], encoding="utf-8") as fh:
            n = sum(1 for line in fh if "TODO" in line)
        return {"content": [{"type": "text", "text": f"{n} TODOs"}]}
    except FileNotFoundError:
        return {"content": [{"type": "text", "text": "File not found"}]}
 
async def gatekeeper(tool_name, tool_input, context):
    if tool_name == "Bash" and "rm " in str(tool_input.get("command", "")):
        return PermissionResultDeny(message="No deletions allowed")
    return PermissionResultAllow()
 
async def audit(input_data, tool_use_id, context):
    print(f"[hook] {input_data['tool_name']}")
    return {}
 
async def main():
    server = create_sdk_mcp_server(name="repo", tools=[count_todos])
    options = ClaudeAgentOptions(
        system_prompt="You are a meticulous code auditor.",
        mcp_servers={"repo": server},
        allowed_tools=["Read", "Glob", "Grep", "mcp__repo__count_todos"],
        can_use_tool=gatekeeper,
        hooks={"PreToolUse": [HookMatcher(hooks=[audit])]},
        max_turns=10,
    )
 
    async with ClaudeSDKClient(options=options) as client:
        await client.query(
            "Find the Python file with the most TODO comments and report it."
        )
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
            elif isinstance(msg, ResultMessage):
                print(f"\nUsage: {msg.usage}")
 
anyio.run(main)

Cet agent parcourt l'arborescence, lit les fichiers candidats, appelle votre outil count_todos et signale le gagnant — pendant que chaque appel d'outil est audité et que les suppressions sont bloquées. Notez max_turns=10, qui plafonne la boucle pour qu'un agent désorienté ne tourne pas indéfiniment (et ne fasse pas grimper le coût).

Tester votre implémentation

Vérifiez que chaque pièce fonctionne :

  1. Agent en lecture seule — exécutez l'étape 2 dans un dossier contenant deux fichiers .py. Vous devriez voir un résumé par fichier et aucune écriture.
  2. Outils personnalisés — exécutez l'étape 4 et confirmez que l'agent appelle bien get_order et shipping_cost pour la commande A101.
  3. Barrière de permission — demandez à l'agent de l'étape 5 d'exécuter rm -rf /tmp/x et confirmez qu'il est bloqué avec votre message de refus.
  4. Hooks — confirmez que les lignes [AUDIT] s'affichent avant chaque appel d'outil à l'étape 6.

Dépannage

  • CLINotFoundError ou « command not found » — la CLI Claude Code est absente. Exécutez npm install -g @anthropic-ai/claude-code.
  • Erreurs d'authentification — vérifiez que ANTHROPIC_API_KEY est exportée dans le même shell qui exécute le script.
  • L'agent refuse d'appeler un outil personnalisé — vérifiez qu'il figure dans allowed_tools avec le nom exact mcp__server__tool, et que sa description indique clairement quand l'utiliser.
  • Boucles incontrôlées ou coût élevé — définissez max_turns et inspectez ResultMessage.total_cost_usd pour repérer tôt les invites coûteuses.
  • RuntimeError sur la boucle d'événements — pilotez toujours le SDK avec anyio.run(main) ou asyncio.run(main()) ; n'appelez jamais les fonctions asynchrones du SDK de manière synchrone.

Patterns de production

  • Moindre privilège par défaut. Commencez avec une liste allowed_tools vide et ajoutez les capacités une à une selon les besoins de la tâche.
  • Plafonnez les tours et le budget. Utilisez max_turns et journalisez total_cost_usd de chaque ResultMessage dans un magasin de métriques.
  • Gardez les outils purs et rapides. Les outils personnalisés doivent être petits, déterministes et rapides. Reléguez les E/S lentes derrière une file plutôt que de bloquer la boucle de l'agent.
  • Journalisez tout avec les hooks. Pour les tâches cron et les workers en arrière-plan, les journaux d'audit basés sur les hooks ne sont pas négociables — c'est ainsi que vous déboguez une exécution survenue à 3 h du matin.
  • Épinglez le modèle. Définissez model explicitement dans ClaudeAgentOptions pour qu'un changement de valeur par défaut du fournisseur n'altère jamais silencieusement le comportement ou le coût.

Étapes suivantes

Conclusion

Le Claude Agent SDK pour Python transforme le harnais d'agent derrière Claude Code en une bibliothèque que vous contrôlez. Vous avez exécuté un agent à usage unique avec query, mené une conversation avec état grâce à ClaudeSDKClient, exposé votre propre logique via le décorateur @tool, et superposé des rappels de permission et des hooks pour la sécurité et l'observabilité.

Le pattern passe à l'échelle proprement : décrivez l'objectif, mettez les outils en liste blanche, protégez les plus dangereux et laissez le SDK exécuter la boucle. Commencez par un agent en lecture seule sur une tâche réelle de votre base de code, puis accordez les capacités délibérément à mesure que la confiance grandit. C'est ainsi que vous livrez des agents autonomes à la fois utiles et sûrs.