4 min de lecture
Ce dont un agent a vraiment besoin d'un outil
Après un an à construire des serveurs MCP pour de vrais flux de travail, la surface de l'outil compte plus que le modèle derrière. Sept règles que j'applique désormais avant qu'un outil parte en production.
Un outil est une API avec le pire client que vous aurez jamais. Le modèle ne lit pas votre documentation. Il ne réessaie pas avec une requête plus maligne après une stack trace. Il reçoit une description, un schéma, et ce que votre outil renvoie, et à partir de là il doit décider s'il vous rappelle.
J'ai construit des serveurs MCP pour la réconciliation de commandes, des pipelines de contenu et quelques systèmes internes que je ne peux pas nommer. Le modèle n'a cessé de s'améliorer en dessous. Les échecs qui restaient étaient les miens, et ils étaient presque toujours dans la surface de l'outil. Voici les règles qui les ont corrigés.
#1. La description est le contrat
La description est la seule documentation que le modèle lira, elle doit donc dire trois choses : quand utiliser l'outil, quand ne pas l'utiliser, et ce qui revient. Un nom ne suffit pas.
{
"name": "orders_search",
"description": "Find orders by customer email, order number or date range. Use this before orders_update to get the order id. Returns at most 50 orders, newest first, with a cursor for more. Does not return line items — call orders_get for one order's details.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Email, order number, or free text." },
"since": { "type": "string", "format": "date" },
"cursor": { "type": "string" }
},
"required": ["query"]
}
}La phrase sur orders_get empêche l'échec le plus fréquent que j'ai observé : le modèle qui relance la recherche avec une autre formulation, en espérant voir apparaître les lignes de commande.
#2. Des entrées petites, des schémas stricts
Chaque champ optionnel est une décision que le modèle doit prendre. Les enums battent le texte libre. Les dates dans un seul format. Un limit avec un plafond dur côté serveur, quoi que dise l'entrée. Je génère les schémas depuis Zod pour que la validation et la description ne puissent pas diverger.
const SearchInput = z.object({
query: z.string().min(2).describe('Email, order number, or free text.'),
status: z.enum(['open', 'paid', 'shipped', 'cancelled']).optional(),
since: z.string().date().optional(),
cursor: z.string().optional(),
})#3. Renvoyer des erreurs sur lesquelles le modèle peut agir
Une exception est une impasse. Une erreur structurée avec un indice est une prochaine étape.
return {
ok: false,
error: 'ORDER_NOT_FOUND',
hint: 'No order matches "KF-1042". Order numbers look like KF-<digits>; try orders_search with the customer email instead.',
}Le champ hint a changé le comportement plus que n'importe quel prompt que j'ai écrit. Il transforme une boucle de nouvelles tentatives en un seul appel corrigé.
#4. Tout ce qui écrit prend une clé d'idempotence
Les agents réessaient. Les réseaux flanchent. Un outil orders_refund sans clé d'idempotence finira, un jour, par rembourser deux fois. Exigez la clé, stockez le résultat sous cette clé, et renvoyez le résultat stocké quand la même clé revient.
#5. Paginer avec des curseurs, et plafonner
Les offsets cassent quand les données bougent sous l'agent, et elles bougent toujours. Renvoyez un cursor, acceptez un cursor, et ne renvoyez jamais plus que le plafond même si on vous le demande. La description doit mentionner le plafond pour que le modèle planifie en conséquence.
#6. Une capacité par outil, et les lectures n'écrivent jamais
orders_search ne modifie jamais rien. orders_update ne cherche jamais. Les outils destructifs sont séparés, nommés comme tels, et dans mes serveurs ils exigent un argument de confirmation explicite que le modèle doit fournir. Découper ainsi rend aussi les permissions lisibles : un déploiement en lecture seule n'enregistre tout simplement pas les outils d'écriture.
#7. L'évaluer comme du logiciel
Un outil qui marche en démo et échoue à la dixième conversation n'est pas terminé. Je garde des transcriptions de référence, je les rejoue à chaque changement, et je note les sorties. Assay est le petit évaluateur que je maintiens pour cela : il note la sortie du modèle selon un ensemble de métriques de qualité, pour qu'une régression apparaisse comme un nombre, pas comme une impression.
#Les symptômes, si vous en déboguez un en ce moment
| Règle enfreinte | Ce que vous voyez dans la transcription |
|---|---|
| Description vague | Le modèle appelle le mauvais outil, ou le bon pour la mauvaise raison |
| Schéma lâche | Des dates dans trois formats, des limites à 10 000 |
| Erreurs levées | Le même appel répété avec de petites reformulations |
| Pas de clé d'idempotence | Des écritures en double après un timeout |
| Pagination par offset | Des enregistrements sautés ou répétés en cours d'exécution |
| Lecture et écriture mélangées | Une « recherche » qui a modifié quelque chose |
| Pas d'évaluation | Ça marchait hier |
Rien de tout cela n'est exotique. C'est de la conception d'API, appliquée à un client qui ne sait pas lire entre les lignes. Le modèle n'est pas la partie difficile ; c'est la surface que vous lui tendez.
Ça vous a parlé ?
Conversation
Vous construisez quelque chose de ce genre ?
Dites-moi sur quoi vous travaillez. Je réponds sous un jour.