Agent Tool Builder
Les outils sont la manière dont les agents IA interagissent avec le monde. Un outil bien conçu fait la différence entre un agent qui fonctionne et un qui hallucine, échoue silencieusement ou coûte 10 fois plus de tokens que nécessaire.
Cette compétence couvre la conception d'outils, du schéma à la gestion des erreurs. Meilleures pratiques JSON Schema, rédaction de descriptions qui aident réellement le LLM, validation, et le standard MCP émergent qui devient la lingua franca pour les outils IA.
Insight clé : Les descriptions d'outils sont plus importantes que les implémentations d'outils. Le LLM ne voit jamais votre code - il ne voit que le schéma et la description.
Principes
- La qualité de la description > la qualité de l'implémentation pour la précision du LLM
- Visez moins de 20 outils - plus crée de la confusion
- Chaque outil a besoin d'une gestion d'erreur explicite - les échecs silencieux empoisonnent les agents
- Retournez des chaînes de caractères, pas des objets - les LLM traitent le texte
- Validez avant l'exécution - rejetez, corrigez ou escaladez, ne jamais échouer silencieusement
- Testez les outils avec le LLM, pas seulement avec des tests unitaires
Capacités
- agent-tools
- function-calling
- tool-schema-design
- mcp-tools
- tool-validation
- tool-error-handling
Périmètre
- multi-agent-coordination → multi-agent-orchestration
- agent-memory → agent-memory-systems
- api-design → api-designer
- llm-prompting → prompt-engineering
Outillage
Standards
- JSON Schema - Quand : toutes les définitions d'outils. Remarque : le format universel pour les schémas d'outils
- MCP (Model Context Protocol) - Quand : construction d'outils réutilisables et multiplateformes. Remarque : le standard ouvert d'Anthropic, largement adopté
Frameworks
- SDK Anthropic - Quand : agents basés sur Claude. Remarque : le runner d'outils bêta gère la plupart de la complexité
- OpenAI Functions - Quand : agents basés sur OpenAI. Remarque : utilisez le mode strict pour garantir la conformité du schéma
- SDK Vercel AI - Quand : gestion d'outils multi-fournisseurs. Remarque : abstrait les différences entre les fournisseurs
- LangChain Tools - Quand : agents basés sur LangChain. Remarque : convertit les outils MCP au format LangChain
Modèles
Conception de schéma d'outil
Créer des schémas JSON clairs et sans ambiguïté pour les outils
Quand l'utiliser : définition de tout nouvel outil pour un agent
MEILLEURES PRATIQUES DE SCHÉMA D'OUTIL :
1. Descriptions détaillées (le plus important)
""" MAUVAIS - Trop vague : { "name": "get_stock_price", "description": "Obtient le prix de l'action", "input_schema": { "type": "object", "properties": { "ticker": {"type": "string"} } } }
BON - Complet : { "name": "get_stock_price", "description": "Récupère le prix actuel de l'action pour un symbole boursier donné. Le symbole boursier doit être un symbole valide pour une société cotée en bourse sur une grande bourse américaine comme NYSE ou NASDAQ. Retourne le dernier prix de transaction en USD. À utiliser lorsque l'utilisateur demande des prix d'actions actuels ou récents. Ne fournit PAS de données historiques, d'informations sur la société ou de prédictions.", "input_schema": { "type": "object", "properties": { "ticker": { "type": "string", "description": "Le symbole boursier, par ex. AAPL pour Apple Inc." } }, "required": ["ticker"] } } """
2. Descriptions des paramètres
""" Chaque paramètre a besoin :
- De quoi il s'agit
- Du format attendu
- D'un exemple de valeur
- Des cas limites/limitations
{ "location": { "type": "string", "description": "Ville et état/pays. Format : 'Ville, État' pour les États-Unis (par ex., 'San Francisco, CA') ou 'Ville, Pays' pour l'international (par ex., 'Tokyo, Japon'). N'utilisez pas de codes postaux ou de coordonnées." }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Unité de température. Par défaut, utilise la locale de l'utilisateur si non spécifié. Utilisez 'fahrenheit' pour les utilisateurs américains, 'celsius' pour les autres." } } """
3. Utilisez des énumérations quand c'est possible
""" Les énumérations contraignent le LLM à des valeurs valides :
"priority": { "type": "string", "enum": ["low", "medium", "high", "critical"], "description": "Niveau de priorité de la tâche" }
"action": { "type": "string", "enum": ["create", "read", "update", "delete"], "description": "L'opération CRUD à effectuer" } """
4. Requis vs Optionnel
""" Soyez explicite sur ce qui est requis :
{ "type": "object", "properties": { "query": {...}, // Requis "limit": {...}, // Optionnel avec valeur par défaut "offset": {...} // Optionnel }, "required": ["query"], "additionalProperties": false // Mode strict } """
Outil avec exemples d'entrée
Utiliser des exemples pour guider l'utilisation des outils par le LLM
Quand l'utiliser : outils complexes avec des objets imbriqués ou des entrées sensibles au format
EXEMPLES D'UTILISATION D'OUTIL (Fonctionnalité bêta d'Anthropic) :
""" Les exemples montrent à Claude des modèles concrets que les schémas ne peuvent pas exprimer. Améliore la précision de 72 % à 90 % pour les opérations complexes. """
{ "name": "create_calendar_event", "description": "Crée un événement de calendrier avec des participants et des rappels optionnels", "input_schema": { "type": "object", "properties": { "title": {"type": "string", "description": "Titre de l'événement"}, "start_time": { "type": "string", "description": "Date-heure ISO 8601, par ex. 2024-03-15T14:00:00Z" }, "duration_minutes": {"type": "integer", "description": "Durée de l'événement"}, "attendees": { "type": "array", "items": {"type": "string"}, "description": "Adresses e-mail des participants" } }, "required": ["title", "start_time", "duration_minutes"] }, "input_examples": [ { "title": "Réunion d'équipe", "start_time": "2024-03-15T09:00:00Z", "duration_minutes": 30, "attendees": ["[email protected]", "[email protected]"] }, { "title": "Brève discussion", "start_time": "2024-03-15T14:00:00Z", "duration_minutes": 15 }, { "title": "Revue de projet", "start_time": "2024-03-15T16:00:00-05:00", "duration_minutes": 60, "attendees": ["[email protected]"] } ] }
PRINCIPES DE CONCEPTION DES EXEMPLES :
- Utilisez des données réalistes, pas des espaces réservés
- Montrez des motifs de spécification minimaux, partiels et complets
- Restez concis : 1 à 5 exemples par outil
- Concentrez-vous sur les cas ambigus
Gestion des erreurs d'outil
Retourner des erreurs qui aident le LLM à se rétablir
Quand l'utiliser : tout outil pouvant échouer
MEILLEURES PRATIQUES DE GESTION DES ERREURS :
Retournez des erreurs informatives
""" MAUVAIS : {"error": "Échec"} {"error": true}
BON : { "error": true, "error_type": "not_found", "message": "L'emplacement 'Atlantis' est introuvable dans la base de données météo. Veuillez fournir un nom de ville réel comme 'San Francisco, CA'.", "suggestions": ["San Francisco, CA", "Los Angeles, CA"] } """
Résultat d'outil Anthropic avec erreur
""" { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Erreur : L'emplacement 'Atlantis' est introuvable dans la base de données météo. Veuillez fournir un nom de ville réel comme 'San Francisco, CA'.", "is_error": true } """
Catégories d'erreurs à gérer
"""
-
Erreurs de validation d'entrée
- Paramètres requis manquants
- Format invalide
- Valeurs hors plage
-
Erreurs de service externe
- API indisponible
- Limite de débit atteinte
- Délai d'attente dépassé
-
Erreurs de logique métier
- Ressource introuvable
- Permission refusée
- Conflit/doublon
-
Erreurs internes
- Exceptions inattendues
- Corruption de données """
Modèle d'implémentation
""" from dataclasses import dataclass from typing import Union
@dataclass class ToolResult: success: bool content: str error_type: str = None suggestions: list[str] = None
def to_response(self) -> dict:
if self.success:
return {"content": self.content}
return {
"content": f"Erreur ({self.error_type}) : {self.content}",
"is_error": True
}
def get_weather(location: str) -> ToolResult: # Valider l'entrée if not location or len(location) < 2: return ToolResult( success=False, content="L'emplacement doit comporter au moins 2 caractères", error_type="validation_error" )
try:
data = weather_api.fetch(location)
return ToolResult(
success=True,
content=f"Température : {data.temp}°F, Conditions : {data.conditions}"
)
except LocationNotFound:
return ToolResult(
success=False,
content=f"Emplacement '{location}' introuvable",
error_type="not_found",
suggestions=weather_api.suggest_locations(location)
)
except RateLimitError:
return ToolResult(
success=False,
content="Limite de débit du service météo dépassée. Réessayez dans 60 secondes.",
error_type="rate_limit"
)
except Exception as e:
return ToolResult(
success=False,
content=f"Erreur inattendue : {str(e)}",
error_type="internal_error"
)
"""
Modèle d'outil MCP
Construire des outils en utilisant le Model Context Protocol
Quand l'utiliser : créer des outils réutilisables et multiplateformes
IMPLÉMENTATION D'OUTIL MCP :
""" MCP (Model Context Protocol) est le standard ouvert d'Anthropic pour connecter des agents IA à des systèmes externes. Construisez une fois, utilisez partout. """
Serveur MCP de base (TypeScript)
""" import { Server } from "@modelcontextprotocol/sdk/server"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
const server = new Server({ name: "weather-server", version: "1.0.0" });
// Définir les outils server.setRequestHandler("tools/list", async () => ({ tools: [ { name: "get_weather", description: "Obtenir la météo actuelle pour un emplacement. Retourne la température, les conditions et l'humidité. Utilisez pour les demandes météo concernant des villes spécifiques.", inputSchema: { type: "object", properties: { location: { type: "string", description: "Ville et état, par ex. 'San Francisco, CA'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"], default: "fahrenheit" } }, required: ["location"] } } ] }));
// Gérer les appels d'outils server.setRequestHandler("tools/call", async (request) => { const { name, arguments: args } = request.params;
if (name === "get_weather") {
try {
const weather = await fetchWeather(args.location, args.unit);
return {
content: [
{
type: "text",
text: JSON.stringify(weather)
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: Erreur : ${error.message}
}
],
isError: true
};
}
}
throw new Error(Outil inconnu : ${name});
});
// Démarrer le serveur const transport = new StdioServerTransport(); await server.connect(transport); """
Avantages de MCP
"""
- Compatibilité universelle entre les fournisseurs de LLM
- Bibliothèques d'outils réutilisables
- Prise en charge du streaming et du transport SSE
- Observabilité intégrée
- Contrôles d'accès aux outils """
Modèle de runner d'outil
Utiliser les runners d'outils SDK pour une gestion automatique
Quand l'utiliser : construire des boucles d'outils sans gestion manuelle
RUNNER D'OUTIL (SDK Anthropic Bêta) :
""" Le runner d'outils gère automatiquement la boucle d'appels d'outils :
- Exécute les outils lorsque Claude les appelle
- Gère l'état de la conversation
- Gère les tentatives d'erreur
- Fournit un support de streaming """
Exemple Python
""" import anthropic from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool def get_weather(location: str, unit: str = "fahrenheit") -> str: '''Obtenir la météo actuelle dans un lieu donné.
Args:
location: La ville et l'état, par ex. San Francisco, CA
unit: Unité de température, soit 'celsius' soit 'fahrenheit'
'''
# Implémentation
return json.dumps({"temperature": "72°F", "conditions": "Ensoleillé"})
@beta_tool def search_web(query: str) -> str: '''Rechercher des informations sur le web.
Args:
query: La requête de recherche
'''
# Implémentation
return json.dumps({"results": [...]})
Le runner d'outils gère la boucle
runner = client.beta.messages.tool_runner( model="claude-sonnet-4-5", max_tokens=1024, tools=[get_weather, search_web], messages=[ {"role": "user", "content": "Quel temps fait-il à Paris ?"} ] )
Traiter chaque message
for message in runner: print(message.content[0].text)
Ou obtenir simplement le résultat final
final = runner.until_done() """
TypeScript avec Zod
""" import { Anthropic } from '@anthropic-ai/sdk'; import { betaZodTool } from '@anthropic-ai/sdk/helpers/beta/zod'; import { z } from 'zod';
const anthropic = new Anthropic();
const getWeatherTool = betaZodTool({ name: 'get_weather', description: 'Obtenir la météo actuelle dans un lieu donné', inputSchema: z.object({ location: z.string().describe('Ville et état, par ex. San Francisco, CA'), unit: z.enum(['celsius', 'fahrenheit']).default('fahrenheit') }), run: async (input) => { // Entrée typée sécurisée ! return JSON.stringify({temperature: '72°F'}); } });
const runner = anthropic.beta.messages.toolRunner({ model: 'claude-sonnet-4-5', max_tokens: 1024, tools: [getWeatherTool], messages: [{ role: 'user', content: "Quel temps fait-il à Paris ?" }] });
for await (const message of runner) { console.log(message.content[0].text); } """
Exécution parallèle d'outils
Exécuter plusieurs outils simultanément
Quand l'utiliser : appels d'outils indépendants pouvant être exécutés en parallèle
EXÉCUTION PARALLÈLE D'OUTILS :
""" Par défaut, Claude peut appeler plusieurs outils dans une seule réponse. Cela réduit considérablement la latence pour les opérations indépendantes. """
Gestion des résultats parallèles
"""
Claude retourne plusieurs blocs tool_use :
response.content = [ {"type": "text", "text": "Je vais vérifier les deux endroits..."}, {"type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {"location": "San Francisco, CA"}}, {"type": "tool_use", "id": "toolu_02", "name": "get_weather", "input": {"location": "New York, NY"}}, {"type": "tool_use", "id": "toolu_03", "name": "get_time", "input": {"timezone": "America/Los_Angeles"}}, {"type": "tool_use", "id": "toolu_04", "name": "get_time", "input": {"timezone": "America/New_York"}} ]
Idéal pour :
- Vérifications simultanées de météo
- Récupération de données indépendantes
- Opérations sans ordre spécifique
"""
Considérations sur le parallélisme
"""
-
Additif vs multiplicatif : les coûts des tokens augmentent avec les résultats des outils. Chaque résultat parallèle ajoute des tokens à la conversation.
-
Dépendances : vérifiez que les outils n'ont pas de dépendances cachées entre eux. L'exécution parallèle suppose l'indépendance.
-
Taux de succès : si un outil échoue, le message d'erreur est inclus dans la conversation. Claude peut s'adapter et réessayer si l'erreur est informative.
-
Limites : Claude a un plafond d'appels d'outils simultanés (actuellement 5-10 selon le modèle). Au-delà, les appels sont mis en file d'attente et exécutés séquentiellement. """
Modèle des limites d'outils
Gérer les limites de débit et les quotas d'utilisation
Quand l'utiliser : outils interagissant avec des API ayant des limites de débit, ou lorsque vous devez contrôler les coûts
LIMITES D'OUTILS :
""" Les outils avec des limites d'API nécessitent une gestion prudente pour éviter de gaspiller des appels. """
Modèle de limitation
""" import time from datetime import datetime, timedelta from collections import deque
class RateLimiter: def init(self, max_calls: int, window_seconds: int): self.max_calls = max_calls self.window = timedelta(seconds=window_seconds) self.calls: deque = deque()
def can_call(self) -> bool:
now = datetime.now()
# Nettoyer les appels expirés
while self.calls and self.calls[0] < now - self.window:
self.calls.popleft()
return len(self.calls) < self.max_calls
def record_call(self):
self.calls.append(datetime.now())
def rate_limited_tool(name: str, rate_limiter: RateLimiter): def decorator(func): def wrapper(*args, **kwargs): if not rate_limiter.can_call(): retry_after = int((rate_limiter.calls[0] + rate_limiter.window - datetime.now()).total_seconds()) return ToolResult( success=False, content=f"Limite de taux atteinte pour {name}. " f"Réessayez dans {retry_after} secondes.", error_type="rate_limit", suggestions=[f"Réessayez dans {retry_after} secondes"] ) rate_limiter.record_call() return func(*args, **kwargs) return wrapper return decorator
Utilisation
weather_limiter = RateLimiter(max_calls=10, window_seconds=60)
@rate_limited_tool("get_weather", weather_limiter) def get_weather(location: str) -> ToolResult: # Implémentation pass """
Modèle de limitation des coûts
""" class CostTracker: def init(self, budget: float): self.budget = budget self.total_cost = 0.0
def check_cost(self, estimated_cost: float) -> bool:
return self.total_cost + estimated_cost <= self.budget
def record_cost(self, cost: float):
self.total_cost += cost
def cost_aware_tool(func): def wrapper(*args, **kwargs): cost = estimate_tool_cost(func.name, args, kwargs) if not cost_tracker.check_cost(cost): return ToolResult( success=False, content=f"Coût estimé ${cost:.4f} dépasserait le budget " f"restant de ${cost_tracker.budget - cost_tracker.total_cost:.4f}", error_type="budget_exceeded" ) result = func(*args, **kwargs) cost_tracker.record_cost(cost) return result return wrapper """
Modèle de journalisation des outils
Journaliser les appels d'outils pour le débogage et la surveillance
Quand l'utiliser : développement et production pour suivre le comportement des outils
JOURNALISATION DES OUTILS :
""" La journalisation est essentielle pour le débogage des agents. Sans elle, vous ne pouvez pas dire ce que l'agent a réellement fait. """
Modèle de journalisation simple
""" import logging import json from functools import wraps
logger = logging.getLogger("agent.tools")
def logged_tool(func): @wraps(func) def wrapper(*args, **kwargs): tool_name = func.name logger.info(f"Appel d'outil : {tool_name}", extra={ "arguments": kwargs, "timestamp": datetime.now().isoformat() }) try: result = func(*args, **kwargs) logger.info(f"Résultat de l'outil : {tool_name}", extra={ "result": result.content if hasattr(result, 'content') else result, "success": result.success if hasattr(result, 'success') else True }) return result except Exception as e: logger.error(f"Erreur d'outil : {tool_name}", extra={"error": str(e)}) raise return wrapper
@logged_tool def get_weather(location: str) -> ToolResult: # Implémentation pass """
Modèle de traçage structuré
""" from dataclasses import dataclass, field from datetime import datetime import uuid
@dataclass class ToolTrace: tool_name: str input: dict output: str success: bool duration_ms: float timestamp: datetime = field(default_factory=datetime.now) trace_id: str = field(default_factory=lambda: str(uuid.uuid4())) error_type: str = None
class ToolTracer: def init(self): self.traces: list[ToolTrace] = []
def trace(self,
tool_name: str,
input: dict,
output: str,
success: bool,
duration_ms: float,
error_type: str = None):
trace = ToolTrace(
tool_name=tool_name,
input=input,
output=output,
success=success,
duration_ms=duration_ms,
error_type=error_type
)
self.traces.append(trace)
return trace
def get_failed_traces(self):
return [t for t in self.traces if not t.success]
def get_summary(self):
total = len(self.traces)
failed = len(self.get_failed_traces())
return {
"total_calls": total,
"failed_calls": failed,
"success_rate": (total - failed) / total if total > 0 else 0,
"avg_duration_ms": sum(t.duration_ms for t in self.traces) / total if total > 0 else 0
}
"""
Résumé
- Les descriptions d'outils sont plus importantes que l'implémentation
- Validez toujours les entrées avant d'appeler l'implémentation
- Utilisez MCP pour les outils multiplateformes
- Gérez les erreurs de manière à ce que le LLM puisse les comprendre et s'en remettre
- Testez les outils avec des LLM réels, pas seulement avec des tests unitaires
- Surveillez l'utilisation des outils et les coûts en production