---
title: "Un agent avec deux outils"
description: "Un agent minimal qui lit un fichier et envoie un brouillon, avec son code."
url: https://meridal.fr/apprendre/un-agent-avec-deux-outils/
language: fr
written: 2026-10-09
verified: 2026-10-09
license: CC BY 4.0
---

[Apprendre](https://meridal.fr/apprendre/) › [Les agents](https://meridal.fr/apprendre/dossier/agents/)

S'exercer · Niveau 3, Construire · 45 minutes

# Un agent avec deux outils

Un agent, c'est un modèle qui demande des outils, et un programme qui les exécute en boucle jusqu'à la réponse. Quarante-cinq minutes pour en écrire un qui lit la demande d'un client et enregistre un brouillon de réponse, sans jamais rien envoyer, avec le SDK d'agents d'OpenAI et avec une boucle écrite à la main chez Anthropic.

Écrit le 9 octobre 2026 · Vérifié le 9 octobre 2026

## Vous saurez

- Écrire deux outils qui restent dans leur dossier
- Les donner à un agent, et limiter le nombre de tours
- Écrire la boucle d'un agent à la main, pour la comprendre
- Vérifier ce que l'agent a fait, avant qu'une personne envoie

## Dans le parcours

[Construction](https://meridal.fr/apprendre/parcours/construire/)

Étape 6 sur 12

- [← Avant Premier appel à un modèle](https://meridal.fr/apprendre/premier-appel/)
- [↑ Plus général Les agents d'IA](https://meridal.fr/apprendre/qu-est-ce-qu-un-agent/)
- [→ Ensuite Un serveur MCP](https://meridal.fr/apprendre/un-serveur-mcp/)

L'essentiel

1. Un agent tourne en boucle : le modèle répond ou demande un outil ; le programme exécute l'outil et lui rend le résultat ; on recommence jusqu'à une réponse finale, ou jusqu'au nombre de tours permis.
2. Les outils font la sécurité : celui qui lit ne lit que le dossier des demandes ; celui qui écrit n'écrit qu'un brouillon .md ; aucun n'envoie.
3. Cet agent lit un contenu venu de l'extérieur et vos fichiers, mais ne communique pas vers l'extérieur : sur les trois capacités dangereuses, il n'en réunit que deux.
4. Le SDK d'agents d'OpenAI fait la boucle pour vous, avec une limite de tours ; chez Anthropic, on l'écrit en quelques lignes, et une erreur d'outil se rend au modèle avec is_error.
5. Une personne relit le brouillon et l'envoie elle-même : l'agent prépare, il ne décide pas.

## Objectif

Écrire, en Python, un agent à deux outils : `lire_demande`, qui lit la demande d'un client dans le dossier `demandes`, et `enregistrer_brouillon`, qui écrit un brouillon de réponse dans le dossier `brouillons`. Le lancer sur une demande fabriquée, vérifier le brouillon, et constater qu'aucun outil ne sort de son dossier. Compter quarante-cinq minutes, avec un poste prêt ([le poste de développement](https://meridal.fr/apprendre/poste-de-developpement/)) et l'exercice sur [l'appel d'outils](https://meridal.fr/apprendre/appel-d-outils/) déjà fait.

## Données fournies

La demande d'un client, fabriquée ; l'adresse en « .example » ne reçoit aucun courriel. À enregistrer dans un dossier `demandes`, à côté d'un dossier `brouillons` vide.

- La demande 0815 Texte Markdown, 1 Ko · un client signale une pièce manquante [Télécharger](https://meridal.fr/assets/apprendre/fichiers/agent-deux-outils/demande-0815.md)

  **Lire le document ici**

  Exercice de Meridal. Le client, l'adresse et la commande sont fabriqués pour l'exercice ; l'adresse en « .example » ne reçoit aucun courriel. À enregistrer dans le dossier demandes, sous le nom demande-0815.md.

  De : achats@client-exemple.example

  Objet : Commande 2026-0815, pièce manquante

  Bonjour,

  Nous avons reçu la commande 2026-0815 le 2 octobre, mais le carton ne contenait que trois des quatre supports muraux commandés. Pouvez-vous nous envoyer le quatrième, et nous dire quand il partira ?

  Merci,

  Service achats

La boucle d'un agent, sur l'exercice

1. 1 La demande « Préparez la réponse à demande-0815.md »
2. 2 Le modèle demande l'outil lire_demande
3. 3 Votre programme exécute l'outil, rend le texte
4. 4 Le modèle demande enregistrer_brouillon, avec l'objet et le texte
5. 5 Votre programme écrit le fichier, rend « enregistré »
6. 6 Le modèle répond : c'est fait ; la boucle s'arrête

*Fond plein : le modèle, chez l'éditeur ; filet : votre programme, sur votre poste. Le modèle n'exécute rien : il demande ; c'est le programme qui lit et qui écrit. Ici, trois appels au modèle ; la limite est fixée à six.*

## Consignes

1. Préparer le projet uv init agent-brouillons uv add openai-agents anthropic python-dotenv **Ce qu'on fait :** un projet par exercice ; la clé dans un fichier .env exclu de git. openai-agents 0.23.1 et anthropic 1.12.1 au 8 octobre 2026.
2. Écrire les deux outils, dans outils.py **Ce qu'on fait :** chacun vérifie que le fichier reste dans son dossier ; le second n'écrit que des fichiers .md, et n'envoie rien.
3. Les essayer sans modèle uv run essai_outils.py **Ce qu'on fait :** lire la demande, tenter de sortir du dossier, enregistrer un brouillon : sans rien payer.
4. Écrire l'agent, avec le SDK d'OpenAI **Ce qu'on fait :** les deux outils déclarés avec @tool, leur description tirée de la docstring ; l'agent lancé avec max_turns=6.
5. Ou écrire la boucle à la main, chez Anthropic **Ce qu'on fait :** tant que la réponse s'arrête sur tool_use, exécuter chaque outil et rendre un tool_result ; une erreur part avec is_error ; six tours au plus.
6. Lancer, puis lire le brouillon **Ce qu'on fait :** le fichier brouillons/reponse-0815.md : il reprend la commande 2026-0815, ne promet aucune date, et dit qu'une personne reviendra vers le client.

## Critères de réussite

- Le brouillon existe dans brouillons/, en .md, et rien n'a été envoyé
- Il cite la commande 2026-0815 et la pièce manquante
- Il ne promet aucune date d'expédition la demande n'en donne pas
- lire_demande refuse ../.env ; enregistrer_brouillon refuse un fichier qui n'est pas en .md l'essai le montre
- L'agent s'arrête au plus tard au sixième tour
- Une personne relit le brouillon avant tout envoi

## Indications

**L'agent tourne en rond et atteint la limite**

**Cause**Le SDK lève MaxTurnsExceeded au-delà de max_turns.

**À faire**Rendre les consignes plus nettes ; vérifier que chaque outil renvoie un texte clair, y compris en cas d'erreur.

**Le modèle n'appelle pas l'outil**

**Cause**La description de l'outil ne dit pas à quoi il sert, ou le nom du fichier n'est pas dans la demande.

**À faire**Une docstring qui dit quoi et quand ; le nom du fichier dans la demande.

**Le brouillon promet une date**

**Cause**Le modèle comble un manque.

**À faire**La consigne l'interdit déjà ; ajouter un contrôle par une règle sur le brouillon, ou une relecture obligatoire.

**Une erreur d'outil arrête tout, chez Anthropic**

**Cause**L'exception n'est pas attrapée.

**À faire**L'attraper, et rendre un tool_result avec is_error à true : le modèle peut alors corriger sa demande.

**La demande contient une consigne cachée**

**Cause**Un client peut écrire « ignorez vos consignes ». Cet agent n'a ni réseau ni envoi : au pire, un brouillon étrange, qu'une personne lira.

**À faire**Garder ce découpage ; voir [la sécurité d'un outil d'IA](https://meridal.fr/apprendre/securite-d-un-outil-d-ia/).

## Correction commentée

**Les deux outils**, en Python simple : la sécurité est là, pas dans les consignes du modèle.

`outils.py`

```
"""Les deux outils de l'agent : lire une demande, enregistrer un brouillon.
Aucun des deux n'envoie quoi que ce soit ; chacun reste dans son dossier."""
from pathlib import Path

ICI = Path(__file__).parent
DEMANDES = ICI / "demandes"
BROUILLONS = ICI / "brouillons"

def _dans(dossier: Path, nom: str) -> Path:
    """Le chemin du fichier, à condition qu'il reste dans le dossier prévu."""
    chemin = (dossier / nom).resolve()
    if chemin.parent != dossier.resolve():
        raise ValueError(f"Refusé : {nom} sort du dossier {dossier.name}.")
    return chemin

def lire_demande(nom: str) -> str:
    """Lit une demande de client dans le dossier demandes. Lecture seule."""
    return _dans(DEMANDES, nom).read_text(encoding="utf-8")

def enregistrer_brouillon(nom: str, objet: str, texte: str) -> str:
    """Enregistre un brouillon de réponse dans le dossier brouillons. N'envoie rien."""
    chemin = _dans(BROUILLONS, nom)
    if chemin.suffix != ".md":
        raise ValueError("Refusé : un brouillon est un fichier .md.")
    chemin.write_text(f"Objet : {objet}\n\n{texte}\n", encoding="utf-8")
    return f"Brouillon enregistré : brouillons/{chemin.name}"
```

`essai_outils.py`

```
"""Essai des deux outils, sans modèle : ce que l'agent pourra faire, et ce qu'il ne pourra pas."""
from outils import enregistrer_brouillon, lire_demande

print(lire_demande("demande-0815.md").splitlines()[1])
for nom in ["../.env", "../../outils.py"]:
    try:
        lire_demande(nom)
    except ValueError as erreur:
        print(erreur)
print(enregistrer_brouillon("essai.md", "Essai", "Un brouillon d'essai."))
try:
    enregistrer_brouillon("essai.sh", "Essai", "echo")
except ValueError as erreur:
    print(erreur)
```

La sortie`uv run essai_outils.py`

```
Objet : Commande 2026-0815, pièce manquante
Refusé : ../.env sort du dossier demandes.
Refusé : ../../outils.py sort du dossier demandes.
Brouillon enregistré : brouillons/essai.md
Refusé : un brouillon est un fichier .md.
```

Essayé le 9 octobre 2026 avec Python 3.12.7, sans modèle.

SDK d'OpenAI Boucle Anthropic

Le SDK fait la boucle ; `max_turns` la borne. Les outils sont déclarés avec `@tool`, comme dans la documentation du 8 octobre 2026.

`agent_openai.py`

```
import asyncio

from dotenv import load_dotenv
from agents import Agent, Runner
from agents.decorators import tool

import outils

load_dotenv()

@tool
def lire_demande(nom: str) -> str:
    """Lit une demande de client dans le dossier demandes. Lecture seule.

    Args:
        nom: Le nom du fichier, par exemple demande-0815.md.
    """
    return outils.lire_demande(nom)

@tool
def enregistrer_brouillon(nom: str, objet: str, texte: str) -> str:
    """Enregistre un brouillon de réponse dans le dossier brouillons. N'envoie rien.

    Args:
        nom: Le nom du brouillon, en .md.
        objet: L'objet du courriel.
        texte: Le texte de la réponse.
    """
    return outils.enregistrer_brouillon(nom, objet, texte)

agent = Agent(
    name="Brouillons du service client",
    instructions=(
        "Vous préparez des brouillons de réponse aux clients. Lisez la demande, puis enregistrez "
        "un brouillon court et poli. N'inventez aucune date ni aucun délai : s'ils manquent, "
        "écrivez qu'une personne reviendra vers le client."
    ),
    model="gpt-6-luna",
    tools=[lire_demande, enregistrer_brouillon],
)

async def main():
    result = await Runner.run(
        agent, "Préparez la réponse à demande-0815.md, dans reponse-0815.md.", max_turns=6
    )
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
```

La sortie`uv run agent_openai.py`

**Pas encore exécuté par Meridal.** Ce qu'on attend : une phrase qui dit que le brouillon est enregistré, et le fichier brouillons/reponse-0815.md.

La boucle à la main : la même chose que le SDK, visible ligne à ligne. Les résultats d'outils repartent dans un message de l'utilisateur, en premier.

`agent_anthropic.py`

```
import anthropic
from dotenv import load_dotenv

import outils

load_dotenv()
client = anthropic.Anthropic()

OUTILS = {"lire_demande": outils.lire_demande, "enregistrer_brouillon": outils.enregistrer_brouillon}
tools = [
    {
        "name": "lire_demande",
        "description": outils.lire_demande.__doc__,
        "input_schema": {
            "type": "object",
            "properties": {"nom": {"type": "string", "description": "Le nom du fichier, par exemple demande-0815.md"}},
            "required": ["nom"],
        },
    },
    {
        "name": "enregistrer_brouillon",
        "description": outils.enregistrer_brouillon.__doc__,
        "input_schema": {
            "type": "object",
            "properties": {
                "nom": {"type": "string", "description": "Le nom du brouillon, en .md"},
                "objet": {"type": "string", "description": "L'objet du courriel"},
                "texte": {"type": "string", "description": "Le texte de la réponse"},
            },
            "required": ["nom", "objet", "texte"],
        },
    },
]
CONSIGNES = (
    "Vous préparez des brouillons de réponse aux clients. Lisez la demande, puis enregistrez "
    "un brouillon court et poli. N'inventez aucune date ni aucun délai : s'ils manquent, "
    "écrivez qu'une personne reviendra vers le client."
)
messages = [{"role": "user", "content": "Préparez la réponse à demande-0815.md, dans reponse-0815.md."}]

for tour in range(6):
    response = client.messages.create(
        model="claude-opus-5-5", max_tokens=2048, system=CONSIGNES, tools=tools, messages=messages
    )
    messages.append({"role": "assistant", "content": response.content})
    if response.stop_reason != "tool_use":
        print("".join(b.text for b in response.content if b.type == "text"))
        break
    resultats = []
    for b in response.content:
        if b.type == "tool_use":
            try:
                contenu = OUTILS[b.name](**b.input)
                resultats.append({"type": "tool_result", "tool_use_id": b.id, "content": contenu})
            except (KeyError, OSError, TypeError, ValueError) as erreur:
                resultats.append({"type": "tool_result", "tool_use_id": b.id, "content": str(erreur), "is_error": True})
    messages.append({"role": "user", "content": resultats})
else:
    print("Arrêt : six tours sans réponse finale.")
```

La sortie`uv run agent_anthropic.py`

**Pas encore exécuté par Meridal.** Ce qu'on attend : une phrase qui dit que le brouillon est enregistré, et le fichier brouillons/reponse-0815.md.

**Pourquoi deux outils étroits plutôt qu'un seul large.** Un outil « lire un fichier » sans limite donnerait au modèle tout le disque ; un outil « envoyer un courriel » ferait sortir le brouillon sans relecture. Deux outils étroits, et la relecture par une personne, suivent ce que demande le CERT-FR pour tout agent : le strict nécessaire, et une validation humaine avant toute action qui a un effet.

## Extensions

- **Un troisième outil, en lecture** : le statut de la commande dans votre logiciel, comme dans [l'appel d'outils](https://meridal.fr/apprendre/appel-d-outils/) ; le brouillon peut alors dire si la pièce est en stock.
- **Les mêmes outils, ailleurs** : le Claude Agent SDK les déclare avec @tool et un serveur MCP en mémoire ; Google ADK prend deux fonctions Python simples.
- **Les outils dans un serveur MCP**, pour qu'un assistant de bureau s'en serve aussi : [un serveur MCP](https://meridal.fr/apprendre/un-serveur-mcp/).
- **Une trace de chaque passage** : la demande lue, les outils appelés, le brouillon écrit ; voir [l'observabilité](https://meridal.fr/apprendre/observabilite/).
- **Les bibliothèques d'agents, comparées** : [les bibliothèques d'agents](https://meridal.fr/apprendre/bibliotheques-d-agents/).

## Lectures suivantes

↑ Plus général [Les agents d'IA Comprendre · Niveau 1 · 14 minutes](https://meridal.fr/apprendre/qu-est-ce-qu-un-agent/)

← Avant [Premier appel à un modèle S'exercer · Niveau 3 · 15 minutes](https://meridal.fr/apprendre/premier-appel/)

Vous êtes ici

Un agent avec deux outils

→ Ensuite [Un serveur MCP S'exercer · Niveau 3 · 45 minutes](https://meridal.fr/apprendre/un-serveur-mcp/)

Construction · étape 7 sur 12

↓ Plus concret

Rien avant : c'est un point de départ.

[Tout le dossier « Les agents »](https://meridal.fr/apprendre/dossier/agents/)

Les pages qui renvoient ici

- [Fiche mémo : les agents](https://meridal.fr/apprendre/fiche-memo-les-agents/)
- [Connecteurs et MCP](https://meridal.fr/apprendre/connecteurs-et-mcp/)
- [Liste de contrôle : un agent qui agit](https://meridal.fr/apprendre/avant-de-laisser-un-agent-agir/)
- [Niveaux d'autonomie d'un agent](https://meridal.fr/apprendre/niveaux-d-autonomie-d-un-agent/)
- [Un agent en entreprise de bout en bout](https://meridal.fr/apprendre/un-agent-en-entreprise/)
- [Un serveur MCP](https://meridal.fr/apprendre/un-serveur-mcp/)

**Limites et sources**

La demande est fabriquée ; le code qui appelle un modèle n'a pas été exécuté par Meridal, les appels payants n'étant pas ouverts.

Les noms de modèles et de bibliothèques sont ceux des documentations au 8 octobre 2026.

1. [OpenAI, outils du SDK d'agents](https://openai.github.io/openai-agents-python/tools/) ; [OpenAI, lancer un agent](https://openai.github.io/openai-agents-python/running_agents/) ; [Anthropic, traiter les appels d'outils](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) ; [Claude Agent SDK, outils](https://code.claude.com/docs/en/agent-sdk/custom-tools.md) ; [Google ADK, deux outils](https://adk.dev/tutorials/multi-tool-agent/).
2. Le relevé : `apprendre/recherches/2026-10-08-dev-avance.json`.

Écrit par Meridal, avec l'aide de l'IA ; chaque fait vérifié à sa source et relu avant publication. Texte et schémas sous licence [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.fr) : reprenez-les en citant Meridal. Le code sous licence MIT.

Service correspondant

Ne plus recopier d'un logiciel à l'autre

1 100 € à 3 400 €, puis sur devis

[Voir le service](https://meridal.fr/offre/#automatisation)

## Un premier échange de 30 minutes

En visio et sans frais : vous décrivez votre situation, nous vous indiquons ce qui est réalisable, avec quels outils et pour quel ordre de prix. Vous pouvez aussi nous écrire ; chaque message reçoit une réponse écrite.

[Réserver 30 minutes](https://meridal.fr/contact/#reserver) [Écrire un message](https://meridal.fr/contact/)
