30.10.26
🚀 Programmation avec IA : gardez le contrôle ⚡13 min de lecture2 575 mots

🚀 Programmation avec IA : gardez le contrôle ⚡

Programmation avec intelligence artificielle : découvrez comment les agents de programmation peuvent aider, sans décider à votre place. Risques, tests de charge, fiabilité.

programmation avec intelligence artificielleagents de programmationtests de chargeconcurrence logiciellefiabilité des logiciels
🚀 Programmation avec IA : gardez le contrôle ⚡

Une IA peut réécrire votre logiciel. Elle ne peut pas décider à votre place

Demander à un agent d'intelligence artificielle de réécrire une application dans un autre langage est tentant : on fournit le code, on donne une consigne, puis on compare. Le problème, c'est qu'une application ne se limite pas à produire du code qui compile. Elle contient aussi des choix qui ne se voient pas toujours à première vue : faut-il garder la compatibilité avec les versions précédentes ? Quelle latence est acceptable ? Et que se passe-t-il si le système est surchargé ou redémarre au mauvais moment ?

Une expérimentation autour de Campfire Once, réécrit depuis Ruby on Rails vers Rust puis vers d'autres langages, met ces questions au centre. Les agents ne font pas tous les mêmes choix. Et quand on regarde des résultats de tests de charge, présentés comme un « verdict » sur un langage, on découvre vite qu'ils dépendent fortement de ce qu'on mesure (et de ce qu'on oublie de mesurer).

La leçon est simple : l'IA peut accélérer certaines tâches, mais elle ne remplace pas le jugement d'ingénierie. Voici comment lire ces comparaisons, comprendre ce qu'elles disent (et ce qu'elles taisent), et décider ce qu'il faut vérifier avant de confier une partie de votre logiciel à une IA.

Une réécriture ne consiste pas seulement à changer de langage

Dans l'étude de Campfire Once, l'application de départ est une base Ruby on Rails. Les versions Rust et Elixir ne sont pourtant pas de simples traductions « au mot près ». Elles divergent aussi sur l'architecture et sur certaines contraintes qu'elles maintiennent.

La version Rust, par exemple, abandonne certains aspects de compatibilité avec l'original et remplace Redis par des files d'attente en mémoire pour des notifications. De son côté, la version Elixir reste plus proche du comportement initial sur plusieurs points. Résultat : si deux versions « diffèrent », on ne peut pas automatiquement attribuer l'écart au seul langage. On compare à la fois des langages, des choix d'architecture et des décisions de mise en œuvre.

C'est une nuance qu'on oublie trop souvent dans les démonstrations d'IA générative. Une consigne du type « réécris cette application en Rust » laisse en réalité beaucoup de portes ouvertes. Le modèle doit trancher entre plusieurs solutions plausibles. Il ne connaît pas vos priorités produit.

Les exigences implicites deviennent des choix techniques

Avant même de réécrire, l'équipe doit clarifier des points qui ne sont pas de simples détails :

  • Faut-il une compatibilitĂ© totale avec les clients et les donnĂ©es existantes ?
  • Quelle dĂ©gradation accepte-t-on en cas de forte charge ?
  • On privilĂ©gie le dĂ©bit, la latence ou la consommation mĂ©moire ?
  • Une notification perdue après un redĂ©marrage est-elle tolĂ©rable ?
  • Le système doit-il garder une dĂ©pendance externe, ou peut-il utiliser des files locales ?

Il n'y a pas toujours de « bonne » réponse. Le bon choix dépend du produit, des utilisateurs et des contraintes opérationnelles. Une application de discussion interne ne vise pas forcément les mêmes exigences qu'un service de paiement ou un outil médical.

Le point important n'est pas uniquement que l'agent « peut se tromper ». Il peut aussi implémenter proprement une décision que personne n'a explicitement validée. Le résultat peut passer les tests disponibles, paraître cohérent, et pourtant ne pas correspondre au contrat attendu par le produit.

Comparer des réécritures exige un périmètre commun

