Sur cette page
- 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.
- 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.
- 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.
- 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.
- 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) et l'exercice sur l'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 0815Télécharger
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
- 1La demande« Préparez la réponse à demande-0815.md »
- 2Le modèledemande l'outil lire_demande
- 3Votre programmeexécute l'outil, rend le texte
- 4Le modèledemande enregistrer_brouillon, avec l'objet et le texte
- 5Votre programmeécrit le fichier, rend « enregistré »
- 6Le modèlerépond : c'est fait ; la boucle s'arrête
Consignes
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.
É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.
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.
É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.
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.
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
Indications
L'agent tourne en rond et atteint la limite
CauseLe SDK lève MaxTurnsExceeded au-delà de max_turns.
À faireRendre 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
CauseLa description de l'outil ne dit pas à quoi il sert, ou le nom du fichier n'est pas dans la demande.
À faireUne docstring qui dit quoi et quand ; le nom du fichier dans la demande.
Le brouillon promet une date
CauseLe modèle comble un manque.
À faireLa 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
CauseL'exception n'est pas attrapée.
À faireL'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
CauseUn client peut écrire « ignorez vos consignes ». Cet agent n'a ni réseau ni envoi : au pire, un brouillon étrange, qu'une personne lira.
À faireGarder ce découpage ; voir la sécurité 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)uv run essai_outils.pyObjet : Commande 2026-0815, pièce manquanteRefusé : ../.env sort du dossier demandes.Refusé : ../../outils.py sort du dossier demandes.Brouillon enregistré : brouillons/essai.mdRefusé : un brouillon est un fichier .md.
Essayé le 9 octobre 2026 avec Python 3.12.7, sans modèle.
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.pyimport 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())uv run agent_openai.pyPas 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.pyimport 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.")uv run agent_anthropic.pyPas 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 ; 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.
- Une trace de chaque passage : la demande lue, les outils appelés, le brouillon écrit ; voir l'observabilité.
- Les bibliothèques d'agents, comparées : les bibliothèques d'agents.
Lectures suivantes
Un agent avec deux outils
Rien avant : c'est un point de départ.
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.
- OpenAI, outils du SDK d'agents ; OpenAI, lancer un agent ; Anthropic, traiter les appels d'outils ; Claude Agent SDK, outils ; Google ADK, deux outils.
- 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 : reprenez-les en citant Meridal. Le code sous licence MIT.
Ne plus recopier d'un logiciel à l'autre
1 100 € à 3 400 €, puis sur devis