Imposer nos standards de qualité à l’IA

Dans un article précédent, on vous a brièvement présenté l’état de l’art des outils pour intégrer l'IA à votre développement grâce aux Coding Agents. Malheureusement (ou heureusement ?) l'IA reste l'IA. La qualité du code généré, la cohérence architecturale et la maintenance à long terme restent des préoccupations majeures. Il nous revient donc de faire en sorte que ces modèles aillent dans la bonne direction. Dans cet article, nous essaierons de vous montrer comment.

Pour rappel, nous utilisons comme exemple le développement d’une application front-end, développée par Cline avec Claude 3.5 sonnet en 25 minutes, comme en témoigne cette vidéo condensée :

0:00
/4:03

Mais est-ce qu’on peut vraiment créer une app à partir d’un simple paragraphe en ne faisant (presque) rien ? La réponse est oui, si vous n’êtes pas regardants sur l’app en question. En fait, à l'inverse d’un humain, pour qui chaque itération amène vers une meilleure version du produit, l’IA a tendance à l’empirer si elle n’a pas atteint son objectif dès le premier coup. Plus l’IA est utilisée, moins son utilisation est pertinente. 

Quand on y réfléchit, c'est logique. L’humain qui veut améliorer son produit passe par une phase d’analyse et de réflexion avant de remettre les mains dans le cambouis, là où l’IA ne sait pas prendre de recul sur son travail. Mais ce n’est pas une fatalité. Comme souvent, la solution peut être trouvée dans le biomimétisme en forçant l’IA à analyser l’état de l’application grâce aux tests. 

Les tests comme contrat

Chez Ippon, on aime bien le TDD, on s’est donc tout naturellement demandé si Cline avait les mêmes goûts que nous. Disclaimer immédiat pour les puristes : le starter s’apparente plus à de l’ATDD et du Test-First qu’à du TDD, mais permet de poursuivre de nouvelles itérations en TDD.

De plus, des tests, que ce soit en termes de fonction ou de structure, ça marche super bien avec nos outils. L’avantage des tests, c'est qu’on utilise un langage formel et sans ambiguïté qui permettra au modèle de mieux comprendre nos attentes. C’est le prompt parfait : un contrat clair entre l’IA et le développeur. Sans oublier que le modèle est capable d'exécuter tous ces tests et d’en interpréter les résultats.

Sans aller jusqu’à l’auto-citation, on a dit dans le premier article de cette série :

[...] elle est capable de générer des applications fonctionnelles complètes. À condition d'avoir un développeur expérimenté qui regarde par-dessus son épaule.

Ici, les tests prennent le rôle du développeur expérimenté et forcent l’IA à rester dans les clous.

Dans le cas de notre vidéo, qui est une application front-end en Vue 3 composée d’un tableau de bord et d’une navbar, nous avons mis en place les tests Cypress suivants (tests de composant issus de la pyramide de test) :


Grâce à Cline, nous pouvons spécifier à l’IA de prendre en compte ces tests en fournissant les fichiers dans lesquels ils sont déclarés :

I want you to add the dashboard to this Vue app using the tests from @/cypress/e2e/ and making sure the hexagonal architecture is correctly implemented as described in the tests from @/src/HexagonalArchTest.spec.ts   
The final application should look like the joined mockup picture.

Ces tests font maintenant partie du prompt, c'est-à-dire des spécifications de la tâche demandée à Cline. Maintenant, nous pouvons aussi spécifier dans le prompt que cette tâche n’est pas complète tant que les tests ne passent pas :

The conditions before a task to create a feature can be considered as complete are :
- the unit and/or end to end headless tests pass (npm run test:e2e:headless)

Et là, on assiste à quelque chose d’assez magique quand on a jamais vu ça :

Cline itère tout seul sur le problème qui lui a été donné. Les agents codent, testent, analysent les résultats des tests, modifient leur code, et ainsi de suite jusqu’à ce que les tests passent et que la tâche soit considérée comme terminée.

Concrètement, comment ça se passe ? Rentrons dans les détails en conservant l’exemple de la vidéo (cf 2:55). Après un premier round de corrections, Cline exécute à nouveau les tests. Voilà un extrait du prompt brut, tel qu’il est envoyé à l’API suite à l’exécution des tests :

**User:**

[execute_command for 'npm run test:e2e:headless'] Result:
[...]

  Running:  dashboard.cy.ts                                                                 (1 of 2)

