· 9 min de lecture

Ton agent n’a pas besoin de chaque outil dans son contexte

L’agent IA de DynoTable peut atteindre 38 outils. Il en voit rarement tous à la fois. Où nous avons tiré la ligne entre ce que le modèle obtient immédiatement et ce qu’il doit aller chercher s’est avéré être la décision la plus lourde de conséquences de toute la boîte à outils, et la ligne a fini nulle part près de là où nous l’avions d’abord mise.

Nous avons construit un mécanisme de découverte pour que le modèle commence avec un petit ensemble et cherche le reste. Puis nous avons regardé les modèles cheap l’utiliser, et remis 27 des 38 outils dans le cœur toujours visible. Le mécanisme a survécu. Notre théorie sur qui en avait besoin, non.

Nous avons appris ça en construisant une surface d’outils pour des modèles auxquels on ne peut pas faire confiance pour aller chercher.

Un outil que le modèle n’appelle jamais te coûte quand même

Chaque outil que tu exposes, c’est son nom, sa description et son schéma d’input complet, sérialisés dans la requête avant que l’utilisateur ait tapé quoi que ce soit. Trente-huit de ceux-là, ce n’est pas gratuit.

Les tokens sont la plus petite moitié de la facture. Le vrai coût, c’est la précision de sélection : plus un modèle scanne d’options quasi identiques, plus souvent il en choisit la mauvaise. Notre catalogue est plein d’options quasi identiques exprès. Cinq de nos outils existent en double, openTable et proposeOpenTable, openWorkbench et proposeOpenWorkbench, et ainsi de suite. Chaque paire fait la même chose ; l’une le fait immédiatement, l’autre émet une puce que l’utilisateur clique d’abord. Cette distinction est porteuse pour la sécurité de l’ et presque invisible dans une liste plate de noms.

Le guide client propre du mène avec exactement ce point : charger chaque définition d’outil d’avance gaspille des tokens, ajoute de la latence, et dégrade la performance du modèle. Être d’accord avec ça est facile. Décider quels outils perdent leur siège, c’est là que ça devient intéressant.

Nous avons construit un outil de recherche. Le modèle plancher ne l’appelait pas.

Le mécanisme est à deux niveaux. Un ensemble d’outils est actif dès la première étape. Le reste est invisible jusqu’à ce que le modèle appelle searchTools(query), qui score le catalogue sur noms, descriptions et mots-clés, renvoie les correspondances, et les ajoute à l’ensemble d’outils que le modèle a le droit d’appeler aux étapes suivantes.

Catalogue d’outilsBoucle d’agentModèleCatalogue d’outilsBoucle d’agentModèleétape 1 — ensemble actif = le cœurinlineétape 2 — ensemble actif élargisearchTools("export csv")scorer noms + mots-clésstartExport, getExportStatus,listActiveExportscorrespondances (noms maintenantappelables)startExport({tabId})

Puis nous l’avons couru contre notre modèle plancher. Nous ne tunons pas cet agent contre un modèle frontier ; il tourne sur tes propres credentials Bedrock, donc les gens choisissent des modèles cheap, et nous optimisons pour le moins cher. Interrogé sur un fichier attaché, ce modèle est allé chasser dans le listing des onglets ouverts à la place. Il appelait rarement l’outil de recherche du tout. Tout ce qui n’était pas directement visible n’existait pas pour lui.

Ce résultat tue le design évident. Si la découverte est la seule route vers un outil, chaque requête qui a besoin de cet outil dépend du modèle qui choisit d’aller chercher, et les modèles les plus susceptibles d’avoir besoin d’aide sont les moins susceptibles de la demander.

Donc le découpage a cessé d’être « petit cœur, grande queue » et est devenu une question sur la requête, pas sur l’outil : la formulation de l’utilisateur nomme-t-elle l’outil ? Les 11 outils que nous avons gardés découvrables sont ceux où la réponse est oui. « Exporte ça en CSV » fait chercher un modèle export. « Montre-moi les commandes du mois dernier » ne le fait pas chercher un outil de réglage de filtre, donc celui-là reste inline. Les statistiques d’index, les specs sauvegardées, l’introspection de relations, et les surfaces de changements staged sont toutes des choses qu’un utilisateur demande par nom quand il les veut, et jamais implicitement.

Vingt-sept inline n’est pas un nombre que nous aurions défendu à l’avance. C’est le nombre qui a survécu au contact avec le modèle contre lequel nous livrons vraiment.

La course qui aurait rendu la découverte silencieusement inutile

La découverte a une contrainte de timing facile à rater et dure à remarquer.

Quand l’outil de recherche renvoie des correspondances, ces noms doivent rejoindre l’ensemble autorisé avant que la prochaine étape du modèle soit préparée. L’endroit évident pour le faire est le hook qui tire quand une étape se termine. Ce hook est documenté pour tirer, dans certaines versions du SDK, après la préparation de l’étape suivante, ce qui veut dire que la mutation atterrit une étape trop tard.

Le mode d’échec est vilain. Le modèle cherche. Il obtient un résultat correct qui nomme l’outil dont il a besoin. Il appelle cet outil à l’étape suivante et se fait dire que l’outil n’existe pas. Intermittent, dépendant de quelle version du SDK tu as résolue, et ça se lit comme un modèle stupide plutôt qu’un runner cassé.