Comparer deux langages, ça veut dire comparer des systèmes qui remplissent les mêmes fonctions, avec les mêmes contraintes. Concrètement : il faut définir ce qu'on attend comme comportement, les interfaces, les garanties de livraison et la façon dont le système doit se comporter sous charge. Sinon, le test peut finir par mesurer surtout l'effet de choix architecturaux différents, plus que les propriétés « du langage ».

Et dans un contexte d'entreprise, cette distinction compte encore plus. Une équipe qui demande à un agent de convertir un service peut obtenir un truc fonctionnel, tout en perdant au passage un mécanisme de reprise, une règle de compatibilité, ou une garantie de traitement. Une migration, ça mérite un cahier des charges orienté comportement, pas juste une consigne de transformation du code.

Le code généré doit toujours être compris

L'expérience autour de Campfire Once rappelle une évidence : une application qui compile n'est pas forcément une application bien adaptée à son environnement réel d'exécution. Les critiques de la version Elixir ont notamment pointé un traitement séquentiel des requêtes SQL dans un processus unique, y compris pour des lectures qui pourraient être exécutées en parallèle. C'est une limite à regarder de près, mais ça ne suffit pas à conclure que le langage ou la version est « inférieure » en soi.

La version Rust a aussi des choix à lire attentivement. Certaines opérations de base de données sont exécutées de façon bloquante depuis des tâches asynchrones. Et dans un environnement asynchrone, ces tâches partagent des ressources d'exécution. Si une tâche attend longtemps sans céder la main, d'autres tâches peuvent être retardées sur le même fil de travail.

Asynchrone ne veut pas dire automatiquement efficace

Une requête SQL qui attend une réponse pendant des dizaines de millisecondes mobilise peu de calcul. L'idée de l'asynchrone, c'est justement de permettre au runtime de suspendre la tâche en attente et de laisser avancer d'autres tâches pendant ce temps.

Mais si l'appel est bloquant, le fil de travail peut rester occupé alors qu'il ne fait aucun travail utile. Le même raisonnement s'applique aux verrous : un verrou tenu brièvement peut être acceptable, mais s'il reste pris assez longtemps, les tâches qui partagent ce fil peuvent patienter inutilement.

La documentation de Tokio sur les canaux de diffusion rappelle aussi que l'usage d'un composant asynchrone ne dispense pas de comprendre ses règles de fonctionnement : capacité, retard, comportement des récepteurs. Autrement dit, « asynchrone » ne veut rien dire sans contexte.

Ces observations ne prouvent pas qu'un agent ne peut pas produire du bon code. Elles montrent surtout qu'il faut évaluer le code par rapport au modèle d'exécution et aux exigences du système. Un résultat idiomatique sur le papier peut créer de la contention ou des comportements dégradés sous charge.

La revue doit porter sur le comportement, pas seulement sur le style

Pour une modification générée par IA, une revue utile touche au moins trois niveaux :

  1. Le comportement fonctionnel : cas d'usage, erreurs, règles de compatibilité - est-ce bien conservé ?
  2. Le comportement opérationnel : comment le système réagit à la latence, à la saturation, aux redémarrages, à la perte d'une dépendance ?
  3. Le coût de maintenance : est-ce que l'équipe peut comprendre, diagnostiquer et modifier le code ?

Lire le code, ce n'est pas une formalité « pour cocher une case ». C'est une manière concrète de repérer des décisions prises par le modèle sans connaître le contexte métier. Plus le composant est critique, plus l'équipe doit pouvoir expliquer ses modes de panne.

Un benchmark ne donne pas un verdict universel

Un chiffre de débit est facile à afficher, et facile à mal interpréter. Dans cette étude, les premiers tests regardaient surtout le nombre de requêtes traitées sur une durée donnée. Ce type de test peut aider, mais il ne dit pas à lui seul si le service reste fiable, si les notifications arrivent à temps, ou ce qui se passe quand les événements commencent à s'accumuler.

