1. Erreurs et idempotence dans ChatGPT App
Sur le Web classique, beaucoup vivent encore dans le paradigme « l’utilisateur clique sur un bouton → une requête HTTP → une réponse ». Dans le monde des LLM, ce n’est plus le cas. Le modèle peut décider d’appeler votre outil plusieurs fois, peut régénérer la réponse après que l’utilisateur a cliqué sur Regenerate, peut redemander des précisions ou rencontrer une erreur réseau en chemin. Au final, un même outil peut très bien être appelé deux ou trois fois avec des arguments très similaires.
Dans le même temps, toute erreur a soudain deux consommateurs. D’un côté — le modèle, auquel il faut une explication lisible par machine de ce qui n’a pas fonctionné, afin qu’il puisse corriger les arguments et réessayer. De l’autre — l’UI utilisateur (le widget et le chat lui‑même), où il faut afficher un message humain et proposer des actions, et non « Error: 500 (see logs) ».
Autre point important : les architectures classiques ne prévoient pas vraiment que quelqu’un appuie massivement sur « répéter la réponse », augmentant ainsi le nombre d’appels répétés (retries). Dans ChatGPT, ce scénario est la norme. De plus, la plateforme peut elle‑même relancer un appel en cas de problèmes réseau temporaires. Par conséquent, le concept d’idempotence dans cet écosystème n’est pas une option facultative, mais une exigence de base, en particulier pour les outils qui font quelque chose « pour de vrai » — créent des commandes, débitent de l’argent, envoient des e‑mails, etc.
Ce cours porte précisément sur la manière d’éviter qu’un appel d’outil raté ne gâche l’humeur de l’utilisateur et ne casse votre production.
Insight
ChatGPT ne transmet pas des arguments à vos fonctions, elle devine plutôt un ensemble d’arguments conforme à votre schéma. Elle regarde le JSON Schema, le contexte du dialogue et choisit statistiquement des valeurs — et se trompe assez souvent. Des erreurs comme « mauvais type », « champ obligatoire manquant », « paramètres contradictoires » font partie de la vie normale des tool-calls, et non des cas de force majeure. D’après les données publiques et la télémétrie, de tels ratés représentent facilement jusqu’à ~30 % des appels pour des schémas complexes.
Pour le modèle, ce n’est pas un problème : il interprète votre réponse comme un signal « les arguments étaient mauvais » et réessaie, parfois deux ou trois fois de suite, en modifiant légèrement l’entrée. Pour vous, cela signifie autre chose : chaque outil doit être conçu comme s’il allait presque à coup sûr être appelé plusieurs fois avec des paramètres très proches.
C’est pour cela que l’idempotence est si importante. ChatGPT va essayer encore et encore de deviner avec quels paramètres appeler vos fonctions. 2–3 tentatives par appel — c’est la norme.
2. Configuration sécurisée du widget : text/html+skybridge et _meta
Avant d’aborder les sujets purement serveur (erreurs, retries, idempotence), réglons un point spécifique à l’Apps SDK sur la sécurité de l’UI : comment faire en sorte que votre widget se rende dans le chat en toute sécurité, et non comme « une page web effrayante ».
registerResource et le type MIME text/html+skybridge
Du point de vue de ChatGPT, votre widget est une ressource HTML spéciale qui atterrit dans le sandbox du client ChatGPT, et non directement dans le navigateur de l’utilisateur. Pour que la plateforme comprenne qu’il s’agit d’un widget et non d’un simple HTML, on utilise le type MIME text/html+skybridge.
Au niveau MCP/serveur, vous enregistrez la ressource comme suit (pseudo‑TS) :
// quelque part dans la configuration du serveur MCP
registerResource({
name: "giftgenius-widget",
path: "/widget",
mimeType: "text/html+skybridge", // important !
});
Ce mimeType est un signal pour le client ChatGPT : « ce n’est pas un simple HTML, mais un gabarit de composant pour widget intégré à exécuter dans un environnement isolé ». Si vous indiquez un simple text/html, la plateforme peut afficher le HTML brut ou refuser carrément le rendu.
_meta et contrôle de la sécurité : CSP, domaine et bordure
Ensuite entrent en jeu les métadonnées transmises avec la réponse de l’outil ou de la ressource — _meta. Elles vous permettent de contrôler quelles ressources externes le widget peut charger, comment il se comporte visuellement et même comment le modèle le décrira.
Exemple typique de structure :
const toolResult = {
content: "<!-- HTML du widget -->",
_meta: {
"openai/widgetCSP": "default-src 'self'; img-src https://cdn.example.com",
"openai/widgetDomain": "https://chatgpt.com",
"openai/widgetPrefersBorder": true,
"openai/widgetDescription": "GiftGenius affiche des recommandations de cadeaux sous forme de cartes."
}
};
Passons en revue les champs clés.
- openai/widgetCSP définit la Content Security Policy du widget. C’est votre petit pare‑feu côté navigateur dans ChatGPT : vous énumérez explicitement d’où l’on peut charger scripts, styles, images, faire des XHR, etc. La plateforme attend une politique stricte sans wildcard * ; vous devez indiquer explicitement les domaines utilisés (chat, votre API, CDN).
- openai/widgetDomain définit l’origin dans le contexte duquel votre widget fonctionnera. C’est généralement le domaine de ChatGPT ; vous ne le remplacez pas par votre site, vous indiquez simplement comment cela doit apparaître en environnement isolé.
- openai/widgetPrefersBorder — un indicateur purement visuel : faut‑il dessiner une bordure autour du widget. Pour GiftGenius, il est logique de garder une bordure afin de distinguer visuellement le bloc de recommandations des messages ordinaires du chat.
- openai/widgetDescription — description textuelle pour le modèle. Plutôt que d’« inventer » elle‑même une explication, le modèle peut utiliser cette chaîne pour dire à l’utilisateur quelle interface vient de s’ouvrir. Cela réduit le risque de commentaires étranges ou verbeux du modèle.
Conclusion pratique : en configurant soigneusement mimeType et _meta une bonne fois, vous obtenez une UI sûre et isolée, qui ne va que là où vous l’autorisez et se comporte de manière prévisible tant pour l’utilisateur que pour la plateforme. Pour la partie frontend de la sécurité, c’est réglé : le widget vit dans un sandbox et ne communique que vers les destinations autorisées. Concentrons‑nous maintenant sur la partie serveur — les types d’erreurs, leur description et la manière de rendre les outils idempotents.
Insight : mise en cache du widget
ChatGPT met en cache le HTML du widget au moment de l’enregistrement de l’application. Le widget HTML de ChatGPT n’est pas un « frontend vivant », mais un artefact de build figé. Lors de la publication (Store ou Dev Mode), la plateforme lit la ressource HTML (text/html+skybridge) et utilise ensuite toujours cette version. Toute modification — même une ligne de texte ou une indentation dans une carte — signifie en pratique une nouvelle release.
Conséquence : les changements de structure HTML, de slots, d’attributs data-* et du contrat structuredContent → DOM ne sont pas un « quick fix », mais une véritable migration frontend. Si aujourd’hui vous rendez une liste depuis items[] et que demain vous passez à results[], l’ancien widget ne le saura pas : il continuera à recevoir l’ancien JSON et se comportera incorrectement.
3. Types d’erreurs lors de l’exécution des outils
Passons au cœur du sujet : quels types d’erreurs un outil peut‑il rencontrer et en quoi diffèrent‑elles du point de vue UX et backend. Il est pratique de raisonner en quatre couches d’erreurs.
Erreurs de validation d’entrée
Le niveau le plus basique — lorsque les arguments d’entrée ne correspondent pas du tout au contrat.
Exemples pour notre application d’apprentissage GiftGenius et son outil suggest_gifts (sélection de cadeaux par centres d’intérêt et budget) :
- un âge inférieur à zéro ou supérieur à 120 ;
- un budget négatif ;
- le champ obligatoire relationship_type est manquant ;
- budget_min > budget_max.
Un simple JSON non conforme au schéma entre aussi dans cette catégorie. Idéalement, l’Apps SDK et le JSON Schema filtreront les appels « vraiment mauvais » avant votre code, mais la validation métier (comme la relation budget_min/budget_max) reste à votre charge.
Erreurs de logique métier
Ici, l’entrée est a priori correcte, mais selon vos règles métier, vous ne pouvez pas fournir un résultat normal.
Scénarios typiques :
- aucun cadeau trouvé pour les centres d’intérêt et le budget indiqués ;
- l’utilisateur a dépassé sa limite quotidienne de sélections ;
- le produit que le modèle propose d’acheter n’est plus en vente.
Ce n’est pas « le serveur s’est cassé », mais des situations normales et attendues qu’il faut présenter à l’utilisateur et au modèle de manière exploitable, et non comme un 500 Internal Server Error.
Erreurs d’infrastructure externe
Cette couche concerne déjà « l’enfer technique » : base indisponible, API externe en timeout, exception non gérée dans votre code.
Par exemple :
- la requête au catalogue de cadeaux renvoie 503 ou ne répond pas ;
- MongoDB décide soudain de faire une pause ;
- dans le code de filtrage des cadeaux, vous divisez par zéro.
Du point de vue UX, c’est souvent l’occasion de dire : « Service temporairement indisponible, veuillez réessayer plus tard », parfois — de tenter un retry discret. Mais il est important d’éviter de se taire ou d’afficher un stack trace brut à l’utilisateur.
Erreurs de plateforme/réseau
Enfin, il existe une couche qui peut se produire en dehors de votre code : le tool call n’est pas arrivé, la connexion a été coupée au milieu de la réponse, le streaming a été interrompu. Cela arrive plus souvent qu’on ne le pense. Par exemple, si vous utilisez un tunnel gratuit, aux heures de pointe sa vitesse peut chuter au point que les tool calls de ChatGPT expirent par timeout.
Vous ne pouvez pas tout contrôler, mais vous pouvez certainement concevoir les outils et le widget de sorte que les appels répétés et les interruptions ne transforment pas le système en chaos. C’est précisément pour cela que nous parlons d’idempotence et de gestion soignée des erreurs, plutôt que de « try/catch et on oublie ».
4. Comment décrire et renvoyer les erreurs : pour le modèle et pour l’UI
Changement de mentalité important : votre erreur n’est pas seulement ce que vous avez logué dans console.error. C’est une partie du contrat de l’outil, avec laquelle travailleront le modèle et l’interface.
Structure d’erreur
En général, il est pratique de s’en tenir à une structure simple :
type ToolError = {
code: string; // "VALIDATION_ERROR", "NO_RESULTS", "UPSTREAM_TIMEOUT"
message: string; // lisible par un humain ou concis pour le modèle
retryable: boolean; // s’il est pertinent de réessayer
};
Et on peut envelopper le résultat de l’outil dans une union discriminante :
type SuggestGiftsResult =
| { ok: true; gifts: GiftCard[] }
| { ok: false; error: ToolError };
Par ailleurs, le protocole MCP possède un indicateur séparé « c’est une erreur », mais il reste utile d’adopter un format maison, pour que l’UI et le modèle puissent interpréter de la même manière ce qui s’est passé.
Stratégie « fail gracefully »
Il n’est pas nécessaire de traiter chaque situation désagréable comme une erreur « dure ». Parfois, il est bien plus utile de renvoyer un résultat vide, sans erreur, mais avec une explication.
Par exemple, si aucun cadeau n’est trouvé, il est raisonnable de renvoyer ok : true, un tableau gifts vide [] et un champ noResultsReason pour l’UI et le modèle, plutôt que "NO_RESULTS" en tant qu’erreur. Le modèle peut alors poursuivre le dialogue : « Je n’ai rien trouvé dans ce budget, souhaitez‑vous l’augmenter ou préciser les intérêts ? ».
En revanche, si l’API externe est complètement tombée, cela ressemble davantage à ok : false avec code : "UPSTREAM_UNAVAILABLE" et retryable : true, afin que le modèle ait une chance de réessayer plus tard ou avec d’autres paramètres.
Pour rappel, à la section 3 nous avons quatre couches d’erreurs. Les erreurs de validation vont généralement avec ok : false et retryable : false — le modèle ne doit pas répéter le même appel avec les mêmes arguments. Les situations métier comme « rien trouvé » sont plus souvent renvoyées avec ok : true, un résultat vide et une explication. Les pannes d’infrastructure des services externes — avec ok : false et retryable : true, pour que le modèle puisse réessayer en toute sécurité. Et les erreurs de plateforme/réseau peuvent survenir avant ou après votre code et, en pratique, se traduisent souvent par un nouvel appel de l’outil — d’où l’importance de l’idempotence soignée, dont nous parlons ensuite.
Ne pas divulguer les détails internes
Dans le code serveur, la tentation est grande de simplement relayer error.toString() dans la réponse. Pour les outils LLM, ce n’est pas une bonne idée : vous obtiendrez du bruit dans le dialogue et exposerez potentiellement des informations sensibles (URLs de services internes, stack traces, noms de tables). Recommandation : intercepter les exceptions et les transformer en codes d’erreurs compacts et messages propres.
Exemple de wrapper minimal :
try {
const gifts = await loadGiftsFromCatalog(input);
return { ok: true, gifts };
} catch (err) {
console.error("suggest_gifts failed", err);
return {
ok: false,
error: {
code: "UPSTREAM_ERROR",
message: "Catalog service is unavailable",
retryable: true
}
};
}
Le modèle voit un signal propre, l’UI — un texte compréhensible, et les détails restent dans les logs.
Affichage de l’erreur dans le widget
Du point de vue d’un widget React, la tâche est banale : vérifier ok, et s’il est false, afficher un message amical et, si possible, une manière de continuer.
function GiftResults({ result }: { result: SuggestGiftsResult }) {
if (!result.ok) {
return (
<div>
<p>Impossible de sélectionner des cadeaux : {result.error.message}</p>
{result.error.retryable && <p>Essayez de modifier les paramètres ou de relancer la requête.</p>}
</div>
);
}
if (result.gifts.length === 0) {
return <p>Aucun cadeau trouvé pour ces critères. Essayez d’ajuster le budget ou les centres d’intérêt.</p>;
}
return <GiftCardsList gifts={result.gifts} />;
}
C’est précisément le cas où un message simple et honnête rend l’UX bien plus agréable que « quelque chose s’est mal passé ».
Nous avons déjà convenu que certaines erreurs peuvent être honnêtement marquées retryable : true et proposer à l’utilisateur « d’essayer encore ». Dès que des retries apparaissent (explicites dans l’UI ou implicites côté plateforme), se pose la question suivante : que se passe‑t‑il si un même outil est appelé deux fois avec les mêmes données ? C’est l’histoire de l’idempotence.
5. Idempotence : se protéger d’« un autre appel identique »
Passons à la partie la plus amusante. Formellement, l’idempotence est la propriété d’une opération selon laquelle un appel répété avec les mêmes données d’entrée ne modifie pas l’état du système et le résultat. Au sens strict, cela concerne à la fois l’absence d’effets secondaires répétés et une réponse identique. En pratique, dans ChatGPT Apps, le premier point nous intéresse surtout : éviter que des appels répétés n’altèrent les données et ne créent de nouvelles entités, même si la réponse elle‑même peut légèrement différer.
Dans le contexte de ChatGPT Apps, l’idempotence est une protection contre tout ce qui arrive avec les retries, Regenerate et la logique imprévisible des LLM.
Où l’idempotence est particulièrement importante
Les outils en lecture seule sont généralement sûrs par défaut : appeler suggest_gifts avec les mêmes paramètres ne fait que renvoyer une autre liste de cadeaux. Même si elle diffère un peu, cela ne change pas l’état du système et ne produit pas d’effets secondaires.
Les outils qui modifient l’état de systèmes externes sont critiques :
- création de commande (create_order) ;
- exécution d’un paiement (charge_card, submit_payment) ;
- envoi d’e‑mails et de notifications (send_email, send_sms) ;
- création d’entités à effets secondaires (par exemple, des réservations).
Si un tel outil est appelé deux fois de suite avec des arguments presque identiques, vous pouvez vous retrouver avec des commandes en double, des doubles débits et d’autres joies de la comptabilité.
Patron idempotency_key
L’approche classique : ajouter à l’outil un paramètre supplémentaire idempotency_key — un identifiant de l’opération. Si une requête avec cette clé a déjà été traitée avec succès, le serveur n’exécute pas l’action à nouveau et renvoie le résultat enregistré.
Exemple de schéma étendu pour l’outil hypothétique create_checkout_session dans GiftGenius :
const CreateCheckoutSchema = {
type: "object",
properties: {
giftId: {
type: "string",
description: "ID du cadeau sélectionné"
},
idempotency_key: {
type: "string",
description: "Clé unique de l’opération pour éviter les doublons"
}
},
required: ["giftId", "idempotency_key"]
} as const;
Sur le serveur, le handler fait à peu près ceci :
async function createCheckoutSession(input: CreateCheckoutInput) {
const existing = await db.checkoutSessions.findOne({ idempotencyKey: input.idempotency_key });
if (existing) {
return existing; // renvoyer le résultat précédent
}
const session = await paymentProvider.createSession({ giftId: input.giftId });
await db.checkoutSessions.insert({ idempotencyKey: input.idempotency_key, session });
return session;
}
Si, pour une raison quelconque, le modèle appelle l’outil une deuxième fois avec le même idempotency_key, l’utilisateur ne subira pas un second paiement ; il verra simplement la même session de checkout.
Séparation prepare et commit
Pour les actions particulièrement sensibles (paiements, changements irréversibles), on utilise souvent une approche en deux phases : un outil distinct pour la préparation (prepare_*), un autre pour le commit (commit_*).
Par exemple :
- prepare_order — vérifie la disponibilité du produit, calcule le coût, renvoie un « brouillon de commande » ;
- commit_order — à partir de l’ID du brouillon, crée la commande réelle et lance le paiement.
Ce design offre plusieurs avantages. Premièrement, on peut rendre la première étape entièrement idempotente : un prepare_order répété avec les mêmes paramètres renverra le même brouillon. Deuxièmement, on peut autoriser commit_order uniquement après une confirmation explicite de l’utilisateur, ce qui est pratique du point de vue UX et sécurité.
6. Conception d’outils sûrs
L’idempotence est nécessaire, mais ce n’est pas le seul ingrédient de la sécurité. Le design même de l’ensemble d’outils que vous exposez au modèle compte énormément.
Principe du moindre privilège
L’idée est simple : chaque outil doit faire exactement ce qui est nécessaire au scénario, pas une ligne de plus. Évitez une fonction unique do_anything_with_user_account, qui :
- peut tout lire, mettre à jour et supprimer ;
- prend une chaîne operation et un JSON payload « au petit bonheur la chance ».
Il vaut mieux avoir des tools distincts et bien décrits :
- get_user_profile ;
- update_user_preferences ;
- create_order ;
- cancel_order.
Même logique pour GiftGenius : suggest_gifts se contente de proposer des options ; create_checkout_session n’a pas à savoir comment annuler des commandes ni changer l’e‑mail de l’utilisateur.
Bien séparer les outils « read » et « write »
Un bon pattern consiste à séparer clairement les outils qui lisent uniquement des données de ceux qui les modifient. Interroger le catalogue de cadeaux (search_products, suggest_gifts) est intrinsèquement sûr, même si le modèle en abuse. En revanche, create_order ou charge_payment exigent plus de prudence.
Dans la description de ces outils, il faut indiquer explicitement ce qu’ils font et dans quel contexte les appeler. Par exemple :
{
"name": "create_checkout_session",
"description": "Crée une nouvelle session de paiement pour un cadeau. À appeler UNIQUEMENT après confirmation explicite de l’utilisateur.",
"parameters": { /* ... */ }
}
Ce n’est pas une protection à 100 % (le LLM peut quand même se tromper), mais au minimum vous lui envoyez un signal clair sur les risques.
Human‑in‑the‑loop et confirmations
Pour les actions vraiment « dangereuses », il est pratique de bâtir un scénario avec confirmation. Par exemple, le modèle :
- Appelle d’abord un outil qui prépare les données pour l’achat et les renvoie sous une forme pratique pour l’UI (nom du cadeau, prix, adresse de livraison).
- La plateforme affiche un widget avec un bouton « Confirmer l’achat ».
- Ce n’est qu’après le clic que l’outil de commit est appelé et effectue le paiement réel.
Ainsi, vous ne laissez pas au modèle la possibilité de passer une commande « en douce » sans l’utilisateur, même s’il décide soudain que c’est une très bonne idée.
Sémantique du risque dans les descriptions et annotations
Dans certaines versions de la plateforme, apparaissent des annotations spéciales comme destructiveHint, qui signalent qu’un outil peut faire des actions irréversibles. Même si de tels champs n’existent pas ou sont encore instables, vous pouvez intégrer cette sémantique directement dans la description et les noms de paramètres.
Par exemple, au lieu de :
{
"name": "delete_user_data",
"description": "Supprime les données de l’utilisateur."
}
faire :
{
"name": "request_user_data_deletion",
"description": "Marque le compte de l’utilisateur pour suppression de ses données personnelles conformément à la politique du service. À utiliser UNIQUEMENT après une demande explicite de l’utilisateur."
}
Et, en parallèle, construire un UX de confirmation humain.
7. Petite amélioration pratique de GiftGenius
Relions tout cela à notre application d’apprentissage GiftGenius — App de sélection de cadeaux. Supposons que nous ajoutions à GiftGenius un outil supplémentaire — create_checkout_session, afin que l’utilisateur puisse non seulement choisir un cadeau, mais aussi passer à la commande.
Du point de vue du JSON Schema et de la sécurité, nous faisons ce qui suit.
Premièrement, nous ajoutons idempotency_key et une description soignée :
const CreateCheckoutTool = {
name: "create_checkout_session",
description:
"Crée une session de paiement pour un cadeau sélectionné. " +
"À appeler uniquement après que l’utilisateur a confirmé vouloir acheter ce cadeau.",
parameters: {
type: "object",
properties: {
gift_id: {
type: "string",
description: "Identifiant du cadeau issu du résultat de suggest_gifts."
},
idempotency_key: {
type: "string",
description: "Clé unique de l’opération. Réutilisez la même clé lors d’un nouvel appel."
}
},
required: ["gift_id", "idempotency_key"]
}
} as const;
Deuxièmement, côté serveur, nous implémentons un handler idempotent :
async function handleCreateCheckout(input: CreateCheckoutInput) {
const existing = await db.checkout.findOne({ idempotencyKey: input.idempotency_key });
if (existing) {
return { ok: true, checkout: existing };
}
const checkout = await payments.createSession({ giftId: input.gift_id });
await db.checkout.insert({ idempotencyKey: input.idempotency_key, ...checkout });
return { ok: true, checkout };
}
Troisièmement, nous prenons en compte les erreurs :
try {
return await handleCreateCheckout(input);
} catch (err) {
console.error("create_checkout_session failed", err);
return {
ok: false,
error: {
code: "PAYMENT_PROVIDER_ERROR",
message: "Impossible de créer la session de paiement. Veuillez réessayer plus tard.",
retryable: true
}
};
}
Dans le widget, nous affichons un état d’erreur compréhensible et, éventuellement, un bouton « Réessayer » au niveau de l’UI, qui initie un nouveau dialogue avec le modèle.
Pas à pas, notre gentil projet pédagogique cesse d’être « un jouet de démo » et se transforme lentement en quelque chose de théoriquement publiable en production.
8. Erreurs courantes dans la gestion des erreurs et de l’idempotence des outils
Erreur n° 1 : Une erreur = simplement throw et 500.
Si, au moindre problème, votre tool se contente de lever une exception qui devient « quelque chose s’est mal passé », le modèle et l’UI restent sans informations. Le modèle ne comprend pas s’il faut répéter l’appel avec d’autres arguments, et l’utilisateur ne sait pas quoi faire ensuite. Il est bien préférable de renvoyer une erreur structurée avec code, message concis et indicateur retryable, et de journaliser les détails côté serveur.
Erreur n° 2 : Absence de distinction entre les types d’erreurs.
Mélanger les erreurs de validation, métier et infrastructurelles dans le même sac est une mauvaise idée. La situation « rien trouvé » ressemble alors pour le modèle et l’utilisateur à « la base de données a chuté ». Cela casse l’UX et empêche le modèle de réagir correctement : au lieu de proposer de modifier la requête, il passe en mode « désolé, le service est en panne ». C’est particulièrement pénible lorsque vous mélangez, par exemple, des erreurs métier et des erreurs d’infrastructure de la section 3.
Erreur n° 3 : Opérations non idempotentes dans un monde de retries.
Concevoir l’outil create_order comme s’il n’était appelé qu’une seule fois est le chemin direct vers des doublons de commandes, surtout lorsque l’utilisateur clique activement sur Regenerate ou que la connexion se coupe en chemin. Si l’outil a des effets de bord, il faut presque toujours ajouter idempotency_key et stocker les résultats pour qu’un nouvel appel ne crée pas de nouvelles entités.
Erreur n° 4 : Un outil « universel » monstrueux.
Parfois, les développeurs tentent de créer un super‑outil avec un paramètre action qui sait tout faire : chercher, créer, modifier et supprimer. Pour un LLM, c’est presque garanti d’induire un comportement imprévisible : le modèle apprend plus difficilement quand appeler quoi, et les conséquences des erreurs deviennent bien plus lourdes. Il est préférable de découper en petits outils, bien décrits, si possible read‑only, et, séparément, des outils mutateurs soigneusement conçus avec confirmations.
Erreur n° 5 : Les détails internes fuitent dans les réponses.
Envoyer au modèle et à l’UI un stack trace brut ou le texte complet des exceptions est une paresse d’ingénierie classique. C’est désagréable pour l’utilisateur, cela peut exposer la structure interne du système et n’aide pas le modèle à se corriger. Il faut intercepter les exceptions, les mapper en codes compacts et messages simples, et garder tous les détails dans les logs et la solution de monitoring.
Erreur n° 6 : Absence de liaison entre erreurs et UX du widget.
Souvent, la partie serveur renvoie soigneusement des codes d’erreurs, mais le widget côté UI tombe dans un spinner infini ou un bloc vide. L’utilisateur voit « rien ne s’est passé », le modèle voit que le tool‑call s’est terminé et continue le dialogue comme si de rien n’était. Bien mieux : prévoir des états error et empty dédiés, afficher des messages clairs et, si possible, suggérer des actions (modifier les paramètres, réessayer plus tard).
Erreur n° 7 : Ignorer le principe du moindre privilège.
Même si vous avez mis en place l’idempotence et une bonne gestion des erreurs, mais exposez un outil du type execute_sql_anywhere qui peut tout faire, le risque reste énorme. Le LLM peut l’appeler dans un mauvais contexte ou avec des paramètres erronés. Chaque outil doit être aussi étroit que possible et ne faire qu’une seule action claire — surtout quand il s’agit d’argent ou de données personnelles utilisateur.
GO TO FULL VERSION