Le correctif est de muter l’ensemble autorisé à l’intérieur de l’exécution propre de l’outil de recherche, qui est garantie de se terminer avant que la boucle avance. C’est une différence d’une ligne dans l’endroit où vit une instruction, et c’est la différence entre un mécanisme de découverte qui marche et un qui échoue une fraction du temps pour des raisons que personne n’attribuera correctement.

Trois recherches, puis stop

La recherche est plafonnée à 3 appels par tour d’agent. Le quatrième renvoie ceci au lieu de tourner :

{"error": "search-budget-exhausted", "budgetCap": 3}

Le plafond existe à cause d’une boucle précise. Le modèle cherche, ne trouve pas ce qu’il imaginait, cherche encore avec un synonyme, ne trouve pas ça non plus, et brûle tout son budget d’étapes à l’intérieur de l’outil de recherche sans jamais toucher la base. Le plafonner force une décision, s’engager sur un des outils déjà trouvés, ou demander à l’utilisateur, au point où chercher davantage a cessé de payer.

Le message d’erreur quand un modèle appelle un outil qu’il n’a pas découvert suit le même principe que nous utilisons pour chaque validateur de l’agent :

Tool 'startExport' not in active set. Call searchTools(query='startExport')
to discover it, or use one of: <inline tool names>

Un rejet qui nomme l’action de récupération coûte une étape en plus. Un rejet qui dit juste non coûte le tour.

Une ligne par outil, tout le reste dérivé

Chaque outil est déclaré une fois, dans une seule liste plate, et la ligne porte toute l’identité de l’outil : son nom et sa description, les mots-clés sur lesquels la recherche matche, s’il démarre inline ou découvrable, dans quel tier il tourne, et comment il est exposé via MCP.

Ces tiers comptent autant que le découpage de visibilité. Vingt et un outils sont silent, des lectures qui tournent sans interrompre personne. Seize sont gated derrière l’échelle d’autorisation. Exactement un n’appartient ni à l’un ni à l’autre, parce que l’outil de recherche n’est pas une capacité que l’agent utilise sur tes données ; c’est une partie de la boucle elle-même. L’exposition MCP est un troisième axe sur la même ligne : lecture seule, staging, full, ou exclu purement et simplement, ce qui est le cas de trois outils.

La règle qui garde ça honnête, c’est que chaque autre liste du système est dérivée de ces lignes : l’ensemble du tier silent, les tiers de portée MCP, l’ensemble write-scoped, et aucune n’est maintenue à la main. Une liste silent tenue à la main à côté d’une liste MCP tenue à la main, c’est exactement comme ça qu’un outil finit correctement gated dans le chat et discrètement ungated vers un client externe.

La contrainte que nous n’avions pas vue venir, c’est que la liste de déclarations doit contenir zéro import runtime. Elle est partagée par l’UI desktop et le backend, et un seul import atteint, transitivement, une dépendance crypto Node-only via l’implémentation d’un outil. Tire ça dans le bundle navigateur et l’app échoue au chargement de module. Ni le type checker ni les tests unitaires ne le catchent ; les deux résolvent l’import joyeusement. Ce qui le catch, c’est un test qui lit le fichier comme texte et échoue sur toute instruction import, ce qui paraît grossier jusqu’à la première fois que ça te sauve.

Ce qui se transfère si tu en construis un

  • Compte tes outils avant de défendre ton architecture. Le bon découpage est une mesure, pas un principe.
  • Teste la découverte contre ton modèle le plus faible. Un modèle frontier cherchera quand il le doit ; ça ne te dit rien sur le modèle que tes utilisateurs choisissent.
  • Décide de la visibilité selon que la formulation propre de l’utilisateur nomme l’outil. Les outils invoqués implicitement appartiennent inline ; ceux que les gens demandent par nom peuvent être trouvés.
  • Vérifie quand les hooks d’étape de ton framework tirent vraiment avant d’y mettre quoi que ce soit sensible à l’ordre.
  • Plafonne les meta-outils. Tout ce qui peut être appelé en boucle sans toucher d’état réel le sera, et un budget d’étapes dépensé à chercher est un tour gaspillé.
  • Fais que les erreurs d’outil non découvert nomment l’appel de récupération, comme toute autre erreur de validateur.
  • Déclare chaque outil une fois et dérive chaque autre liste depuis lui. Deux listes tenues à la main des mêmes outils finissent par diverger, et la divergence apparaît à une frontière de sécurité.
  • Si un module porte une contrainte porteuse que ton compilateur ne peut pas exprimer, écris le test grossier qui l’enforce comme texte.

Où ça tourne

Tout ça livre à l’intérieur du catalogue d’outils de DynoTable, requêtage schema-aware sur tes propres credentials , avec des écritures qui n’atterrissent jamais ailleurs que dans une zone de staging relisible. Les mêmes déclarations pilotent le serveur MCP auquel se connectent les agents externes, où le tier d’exposition de chaque ligne devient la portée accordée à un client externe ; comment nous avons rendu ça sûr (OAuth, consentement, isolation des credentials) est une histoire séparée.

Et la couche sous tout ça, les validateurs qui rendent chacun de ces outils survivable par un modèle cheap, est son propre article.

Travaille avec DynamoDB sans la Console

Un client de bureau rapide pour DynamoDB qui exécute le vrai SQL que DynamoDB ne peut pas — JOINs, GROUP BY, agrégations — avec édition visuelle et un agent IA sur tes propres clés Bedrock.

Essai gratuit de 30 jours, sans carte bancaire — ensuite la formule Gratuit, sans limite de durée.