Le résultat le plus marquant dans la comparaison sous forte charge : environ 1 % des notifications attendues étaient livrées dans la configuration de test de la version Rust, contre 100 % pour la version Elixir testée, avec moins d'événements au total. Ce n'est pas à ignorer. Mais avant d'en tirer des conclusions sur « le langage », il faut regarder comment les requêtes sont envoyées et ce qu'on appelle exactement « une livraison réussie ».

Boucle fermée ou débit d'arrivée constant

En boucle fermée, chaque client envoie une nouvelle requête uniquement après avoir reçu la réponse précédente. Le débit dépend alors de la vitesse du système : s'il ralentit, la charge baisse mécaniquement. Ce test répond à une question utile : « jusqu'où ce système peut-il aller dans ces conditions ? »

Mais ce n'est pas forcément la réalité. En production, une vague d'utilisateurs peut arriver au même moment, sans attendre que « le serveur ait fini ». Pour étudier ce cas, on peut imposer un débit d'arrivée constant, indépendamment des temps de réponse. Là, on voit mieux comment le système encaisse (ou n'encaisse pas) un flux contraint, y compris quand il commence à être saturé.

Ces méthodes ne se remplacent pas. Une boucle fermée peut masquer l'accumulation de retard parce que la charge baisse avec le serveur. Un débit constant révèle mieux certains signes de saturation, mais ne couvre pas automatiquement toutes les questions de production. Le scénario choisi doit refléter le risque qu'on cherche à mesurer.

Mesurer plusieurs dimensions en mĂŞme temps

Dans les mesures complémentaires rapportées par l'auteur, la version Rust a livré environ 14 % des événements à 100 requêtes POST par seconde, sans erreurs HTTP. La version Elixir en a livré environ 60 %, mais environ 23 % des requêtes POST ont expiré. Sur la latence des notifications, les valeurs maximales approchaient aussi trois minutes dans certains résultats.

Ces chiffres sont valables pour cette configuration et ces versions, pas pour « Rust vs Elixir » en général. Ils montrent surtout pourquoi débit et taux de livraison, pris séparément, ne suffisent pas. On peut livrer une plus grande proportion d'événements tout en accumulant un retard énorme. Ou traiter plus de requêtes tout en faisant échouer certains clients.

Pour un test de charge vraiment pertinent, il faut viser au minimum :

  • le dĂ©bit et le taux d'erreurs HTTP ;
  • les percentiles de latence, y compris les valeurs extrĂŞmes ;
  • le nombre d'Ă©vĂ©nements produits et effectivement reçus ;
  • la mĂ©moire consommĂ©e et son Ă©volution dans le temps ;
  • les comportements de reconnexion et de reprise après saturation.

Le choix des métriques dépend du produit. Pour un outil de discussion, une notification en retard de plusieurs minutes peut être moins grave qu'une reconnexion rapide suivie d'un rechargement des messages récents. Pour un autre système, perdre un seul événement peut être inacceptable. Le benchmark doit rendre ces différences visibles.

La fiabilité repose sur des compromis explicites

La version Rust étudiée utilisait un canal de diffusion de capacité limitée pour envoyer des événements aux clients connectés. Si un récepteur n'arrive pas à suivre, il peut être signalé comme en retard. Cette approche limite l'usage mémoire et permet de détecter qu'un client ne consomme pas assez vite.

Quand la capacité du canal passe de 256 à 16 384 événements, le taux de livraison observé grimpe fortement : environ 90 % à 100 requêtes par seconde dans le test rapporté. Mais la latence maximale explose aussi : d'environ 11 secondes à plus de 130 secondes. En pratique, mieux vaut livrer « plus » ne veut pas dire livrer « utile ». Garder un événement en file ne le rend pas automatiquement pertinent si l'utilisateur le reçoit trop tard.

Choisir entre abandonner, attendre ou déconnecter

