Créer un tool CrewAI personnalisé : guide pas à pas
Créez un tool CrewAI personnalisé en Python : décorateur @tool, validation, gestion des timeouts et rate limits, debugging et bonnes pratiques.
Introduction
Un agent sans outil, c'est un navigateur sans connexion — du potentiel, mais aucune action possible. Les tool sont le pont entre vos agents et le monde réel : recherche web, appels API, lecture de fichiers, requêtes base de données.
Ce tutoriel couvre la création d'un tool personnalisé, son association à un agent, et la gestion des erreurs en production.
Résumé rapide
| Critère | Détail |
|---|---|
| Type | Outil (@tool decorator ou classe Tool) |
| Dépendance | crewai[tools] |
| Association | via le paramètre tools= de l'agent |
| Erreurs fréquentes | timeout, rate limit, schéma de retour invalide |
| Debug | verbose=True, dry_run, examen du résultat |
Qu'est-ce qu'un tool dans CrewAI ?
Un tool est une fonction Python annotée qu'un agent peut invoquer pendant son raisonnement. Il expose :
- Un nom — utilisé par l'agent pour décider quand appeler le tool.
- Une description — c'est elle que le modèle lit pour comprendre quand déclencher l'appel (tool calling).
- Un schéma d'entrée — paramètres attendus, typés et documentés.
- Une fonction sous-jacente — le code qui s'exécute réellement.
Deux interfaces de définition, toutes deux importées depuis crewai.tools :
# Méthode 1 : décorateur @tool (recommandée pour les cas simples)
from crewai.tools import tool
@tool("recherche_web")
def recherche_web(query: str) -> str:
"""Recherche des informations sur le web."""
# ... logique
return "..."
# Méthode 2 : sous-classe BaseTool (pour plus de contrôle et un schéma typé)
from crewai.tools import BaseTool
class RechercheWebTool(BaseTool):
name: str = "recherche_web"
description: str = "Recherche des informations sur le web."
def _run(self, query: str) -> str:
# ... logique
return "..."
recherche_tool = RechercheWebTool()
Attention aux imports. Le décorateur s'appelle
tool(minuscule) et la classeBaseTool; les deux vivent danscrewai.tools, pas danscrewai. Avec le décorateur, le nom du tool est passé en argument positionnel et la docstring sert de description si vous n'en fournissez pas d'autre.
Point critique : la description. Floue = agent qui n'appelle jamais son tool, ou qui l'appelle mal.
Créer un tool simple
import json
from crewai.tools import tool
@tool("recherche_web")
def recherche_web(query: str) -> str:
"""Recherche des informations récentes sur le web.
Accepte une requête en français ou en anglais.
Retourne un résumé des premiers résultats avec titre et URL.
"""
# Tool de recherche web simulé.
# En production : remplacez par SerpAPI, Tavily ou DuckDuckGo.
try:
if not query or len(query.strip()) < 3:
return json.dumps({"erreur": "Requête trop courte (min 3 caractères)."})
query = query.strip()
resultats = [
{"titre": f"Résultat pour '{query}' — article 1", "url": "https://exemple.com/1"},
{"titre": f"Résultat pour '{query}' — article 2", "url": "https://exemple.com/2"},
]
return json.dumps({"requete": query, "resultats": resultats}, ensure_ascii=False)
except Exception as e:
return json.dumps({"erreur": f"Échec de la recherche : {str(e)}"})
| Élément | Rôle |
|---|---|
@tool | Enregistre la fonction comme tool CrewAI |
| Argument du décorateur | Nom unique du tool pour le tool calling |
| Docstring | Description lue par le modèle — être précis |
| Validation entrée | Évite les appels vides ou malformed |
try/except | Retourne un message structuré même en cas d'erreur |
Associer un tool à un agent
Un tool n'a de valeur que relié à un agent. CrewAI utilise le paramètre tools= :
from crewai import Agent, Task, Crew
# 1. Définir l'agent avec son tool
chercheur = Agent(
role="Chercheur web",
goal="Trouver les informations les plus pertinentes sur le sujet demandé.",
backstory="""Tu es un chercheur rigoureux. Tu utilises toujours tes outils de recherche
pour obtenir des informations à jour avant de formuler tes conclusions.""",
tools=[recherche_web],
verbose=True,
)
# 2. Définir la task
tache_recherche = Task(
description="Recherche les dernières avancées sur les frameworks d'agents IA en 2026.",
agent=chercheur,
expected_output="Un résumé de 5 résultats avec titre, source et date.",
)
# 3. Orchestrer dans un crew
crew = Crew(
agents=[chercheur],
tasks=[tache_recherche],
process="sequential",
verbose=True,
)
resultat = crew.kickoff()
print(resultat)
L'agent décide seul quand invoquer recherche_web — en fonction de sa description et de son goal.
Exemple concret : agent de veille IA
Deux tools complémentaires : recherche web + stockage fichier.
from crewai import Agent, Task, Crew
from crewai.tools import tool
import json
@tool("recherche_web")
def recherche_web(query: str) -> str:
"""Recherche des informations sur le web."""
return json.dumps({"resultats": [{"titre": "IA Act adopté", "url": "https://example.com"}]})
@tool("sauvegarder_fichier")
def sauvegarder_fichier(nom_fichier: str, contenu: str) -> str:
"""Écrit du contenu dans un fichier texte local."""
try:
with open(nom_fichier, "w", encoding="utf-8") as f:
f.write(contenu)
return f"Fichier {nom_fichier} créé avec succès."
except Exception as e:
return f"Erreur d'écriture : {e}"
veilleur = Agent(
role="Veilleur IA",
goal="Surveiller les évolutions du secteur de l'IA et produire un rapport synthétique.",
backstory="Tu es un veilleur technologique. Tu effectues des recherches ciblées, compiles les résultats et les stockes.",
tools=[recherche_web, sauvegarder_fichier],
verbose=True,
)
tache = Task(
description="Fais une veille sur l'IA agents en 2026 et sauvegarde un rapport dans veille_ia.txt.",
agent=veilleur,
expected_output="Un fichier veille_ia.txt contenant 3 actualités majeures.",
)
crew = Crew(agents=[veilleur], tasks=[tache], process="sequential")
resultat = crew.kickoff()
L'agent invoque recherche_web pour rassembler les informations, puis sauvegarder_fichier pour les persister.
Valider les arguments avec un schéma Pydantic
Avec le décorateur @tool, CrewAI infère le schéma des arguments à partir des annotations de type. Mais pour des entrées complexes (bornes, formats, valeurs autorisées), il est plus sûr de déclarer explicitement un schéma Pydantic via BaseTool et son attribut args_schema. Le modèle reçoit alors la description de chaque champ, et toute valeur hors contrat est rejetée avant d'atteindre votre code.
from typing import Type
from pydantic import BaseModel, Field
from crewai.tools import BaseTool
class RechercheInput(BaseModel):
"""Schéma d'entrée du tool de recherche."""
query: str = Field(..., min_length=3, description="Termes de recherche (3 caractères min).")
max_resultats: int = Field(5, ge=1, le=20, description="Nombre de résultats (1 à 20).")
class RechercheWebTool(BaseTool):
name: str = "recherche_web"
description: str = (
"Recherche des informations récentes sur le web. "
"Retourne une liste de résultats avec titre, URL et extrait."
)
args_schema: Type[BaseModel] = RechercheInput
def _run(self, query: str, max_resultats: int = 5) -> str:
# Les arguments sont déjà validés par Pydantic : query >= 3 caractères,
# max_resultats borné entre 1 et 20. Inutile de re-vérifier ici.
resultats = [
{"titre": f"Résultat {i+1} pour '{query}'", "url": f"https://exemple.com/{i+1}"}
for i in range(max_resultats)
]
return json.dumps({"requete": query, "resultats": resultats}, ensure_ascii=False)
recherche_tool = RechercheWebTool()
Avantages concrets :
- Erreurs explicites : une requête trop courte lève une
ValidationErrorclaire, que l'agent peut interpréter et corriger à l'itération suivante. - Meilleur tool calling : les
descriptiondes champsFieldsont transmises au LLM, qui sait exactement quoi remplir. - Code plus propre : la validation vit dans le schéma, pas éparpillée dans
_run.
Tester un tool isolément
Avant de brancher un tool sur un agent — et de payer des appels LLM pour découvrir qu'il plante —, testez-le seul. Un tool reste une fonction Python ; appelez-la directement avec .run() (méthode exposée par CrewAI sur les tools) ou via la fonction sous-jacente.
# Test rapide d'un tool décoré avec @tool
resultat = recherche_web.run(query="frameworks agents IA 2026")
print(resultat)
# Test d'un tool BaseTool, y compris la validation du schéma
from pydantic import ValidationError
tool = RechercheWebTool()
print(tool.run(query="LangGraph", max_resultats=3)) # OK
try:
tool.run(query="ab") # query < 3 caractères
except ValidationError as e:
print("Validation refusée comme prévu :", e)
Industrialisez avec pytest pour couvrir les cas limites : entrée vide, caractères spéciaux, dépassement de borne, panne réseau (mockée). Un tool testé en isolation économise des heures de debug au niveau du crew, où les logs sont noyés dans le raisonnement de l'agent.
# test_recherche_web.py
import json
def test_recherche_retourne_json_valide():
sortie = recherche_web.run(query="test recherche")
data = json.loads(sortie) # ne doit pas lever
assert "resultats" in data
assert isinstance(data["resultats"], list)
Gérer erreurs, timeouts et rate limits
Trois causes principales d'échec : timeout, rate limit, schéma de retour invalide.
Timeout
import requests
from crewai.tools import tool
@tool("recherche_web")
def recherche_web(query: str) -> str:
"""Recherche des informations sur le web via une API externe."""
try:
response = requests.get(
"https://api.exemple.com/search",
params={"q": query},
timeout=10, # délai max en secondes
)
response.raise_for_status()
return response.text
except requests.Timeout:
return json.dumps({"erreur": "Délai dépassé (10s). Réessayez."})
except requests.RequestException as e:
return json.dumps({"erreur": f"Échec requête : {str(e)}"})
Toujours renvoyer un message d'erreur plutôt que de lever une exception non gérée : un tool qui raise interrompt brutalement le crew, alors qu'un retour structuré laisse l'agent décider de réessayer ou de changer de stratégie.
Rate limit
import time
from crewai.tools import tool
_appels_recent: list[float] = []
@tool("recherche_web_rate_limited")
def recherche_web_rate_limited(query: str) -> str:
"""Recherche web avec un throttling de 10 requêtes par minute."""
global _appels_recent
maintenant = time.time()
# On ne garde que les appels de la dernière minute
_appels_recent = [t for t in _appels_recent if maintenant - t < 60]
if len(_appels_recent) >= 10:
return json.dumps({"erreur": "Rate limit atteint (10 req/min). Patientez."})
_appels_recent.append(maintenant)
# ... logique de recherche
return json.dumps({"resultats": []})
Pour les API tierces qui renvoient un code 429, ajoutez un retry avec backoff exponentiel (par exemple via la librairie tenacity) : on attend 1s, puis 2s, puis 4s avant d'abandonner. Cela absorbe les pics transitoires sans surcharger le service distant.
Schema invalide
Retournez toujours un format cohérent. Si l'agent attend du JSON structuré et que votre tool renvoie du texte brut, le raisonnement déraille. Documentez le format de retour dans la description du tool.
Bonnes pratiques
1. Une responsabilité par tool. Un tool qui fait recherche + traitement + stockage fait mal une seule chose.
2. Descriptions précises et actionnables. Pas "Outil de recherche" — mais "Recherche des articles récents sur [sujet]. Retourne une liste de 5 résultats avec titre, URL et date."
3. Validation des entrées en premier. Vérifiez les paramètres avant toute logique coûteuse. Un tool qui échoue élégamment vaut mieux qu'un tool qui crash.
4. Naming : verbes pour les actions (recherche_web, lit_fichier) — noms pour les données (scrape_page, appelle_api).
5. Limitez le nombre de tools par agent. 2 à 5 tools maximum. Au-delà, l'agent disperse son attention.
6. Documentez dans le code ET dans la description. Le premier est lu par le modèle, le second par les développeurs.
Pièges courants
| Piège | Symptôme | Correctif |
|---|---|---|
| Mauvais import | ImportError: cannot import name 'Tool' | Importez tool ou BaseTool depuis crewai.tools, jamais depuis crewai. |
| Décorateur sans docstring | L'agent n'appelle jamais le tool | Ajoutez une docstring claire — elle sert de description au LLM. |
| Retour non sérialisable | Erreur de parsing dans le raisonnement | Renvoyez toujours une str (JSON via json.dumps), pas un objet brut. |
| Annotations de type manquantes | Le LLM ne sait pas quoi passer | Typez chaque argument (query: str) ou utilisez args_schema. |
| Exception non gérée | Le crew s'arrête net | Capturez les erreurs et renvoyez un message structuré. |
| Trop de tools sur un agent | L'agent hésite, boucle ou se trompe d'outil | Limitez à 2-5 tools, ou répartissez sur plusieurs agents. |
Un dernier réflexe : si un tool semble ignoré, lisez les logs verbose=True. CrewAI affiche le nom du tool envisagé et les arguments générés — c'est souvent là qu'on repère une description ambiguë ou un argument mal nommé.
Questions fréquentes
Comment créer un tool personnalisé ?
Importez tool depuis crewai.tools et décorez votre fonction avec @tool("mon_nom"). Donnez-lui une docstring précise (elle sert de description au LLM) et des arguments typés. Associez ensuite via tools=[mon_tool] sur l'agent. Pour un schéma d'arguments validé, sous-classez plutôt BaseTool.
Peut-on utiliser plusieurs tools avec un seul agent ?
Oui. Passez une liste au paramètre tools=. L'agent choisit lequel invoquer selon la tâche. Gardez 2-5 tools par agent.
Un tool n'est jamais appelé — comment debugger ?
Activez verbose=True, vérifiez la description (le modèle doit comprendre quand l'appeler), et testez le tool isolément avec des entrées variées.
Les tools supportent-ils les appels asynchrones ?
Oui. Définissez votre fonction en async def — CrewAI gère l'appel non-bloquant. Idéal pour les HTTP parallelisés.
Comment gérer les rate limits ? Implémentez un throttling simple (compteur par minute) avec message d'erreur structuré. Prévoyez un retry avec backoff exponentiel.
Aller plus loin
Les tools sont un pilier de l'architecture agentique. Consultez le guide complet de CrewAI pour comprendre comment ils s'intègrent dans une architecture multi-agents. Pour installer votre premier environnement, le guide d'installation vous donne un setup prêt en 10 minutes. Et pour une vision large de l'outillage agentique, notre article sur les agent tools fait le tour des approches et bonnes pratiques.
Restez informé sur les agents IA
Nouveaux tutoriels, comparatifs et guides pratiques directement dans votre boîte mail.