Aller au contenu
Toutes les notes

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.

json
{
  "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.

typescript
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.

typescript
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 enfreinteCe que vous voyez dans la transcription
Description vagueLe modèle appelle le mauvais outil, ou le bon pour la mauvaise raison
Schéma lâcheDes dates dans trois formats, des limites à 10 000
Erreurs levéesLe même appel répété avec de petites reformulations
Pas de clé d'idempotenceDes écritures en double après un timeout
Pagination par offsetDes enregistrements sautés ou répétés en cours d'exécution
Lecture et écriture mélangéesUne « 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.

M'écrire