Vous avez créé un chatbot qui répond aux questions. Maintenant, vous avez besoin qu’il fasse quelque chose : récupérer des données, appeler une API, mettre à jour une base de données. La différence entre un chatbot et un agent réside dans une seule contrainte : les agents agissent en fonction de ce qu’ils apprennent.
La plupart des tentatives échouent car les développeurs traitent l’appel d’outils comme un ajout, et non comme le cœur du système. Ils appellent un LLM, attendent une réponse, puis intègrent les outils après coup. Les agents de production nécessitent une architecture différente, qui traite le LLM comme un moteur de décision, et non comme un générateur de texte.
Appel d’Outils : Le Contrat, Pas la Fonctionnalité
L’appel d’outils ne consiste pas à donner à un LLM l’accès à des fonctions. Il s’agit de définir un contrat que le LLM doit respecter.
Lorsque vous définissez un outil, vous ne donnez pas au modèle une boîte noire. Vous spécifiez :
- Ce que fait l’outil (description)
- Quels paramètres il requiert (schéma)
- Quel format il renvoie (spécification de sortie)
La plupart des appels d’outils échouent car les descriptions sont vagues. « Récupérer les données utilisateur » vous fait perdre immédiatement : récupérer quelles données ? À quoi ressemble la signature de la fonction ? Que se passe-t-il si l’utilisateur n’existe pas ?
Voici à quoi ressemble une mauvaise définition d’outil :
{
"name": "get_user",
"description": "Get user information",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string"
}
}
}
}
Le LLM ne sait pas ce qui se passe lorsque user_id est invalide. Il ne sait pas si user_id doit être un UUID ou un entier. Il ne sait pas quels champs contient la réponse.
Voici la version améliorée :
{
"name": "get_user_profile",
"description": "Retrieve a user's profile by ID. Returns basic account info including name, email, creation date, and account status. Returns null if user not found.",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "UUID of the user. Format: 550e8400-e29b-41d4-a716-446655440000"
}
},
"required": ["user_id"]
},
"returns": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"email": {"type": "string"},
"status": {"type": "string", "enum": ["active", "suspended", "deleted"]},
"created_at": {"type": "string"}
}
}
}
Claude Sonnet 4 (sortie janvier 2025) a amélioré la cohérence de l’appel d’outils de 34 % par rapport aux versions précédentes lorsque les schémas sont précis. Les définitions vagues le confondent toujours – ce n’est pas une limitation du modèle, c’est un défaut de conception.
La Boucle : Rendre les Décisions Séquentielles
Une boucle d’agent est simple dans sa structure mais défaillante dans presque toutes les premières implémentations.
Le flux de base : LLM décide → l’outil s’exécute → le résultat revient → le LLM décide à nouveau → répéter jusqu’à la fin.
Voici un exemple Python fonctionnel utilisant Claude :
import anthropic
import json
client = anthropic.Anthropic()
tools = [
{
"name": "fetch_order",
"description": "Get order details by order ID",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Unique order identifier"
}
},
"required": ["order_id"]
}
},
{
"name": "update_order_status",
"description": "Update an order's status",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"status": {
"type": "string",
"enum": ["pending", "shipped", "delivered"]
}
},
"required": ["order_id", "status"]
}
}
]
messages = [{"role": "user", "content": "Check order ABC123 and mark it as shipped"}]
while True:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
if response.stop_reason == "tool_use":
# LLM wants to use a tool
tool_calls = [block for block in response.content if block.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool_call in tool_calls:
# Execute tool (stubbed here)
if tool_call.name == "fetch_order":
result = {"id": "ABC123", "status": "pending", "total": 99.99}
elif tool_call.name == "update_order_status":
result = {"success": True, "new_status": "shipped"}
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": json.dumps(result)
})
messages.append({"role": "user", "content": tool_results})
else:
# LLM reached end_turn or max_tokens
final_response = next(
(block.text for block in response.content if hasattr(block, "text")),
None
)
print(final_response)
break
L’erreur critique que font la plupart des développeurs : ils traitent les résultats des outils comme du texte non structuré. Si un outil renvoie du JSON, analysez-le et rendez la structure explicite au LLM. Ne le forcez pas à analyser des chaînes de caractères désordonnées.
Mémoire : Ce dont les Agents Ont Vraiment Besoin de se Souvenir
La mémoire n’est pas l’historique de la conversation. C’est la première chose à désapprendre.
Un agent a besoin de trois types de mémoire :
- Mémoire de session : Ce qui s’est passé dans cette conversation — objectifs de l’utilisateur, contexte des tours précédents. C’est à court terme et spécifique à la conversation.
- Mémoire de connaissance : Faits sur l’utilisateur, le domaine ou l’état du système qui persistent d’une conversation à l’autre. C’est à long terme et partagé.
- Mémoire d’exécution : Ce que l’agent a déjà essayé, ce qui a échoué, ce qui a réussi. Cela évite les boucles et les erreurs répétées.
La plupart des systèmes confondent les trois dans un historique de messages. Cela nuit aux performances.
La mémoire de session doit vivre dans le tableau des messages — mais résumée, pas brute. Après 20 tours, compressez le contexte précédent en un seul message système au lieu de garder les 20 tours en contexte.
La mémoire de connaissance doit être séparée — une base de données vectorielle (Pinecone, Weaviate) ou un magasin clé-valeur structuré. Lorsque vous avez besoin du contexte utilisateur, récupérez-le explicitement avec un appel d’outil, ne le bourrez pas dans l’invite initiale.
La mémoire d’exécution doit être un journal explicite. Avant que l’agent n’essaie un outil, vérifiez s’il a déjà tenté cet outil dans cette session. S’il a échoué la dernière fois, transmettez cet échec au LLM comme contexte.
Exemple de structure :
{
"session_id": "conv_12345",
"user_goal": "Update billing address and confirm new payment method",
"session_context": "User has active subscription. Previously tried to update payment in December but process failed.",
"execution_log": [
{"tool": "fetch_user_profile", "status": "success", "timestamp": "2025-01-15T10:22:00Z"},
{"tool": "validate_address", "status": "failed", "error": "Postal code invalid", "timestamp": "2025-01-15T10:22:15Z"}
],
"knowledge_refs": ["user_payment_history", "subscription_terms"],
"messages": [
{"role": "user", "content": "Update my address..."},
{"role": "assistant", "content": "I'll help with that..."}
]
}
Faites-le Aujourd’hui
Choisissez un outil que votre agent doit appeler. Rédigez le schéma avec une description de 3 phrases, listez chaque paramètre avec son format et ses contraintes, et définissez la forme exacte de la réponse. Testez-le manuellement — donnez le schéma à Claude ou GPT-4o et demandez-lui d’appeler l’outil. S’il l’appelle mal, votre schéma est incomplet.