Dashboard
  ✓ should display files informations (2843ms)
  chart
    1) should display chart labels
    2) should contain the correct dataset values
    ✓ should display received and processed files labels (699ms)
  donut charts
    insurance distribution chart
      ✓ should display the insurance distribution title (929ms)
      ✓ should display insurance distribution legends with correct values (725ms)
      3) should render insurance distribution donut chart with correct data
    staff distribution chart
      ✓ should display the staff distribution title (866ms)
      ✓ should display staff distribution legends with correct values (911ms)
      4) should render staff distribution donut chart with correct data
    donut charts layout
      ✓ should display donut charts in correct order (846ms)

  7 passing (22s)
  4 failing

[...]


 3) Dashboard
      donut charts
        insurance distribution chart
          should render insurance distribution donut chart with correct data:
    AssertionError: Timed out retrying after 4000ms: expected '<window>' to have property 'insuranceDonutInstance'
    at Context.eval (webpack://poc/./cypress/e2e/dashboard.cy.ts:157:0)

4) Dashboard
      donut charts
        staff distribution chart
          should render staff distribution donut chart with correct data:
    AssertionError: Timed out retrying after 4000ms: expected '<window>' to have property 'staffDonutInstance'
    at Context.eval (webpack://poc/./cypress/e2e/dashboard.cy.ts:185:0)

En fait, le LLM fait exactement ce qu’on aurait fait : un copier/coller des résultats du test, directement depuis la console, sans formatage particulier. Au détail près qu’on peut voir un peu de sucre syntaxique qui permet de rajouter du contexte, en l’occurrence que le prompt est le résultat d’une commande.

On peut également voir que pour le LLM, que Cline soit à l’origine du prompt ou que ce soit l’utilisateur, c’est la même chose. Les résultats des tests sont précédés de “**User**:”.

La suite, vous la connaissez. Le LLM analyse les résultats des tests, interprète les erreurs, propose une solution puis l’implémente dans la foulée :

**Assistant:**

<thinking>
We have a few issues to fix:
1. Chart instance is not being properly exposed to the window object
2. Chart data is not being properly accessed in tests

Let's fix these issues one by one, starting with the DonutChart component.
</thinking>


<replace_in_file>
<path>src/infrastructure/primary/vue/dashboard/DonutChart.vue</path>

[...]

Tester l’Interface Utilisateur sans utilisateur

Une application fonctionnelle, c'est bien, mais une application qu’un être humain qui n’est pas bilingue en ligne de commande peut utiliser confortablement, c'est mieux. Ce confort passe par un travail sur l’Interface Utilisateur (UI). De ce travail naît idéalement une maquette, qu’on peut directement fournir à Cline via le prompt. Attention cependant à bien utiliser un modèle qui supporte les images. 

Quand on développe une application, on ne passe généralement pas du temps à tester chaque détail visuel. Si le style du titre ne convient pas, on le voit immédiatement dans le navigateur. Et bien Cline peut faire pareil en utilisant un tool (cf 1:40 sur la vidéo). En l'occurrence le “browser_action”. Celui-ci lance un navigateur et peut prendre des captures d’écran de notre outil. Il a également accès à la console du navigateur. Ces informations sont ensuite transmises au LLM dans le prompt suivant. 

Cline peut décider d’utiliser ce tool de lui-même, mais pour être certain, on peut rajouter les lignes suivantes au prompt :

- perform a visual check using the browser :
- if a mockup is provided the visual test is based on the mockup.
- run "npm run dev &" to launch the server and launch the browser using the correct port
- the visual test should be satisfactory

S’enclenche alors une boucle itérative où Cline ajuste le visuel pour s’aligner au mieux avec le design initial. Malgré tout, on obtient des résultats plus ou moins satisfaisants. Le style a souvent nécessité des retouches de notre part (cf 3:12). Et c’est logique, Cline utilise un LLM multimodal pour traduire la maquette en tokens et reconstruit l’image en fonction de schémas appris, mais ne comprend pas réellement les relations entre les éléments comme un humain le ferait.

Des pistes restent à explorer pour pallier ce problème. On pourrait, par exemple, tenter d’y remédier en donnant à Cline une pattern library, une bibliothèque de composants où il pourrait directement piocher pour assembler la page sans interpréter chaque élément.

L’architecture hexagonale pour ne pas tourner en rond

Chez Ippon, on aime aussi l’architecture hexagonale. Il se trouve que L’architecture hexagonale, et plus généralement tout pattern implémentant le principe du “Separation of Concern” (séparation des préoccupations), est tout trouvé pour optimiser notre utilisation de l’IA. L’idée derrière ce principe est de privilégier l’isolation de chaque aspect du développement, ce qui a deux conséquences très avantageuses dans notre cas : 

  • La réduction des dépendances implique la réduction du contexte nécessaire à la réalisation de nos tâches.

Dans ce cas, on parle du “contexte” comme la mémoire du LLM. Plus précisément, on parle de “context window”. C’est une des caractéristiques “concrètes” de chaque modèle. Le contexte, c'est le nombre de tokens (ou plus simplement le nombre de mots, même si c’est plus compliqué que ça en réalité) qui peuvent être “retenus” par l’IA. Garder le contexte au minimum a deux avantages : 

  1. Limiter les hallucinations, qui ont tendance à être plus importantes si la context window est trop remplie. Empiriquement, nous essayons d’éviter de dépasser 75% de remplissage.
  2. Limiter les coûts. Le coût des requêtes est directement proportionnel au nombre de tokens dans la requête (et dans la réponse). Plus le contexte est grand, plus il y aura un grand nombre de tokens à inclure dans la requête. Certains modèles ont du cache, mais ça reste une bonne pratique de limiter ce contexte. L’optimisation des coûts dans le cadre de l’utilisation des coding agents est un vaste sujet qui fera également l’objet d’un article.

  • La séparation des préoccupations implique que le scope de chaque tâche aura tendance à être réduit. Ce qui tombe bien puisque plus les tâches sont petites, plus l’IA est performante. Plus une tâche est grosse, plus l’IA a de chance de se tromper. Il faut garder en tête qu’au fond un LLM c’est un modèle d’IA qui a pour principe de prédire la suite d’un texte en se basant sur des probabilités. Ce n’est pas une science exacte. Même si l’IA a 99% de chance d’être pertinente, si on lui fournit 1000 occasions de se tromper, les probabilités sont contre nous. Encore une fois pour plus de détails, je vous redirige vers un futur article, qui cette fois-ci aura comme sujet le prompt engineering.

En pratique, qu’est-ce que ça donne ? Est-ce qu’on dit juste à l’IA de respecter l’architecture hexagonale dans le prompt ? On pourrait. On pourrait également vérifier que l’arborescence de fichiers planifiée correspond à nos attentes, et risquer que durant l’implémentation l’IA dévie du plan initial. On pourrait également passer un contrat, en utilisant… Des tests. 

Maintenant comment on teste l’architecture hexagonale de notre projet Vue ? Heureusement, on a des super collègues qui ont déjà réglé la question pour nous. Tristan EYMA et Manon METRAL ont développé une variante TypeScript au framework de test d’architecture, qui vous permet de forcer le respect de certaines dépendances pour garantir une architecture hexagonale. Si vous êtes curieux, ils expliquent tout ça très bien dans cet article : Arch-Unit-Ts.

Une fois ces tests ajoutés à notre projet, il suffit de procéder comme pour n’importe quel test et indiquer dans le prompt qu’il faut que les tests passent. L’IA analyse ensuite le contenu des tests durant la phase de planification et en déduit un certain nombre de contraintes, qu’elle associe à sa compréhension innée du concept d’architecture hexagonale. 

Dans le cas de la vidéo on peut ainsi lire (0:13, puis plus de détails dans l’implémentation à 0:15) :

3. Key architectural requirements from HexagonalArchTest.spec.ts:
- Must follow domain-driven hexagonal architecture
- Clear separation between domain, application, and infrastructure layers
- Primary adapters can only be called from secondaries
- Domain should not depend on infrastructure
- Primary should not depend on secondary


Et en effet, si on regarde l’arborescence générée une fois que les tests passent, on a : 

D’ailleurs, une fois que le projet est lancé, l’IA déduira (la plupart du temps) seule les pratiques de développement mises en place dans celui-ci, et aura tendance à les répliquer. On a donc plus forcément besoin de spécifier qu’on travaille avec une architecture hexagonale lors de futures tâches, même si ça reste une bonne pratique pour éviter tout débordement.

Conclusion

Tout au long de cet article, vous avez pu voir que les solutions trouvées aux problèmes rencontrés sont tout droit sorties d’un bouquin d’ingénierie logicielle. Test Driven Development, Separation Of Concern, Architecture Hexagonale. Ces concepts qu’on a abordés démontrent que l’expertise du développeur reste obligatoire pour utiliser efficacement l’IA. D’autant plus que ces solutions ne sont pas universelles. 

Peut-être que la connaissance détaillée d’un langage n’est plus une métrique de ce qui fait un bon développeur, mais ça reste très largement le cas pour la connaissance des principes algorithmiques, l’expertise dans le processus de développement et dans la gestion de projet. 

Et pour terminer cet article, nous tenons à préciser qu’il sera probablement obsolète dans un an vu la rapidité des innovations. Plus que jamais, c’est notre capacité à apprendre et à s’adapter qui va être challengée. Si vous souhaitez continuer à apprendre et à vous adapter, nous vous invitons à lire l’article suivant de cette série, centré autour du prompt engineering.