Trop d'outils : découverte progressive ou exécution de code
Ce que coûte un catalogue d'outils chargé en entier à chaque tour, et comment choisir entre trois sorties : élaguer, faire découvrir les outils à la demande, ou laisser l'agent écrire du code qui les appelle.
Par Elias Varen7 min de lecture
Soit un agent branché sur soixante outils. Chaque définition (nom, description, schéma des paramètres, deux exemples) pèse en moyenne 400 tokens. Avant que l'utilisateur ait tapé un mot, 24 000 tokens sont partis, et ils repartent à chaque tour de la boucle. Sur une tâche de trente tours, on a fait lire 720 000 tokens de mode d'emploi à un modèle qui s'est servi de quatre outils.
Le cache de prompt atténue la facture sans régler le problème. Un préfixe mis en cache coûte moins cher à relire, mais il occupe toujours la fenêtre, et surtout il occupe l'attention, puisque le modèle doit choisir entre soixante candidats dont une dizaine se ressemblent. Les choses cassent à ce niveau, bien avant la limite de contexte.
L'encombrement abîme le choix avant d'abîmer le budget
Un catalogue d'outils grossit par accrétion. On branche un serveur MCP pour le suivi de tickets, un autre pour la base, un troisième pour la messagerie, et chacun arrive avec quinze outils dont on voulait trois. Personne n'a relu l'ensemble comme un tout.
Les dégradations apparaissent dans un ordre assez constant.
D'abord, la confusion entre voisins. search_documents, find_files, query_knowledge_base et list_pages viennent de quatre serveurs différents et disent presque la même chose. Le modèle en prend un, plausiblement, et pas le bon. L'appel réussit, le résultat est vide, et l'agent conclut que l'information n'existe pas.
Ensuite, la dilution des consignes. Vos instructions système font 1 500 tokens et sont suivies de 24 000 tokens de schémas JSON. La règle « ne modifie jamais un ticket sans confirmation » pèse maintenant six pour cent de ce que le modèle a lu avant de commencer.
Enfin, et c'est le plus coûteux, les résultats, qui n'ont rien à voir avec les définitions. Chaque appel renvoie sa réponse dans le contexte. Un agent qui lit 200 lignes d'un tableur pour en recopier trois dans un autre outil a fait transiter les 200 lignes deux fois par la fenêtre, une fois en sortie d'outil et une fois quand il les réécrit en paramètres.
Avant toute architecture, il existe une sortie qui ne coûte rien, élaguer. On retire les outils que l'agent n'a pas appelés depuis un mois, on regroupe les voisins, on n'expose qu'un sous-ensemble par type de tâche. Sous une vingtaine d'outils bien décrits, je ne fais rien d'autre. Les deux techniques qui suivent servent quand l'élagage ne suffit plus.
Faire chercher les outils au modèle
Le modèle ne reçoit plus les définitions, mais un moyen de les trouver, concrètement un outil de recherche et, au mieux, une liste de noms.
{
"name": "search_tools",
"description": "Cherche parmi les outils disponibles ceux qui correspondent à un besoin. Renvoie jusqu'à 5 définitions complètes, utilisables au tour suivant. À appeler avant de conclure qu'une action est impossible.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Le besoin en mots-clés : objet et action. Exemple : \"facture créer avoir\"."
}
},
"required": ["query"]
}
}Les définitions trouvées sont ajoutées à la suite de la conversation, sans être réinjectées en tête, si bien que le préfixe reste stable et que le cache tient. Le contexte de départ passe de 24 000 tokens à quelques centaines, et ne grossit que de ce qui sert.
Le prix est le suivant.
- Un tour de plus à chaque premier usage d'un outil. Sur un agent interactif, cela représente une à trois secondes de latence visibles.
- Un problème de recherche à la place d'un problème de sélection. Un outil que la recherche ne remonte pas n'existe pas. Avant, une mauvaise description donnait un mauvais choix ; maintenant elle donne une absence.
- Une capacité que le modèle ignore. Avec le catalogue complet sous les yeux, il sait ce qui est possible et peut proposer une action à laquelle l'utilisateur n'avait pas pensé. Avec un moteur de recherche, il ne cherche que ce qu'il a déjà l'idée de chercher.
Le deuxième point produit des pannes particulièrement discrètes. L'utilisateur demande « émets un avoir sur la facture de mars », l'outil s'appelle create_credit_note et sa description est en anglais. La recherche sur « avoir facture » ne renvoie rien, et l'agent répond poliment que la plateforme ne gère pas les avoirs. Rien n'apparaît dans les logs, puisque la panne consiste en une phrase fausse dite avec assurance.
La parade tient en deux mesures. Les descriptions s'écrivent dans le vocabulaire de ceux qui formulent les demandes, synonymes compris. Et les trois à cinq outils qui servent dans presque toutes les sessions restent toujours chargés, la découverte progressive étant réservée à la longue traîne.
Faire écrire un programme qui appelle les outils
L'exécution de code change le modèle d'interaction. Les outils ne sont plus des appels que le modèle émet un par un, mais des fonctions dans un environnement d'exécution, et le modèle écrit un programme qui les compose.
import { listInvoices } from "./tools/billing";
import { sendReminder } from "./tools/mail";
const overdue = (await listInvoices({ status: "unpaid" }))
.filter((i) => daysSince(i.dueDate) > 30);
const failed: string[] = [];
for (const invoice of overdue) {
try {
await sendReminder({ invoiceId: invoice.id, template: "relance-2" });
} catch (e) {
failed.push(`${invoice.id}: ${String(e)}`);
}
}
console.log(JSON.stringify({ sent: overdue.length - failed.length, failed }));Deux gains se cumulent. La découverte devient une lecture de fichiers : l'agent liste le répertoire tools/, ouvre la signature dont il a besoin, et n'a jamais vu les autres. Surtout, les résultats intermédiaires ne traversent plus le contexte, puisque les 2 000 factures impayées restent dans la mémoire du processus et que le modèle ne lit que la dernière ligne. En reprenant l'arithmétique, 2 000 enregistrements à 150 tokens font 300 000 tokens en appels directs, contre une trentaine ici. Les boucles, les filtres et les jointures sont faits par un interpréteur, qui ne se trompe pas en recopiant un identifiant.
Le prix est d'une autre nature, et c'est lui qui me fait hésiter plus souvent qu'on ne le croirait.
Un bac à sable à opérer. Du code écrit par un modèle, sur la foi d'entrées que vous ne maîtrisez pas, s'exécute chez vous. Il faut une isolation réelle (conteneur sans réseau sortant hormis les outils, limites de CPU, de mémoire et de durée, système de fichiers jetable) et quelqu'un pour la maintenir.
Des permissions qui changent de grain. En appels directs, chaque action passe devant vous, et l'on peut demander confirmation avant sendReminder. Dans un script, cinquante envois partent dans une boucle. Soit on valide le script entier avant exécution, ce qui demande à l'utilisateur de lire du code, soit on déplace le contrôle dans les fonctions elles-mêmes, avec un quota par exécution, un mode simulation par défaut ou une écriture réservée à un second passage.
Une observabilité à reconstruire. La trace montrait une suite d'appels lisibles ; elle montre maintenant un bloc de code et une sortie standard. Sans instrumentation des fonctions-outils côté bac à sable, on ne sait plus ce qui a été fait.
Des définitions ou des résultats : d'où viennent vos tokens
Les deux approches ne répondent pas au même encombrement. La découverte progressive traite le poids des définitions, tandis que l'exécution de code traite en plus le poids des résultats et le nombre de tours. Mieux vaut regarder d'où viennent vos tokens avant de choisir, et une trace de dix sessions réelles suffit à le savoir.
| Situation | Ce que je fais |
|---|---|
| Moins de vingt outils, peu de recouvrement | Rien d'autre qu'élaguer et réécrire les descriptions |
| Beaucoup d'outils, appels unitaires, actions sensibles | Découverte progressive, cœur de cinq outils toujours chargé |
| Gros volumes de données entre outils, boucles, agrégats | Exécution de code, fonctions d'écriture idempotentes |
| Agent exposé à des contenus non fiables, pas d'équipe pour opérer un bac à sable | Pas d'exécution de code ; sous-ensembles d'outils par tâche |
| Chaque action doit être confirmée par un humain | Appels directs ; le script fait perdre le grain de contrôle |
Avant d'adopter l'une ou l'autre, quelques questions tranchent.
- Sur vos traces, quelle part du contexte vient des définitions, quelle part des résultats d'outils ? Si les définitions font moins de dix pour cent, la découverte progressive ne changera rien.
- Combien d'outils distincts une session utilise-t-elle réellement ? Si ce sont toujours les huit mêmes, il ne faut pas de mécanisme, mais supprimer les cinquante-deux autres.
- Qui est d'astreinte sur le bac à sable ? Si la réponse est « personne », la question de l'exécution de code est close pour l'instant.
La deuxième question mène le plus souvent à la technique la plus rentable, et la moins intéressante à raconter, puisqu'un outil retiré ne coûte ni tokens, ni tour de recherche, ni conteneur.
À lire ensuite
Toute la rubrique IAIA
Vos descriptions d'outils sont votre vrai prompt
Pourquoi le nom, la description, les paramètres et les retours d'un outil pilotent le comportement d'un agent plus sûrement que le prompt système, et comment les écrire et les tester comme tels.
7 minMembres
IA
Multi-agents : l'isolation de contexte se paie en coordination
Ce que des sous-agents achètent vraiment, ce qu'ils coûtent en tokens et en coordination, et la règle qui réconcilie « tout dans un seul fil » et « un agent par tâche » : paralléliser la lecture, jamais la décision.
7 minMembres
IA
Idempotence des appels d'outils
Pourquoi une étape d'agent rejouée envoie deux fois le mail, d'où tirer la clé d'idempotence, et quels outils méritent ce traitement.
6 minLecture libre