Quand un système est saturé, il faut une politique. Il peut abandonner des événements, accumuler du retard, refuser de nouvelles requêtes, ou déconnecter des clients trop lents. Chaque option a un prix :

  • Abandonner : protège le système, mais peut faire perdre une information importante.
  • Accumuler : Ă©vite une perte immĂ©diate, mais augmente la latence et la mĂ©moire consommĂ©e.
  • Refuser ou ralentir : rend la saturation visible, au prix d'une disponibilitĂ© rĂ©duite.
  • DĂ©connecter un client en retard : provoque une interruption, mais permet souvent une reconnexion et une resynchronisation plus propres.

Dans l'exemple avec des clients de navigateur, une reconnexion suivie du chargement des derniers messages peut être préférable à une notification qui arrive « en silence » plusieurs minutes plus tard. Ce n'est pas une loi universelle : c'est une décision produit à documenter et à tester.

Une file sans limite n'est pas une capacité infinie

L'implémentation Elixir évoquée indique que les boîtes aux lettres des processus GenServer n'ont pas de limite par défaut. Ça peut absorber des pics, mais une file qui grossit finit par coûter cher : la mémoire. Lors du test à 100 requêtes par seconde, la version Elixir rapportée a atteint environ 1,8 Go. Donc « ne pas perdre immédiatement » ne supprime pas le problème : ça le déplace vers la mémoire ou vers une latence excessive.

À un moment, une ressource non bornée rencontre forcément une limite : mémoire disponible, délais d'exécution, contraintes d'hébergement. Un processus tué par manque de mémoire n'est pas une meilleure garantie qu'un client déconnecté avec une stratégie de reprise claire.

Le seuil, lui, doit venir d'un objectif opérationnel : combien de retard tolère-t-on, quelle mémoire peut absorber le service, et que doit faire le client quand il ne suit plus ? La capacité d'une file n'est pas un simple paramètre d'optimisation : elle formalise un compromis entre continuité, fraîcheur des données et protection du système.

Conclusion : gardez la responsabilité des décisions

Les agents d'IA rendent certaines réécritures plus accessibles. Ils n'enlèvent ni les arbitrages d'architecture, ni la nécessité de relire et comprendre le code, ni la difficulté de concevoir un test représentatif. Une comparaison sérieuse doit isoler autant que possible les variables, définir ce qui compte pour le produit, et observer les erreurs, la latence, les files et la mémoire - pas seulement le débit maximal.

En pratique, commencez par écrire les garanties attendues : quelles données peut-on perdre, quel retard est acceptable, comment le service doit réagir en cas de surcharge. Ensuite, faites produire ou modifier le code, puis vérifiez ces garanties avec des tests adaptés et une revue capable d'expliquer les compromis.

Une IA peut générer une solution. La question, c'est si c'est une solution adaptée à votre système et à ce que votre équipe devra maintenir. Pour aller plus loin dans le cas présenté, vous pouvez consulter l'analyse originale de Campfire Once.

Questions frequentes

Un agent peut accélérer une réécriture, produire une première version ou aider à convertir des composants. Mais l’équipe doit définir les exigences, contrôler les écarts de comportement et tester les modes de panne. Sans revue compétente, le code généré peut intégrer des choix non validés.
Cette expérimentation ne permet pas de conclure cela. Les résultats dépendent aussi de l’architecture, des paramètres et des choix d’implémentation. Rust et Elixir ont des modèles d’exécution et des compromis différents. Il faut comparer des scénarios identiques et mesurer plusieurs propriétés du système.
En boucle fermée, les clients envoient une nouvelle requête après la réponse précédente : la charge ralentit avec le système. Avec un débit constant, les demandes arrivent à une cadence imposée, même si le serveur ralentit. Le second scénario révèle mieux certains effets de saturation, mais les deux peuvent être utiles.
Non. Une capacité plus grande peut réduire les pertes immédiates, mais augmenter la latence et la mémoire consommée. Si les événements deviennent obsolètes avant d’être traités, les conserver n’améliore pas nécessairement le service. Fixez le seuil à partir du comportement attendu en cas de retard.

Articles similaires