
Ceci fait partie d’une série en cours : voir le premier et le deuxième article.
Une monstruosité hideuse. Tout ingénieur expérimenté en a déjà vu une : un code si vaste, si risqué et si difficile à comprendre que personne n'ose y toucher. Il n'y a pas de tests unitaires, et chaque modification est source d'une petite crise cardiaque. Seuls les anciens s'y aventurent : ceux qui étaient là lors de la création du monstre et qui ne s'en approchent que lorsqu'il n'y a pas d'alternative. Il est obsolète, non modulaire et les dépendances sont obsolètes. Le composant est trop dangereux pour être sérieusement modifié.
Je me souviens de la première monstruosité que j'ai rencontrée. Une fonction de 5 000 lignes, essentielle au fonctionnement d'une entreprise valant des centaines de millions de dollars, et quasiment personne n'osait y toucher. Lorsqu'elle est tombée en panne, des équipes entières ont été réveillées en pleine nuit. Tout le développement de l'entreprise a été ralenti par la dépendance à ce composant clé. Des millions de dollars ont été dépensés pour tenter de gérer ce monstre.
Quel est le rapport avec les sujets de LLM ? Ils peuvent aussi devenir monstrueux ! Si effrayants à modifier que personne n'y touche. Ou, à l'inverse, les équipes tentent de les corriger et provoquent une avalanche d'incidents.
Les clients ne souhaitent pas payer pour des logiciels qui fonctionnent correctement uniquement les mardis et jeudis ; ils exigent une fiabilité constante et un flux constant de nouvelles fonctionnalités. Pour développer des systèmes à haute fiabilité et durables, il est essentiel de permettre à l'application d'évoluer tout en assurant son fonctionnement continu. Cela s'applique autant aux applications basées sur l'IA de génération qu'aux logiciels traditionnels.
Alors, comment obtenir une application performante, optimisée par l'IA, et non une monstruosité ? Cette série d'articles propose plus d'une douzaine d'approches. Elles partent toutes d'un principe : au lieu d'une seule invite gigantesque, privilégiez plusieurs invites plus petites et plus ciblées, chacune visant à résoudre un problème unique.
La modularisation consiste à décomposer un système complexe en composants plus petits, autonomes et réutilisables. En ingénierie logicielle traditionnelle, cela implique l'écriture de fonctions, de classes et de services gérant chacun une tâche spécifique. Dans le contexte de l'ingénierie des invites pour les LLM, la modularisation consiste à scinder une invite volumineuse et monolithique en invites plus petites et plus ciblées, chacune conçue pour effectuer une tâche unique et bien définie.
La modularisation vous permet d'apporter des modifications à votre système en toute sécurité au fil du temps. Son importance augmente lorsque :
Toutes ces dimensions doivent être comprises lors de la planification du système.
Mais comment la modularisation contribue-t-elle concrètement à la maintenance du système ? Les principaux avantages sont décrits ci-dessous.
Les performances des invites LLM sont intrinsèquement instables. Leur nature est telle que toute modification peut affecter les résultats de manière imprévisible. Vous pouvez gérer ce risque en décomposant les invites principales en composants, où une modification n'affecte que les performances d'une partie du système. Même si une invite est défectueuse, le reste du système fonctionnera comme avant la modification.
Mais que se passe-t-il si les invites fonctionnent comme une chaîne ? La rupture d'un composant ne perturberait-elle pas la chaîne ? Oui, mais les dommages sont tout de même réduits dans ce scénario. Une sortie erronée dans une chaîne d'invites peut fournir des entrées erronées aux invites en aval, mais chaque composant fonctionnerait toujours comme avant la modification sur l'ensemble des entrées valides. Comparez cela à la modification d'une invite géante : la modification peut (et va !) affecter chaque élément logique encodé dans cette invite. Vous n'avez pas perturbé un aspect du système, mais potentiellement tous ses éléments.
(L'exploitation sécuritaire des chaînes d'invites est un chapitre futur de la série. Vous devez prévoir différents types de pannes et disposer de plans d'urgence. Mais cela dépasse le cadre ici)
Quiconque a déjà écrit des tests unitaires sait qu'une fonction simple effectuant une seule tâche est BEAUCOUP plus facile à tester qu'une fonction complexe effectuant plusieurs tâches différentes. Il en va de même pour les invites : une invite courte et ciblée peut être testée beaucoup plus minutieusement, que ce soit manuellement ou de manière entièrement automatisée.
De nombreuses preuves montrent que les invites plus courtes ont tendance à être plus efficaces que les invites plus longues : 1 , 2 , 3 .
Les recherches sur les effets du multitâche sur la performance aux invites sont plus mitigées : 4 , 5. Une invite parfaitement optimisée peut, dans les bonnes conditions, être multitâche. En pratique, il est cependant beaucoup plus facile d'optimiser les invites ciblées, qui permettent de suivre la performance selon une seule dimension principale. Il est donc conseillé de privilégier des invites plus ciblées dans la mesure du possible.
Expliquer les subtilités d'un sujet complexe de 3 000 mots à un nouveau membre de l'équipe est un véritable défi. Et peu importe la quantité d'explications, seuls les auteurs contributeurs maîtrisent ce sujet.
Un système d'invites, chaque partie étant relativement simple, peut être intégré beaucoup plus rapidement ; les ingénieurs commenceront à être productifs plus tôt.
En utilisant différents modèles dans différentes parties du système, vous pouvez réaliser des économies de coûts et de latence significatives sans affecter la qualité de la réponse.
Par exemple, une invite déterminant la langue de saisie n'a pas besoin d'être particulièrement intelligente : elle ne nécessite pas votre modèle le plus récent et le plus cher. En revanche, l'invite générant la réponse basée sur la documentation pourrait bénéficier d'un raisonnement par chaîne de pensée intégré aux modèles haut de gamme.
La plupart des applications logicielles nécessitent l'ajout de fonctionnalités en toute sécurité sur de longues périodes. Il existe cependant une exception : les prototypes d'applications ne sont pas conçus pour une maintenance prolongée ; ils n'intègrent pas de nouvelles fonctionnalités et ne sont pas conçus pour une fiabilité élevée. Ne perdez donc pas de temps avec la modularisation lors de la création de prototypes. En fait, la plupart des modèles de cette série ne s'appliquent pas aux prototypes d'applications. Lors de la création d'un prototype, allez-y rapidement, vérifiez les inconnues critiques, puis jetez le code.
Il est également important de savoir quand arrêter la modularisation. La gestion des invites supplémentaires entraîne des frais supplémentaires et, si les avantages d'une modularisation plus poussée sont faibles, il est conseillé d'arrêter de fragmenter davantage le système.
Si la modularisation des invites était simple, tout le monde le ferait. Pour gérer de nombreuses invites dans un système, il est nécessaire d'investir dans une infrastructure ; sans elle, le chaos est total. Voici les exigences minimales pour l'infrastructure des invites LLM :
Possibilité d'ajouter des invites rapidement et facilement, de manière standardisée. Particulièrement important lorsque les invites sont chargées depuis l'extérieur de la base de code. Voir Principe II : Charger les invites en toute sécurité (si nécessaire) .
Capacité à déployer des invites de manière automatisée.
Capacité à enregistrer et à surveiller les entrées/sorties des invites individuelles.
Possibilité d'ajouter des tests automatisés qui couvrent les invites.
Un moyen de suivre facilement les dépenses en jetons/$ sur diverses invites.
Voyons comment se déroule la construction d’un système alimenté par Gen AI dans la pratique avec et sans modularisation.
Vous développez une application de support technique et souhaitez la mettre en œuvre avec une invite unique. Dans la version la plus simple, vous pouvez imaginer une invite monolithique générant des réponses tout en chargeant la documentation pertinente via RAG .
Cela semble simple et pratique, non ? Mais à mesure que l'on ajoute des fonctionnalités, des problèmes apparaissent avec cette architecture :
Vous souhaitez répondre aux messages dans une liste fixe de langues, mais pas gérer les autres. Pour ce faire, ajoutez des instructions pour répondre uniquement dans certaines langues et demandez au LLM de renvoyer le champ « langue » à des fins de reporting.
Vous souhaitez que toutes les conversations soient classifiées. Ajoutez un champ « étiquette » à l'invite.
En cas d'insatisfaction de l'utilisateur, transférez le problème au support technique. Ajoutez la variable de sortie « escalate_to_human » et les instructions dans l'invite.
Besoin d'une traduction de tous les messages envoyés pour l'audit interne. Renvoyer le champ « Traduit » avec un message en anglais.
Il faut une protection pour garantir que l'application ne demande jamais aux utilisateurs leur localisation ni leur vote lors des dernières élections. Ajoutez des instructions et testez manuellement.
Besoin d'un résumé pour chaque conversation ? Ajoutez un champ « Résumé » à chaque résultat.
Vous commencez peut-être à comprendre le problème : cette invite comporte désormais six sorties. La tester sera un cauchemar. Vous ajoutez la prise en charge d'une autre langue, et soudain, votre application renvoie le résumé en espagnol au lieu de l'anglais. Pourquoi ? Qui sait, les sorties LLM sont instables, donc modifier l'invite peut avoir des résultats imprévisibles.
Félicitations ! Vous avez créé un monstre ! Avec le temps, il grandira et causera encore plus de souffrance.
Une chaîne d'invites et une invite de classification entièrement séparée sont utilisées. L'invite initiale, de grande taille, est modulaire autant que possible.
Une invite détecte la langue, une autre fournit une traduction, une troisième détermine si l'utilisateur est contrarié et transmet le problème à un humain, l'invite de réponse génère la réponse, et le garde-fou vérifie la conformité de la réponse. Les sorties d'une invite sont liées aux entrées de la suivante ; un code traditionnel peut opérer entre ces invites pour, par exemple, vérifier l'éligibilité de la langue, sans impliquer de LLM.
Un changement peut toujours interrompre une invite donnée, mais les risques sont considérablement réduits car :
Vous bénéficiez de tous les avantages de l'IA de génération, mais les risques sont considérablement réduits. De plus, vous pouvez utiliser des modèles moins chers pour certains composants afin de réaliser des économies.
La modularisation vous permet d'isoler les erreurs, d'améliorer la maintenabilité et de construire un système plus fiable. Même les applications de taille moyenne comportent des dizaines, voire des centaines, d'invites de composants. Décomposez les invites jusqu'à ce qu'elles effectuent chacune une tâche unique, et jusqu'à ce que les avantages d'une modularisation plus poussée soient compensés par une complexité opérationnelle accrue. La modularisation de vos invites est indispensable pour que vos applications pilotées par l'IA restent fiables et continuent d'ajouter des fonctionnalités sur le long terme. Il existe déjà de nombreux systèmes « monstres » ; veillez à ne pas en créer de nouveaux !
Si vous avez apprécié cette série, abonnez-vous pour plus de publications.