HemIT

Modularité et IA : comment réduire le contexte ?

IAqualitéBDD
Modularité et IA : comment réduire le contexte ?

Dans mon précédent article sur les tests, je mentionnais que les tests étaient ce qui séparait le vibe coding de l’ingénierie logicielle assistée par IA. Évidemment, c’est aller un peu vite en besogne. Avoir un harnais de test solide nous permet de rattrapper nos erreurs. Est-ce que c’est une bonne raison pour écrire le code de production n’importe comment ?

Probablement pas.

En 15 ans de développement, j’ai dû commencer à prioriser les concepts d’architecture. Malgré toute l’importance que j’apporte à l’architecture hexagonale et ses consœurs, il y a un concept qui me parait beaucoup plus important : la modularité.

Une notion importante : le contexte

Pour nous, pauvres humains

Il y a des limites à ce que nous sommes capables de garder en mémoire. Une limite extrêmement faible même. On l’appelle le Nombre de Miller. Ce nombre vaut 7, plus ou moins 2.

Prenez un temps pour assimiler cette donnée, car elle est vraiment importante. Nous ne pouvons garder en mémoire que 7 éléments. Alors, heureusement pour nous, ce ne sont pas 7 mots, mais 7 idées. Si vous travaillez suffisamment fréquemment avec certains concepts, ils remontent en mémoire comme un seul et même élément. C’est pour cela qu’à bosser plusieurs jours sur le même bout de code, il nous paraît naturel. Puis que, trois mois plus tard, on se demande comment on a pu écrire une telle horreur.

7, c’est si vous êtes en forme. 9 si vous êtes “dans la zone”. Sous stress ? 3, 4… On peut même descendre à 0.

Et les IA, alors ?

Les IA, elles, trichent un peu. Les IA possèdent un contexte : c’est l’ensemble des informations auxquelles elles ont accès pour raisonner. Claude 4.5 Sonnet gère un contexte de 2 millions de tokens. Ce n’est pas une science exacte, mais ça fait environ 1.5 million de mots. Oui, Sonnet peut absorber la quasi-intégralité des livres Game of Thrones parus jusqu’à maintenant. Impressionnant, non ?

Sauf qu’il y a un problème. Ce problème s’appelle l’effet “Lost in the middle”. Pour faire court, récupérer une info dans toute l’œuvre de JRR Martin, c’est compliqué. L’IA va donc prioriser des éléments de contexte. Et quand elle le fait, elle donne plus de poids aux éléments qui se trouvent au début et à la fin de son contexte.

Qu’est-ce que ça veut dire pour nous ? Concrètement, si votre IA ingurgite des centaines de fichiers en entrée, elle va soudainement oublier que non, on ne met pas de requête vers la base de données dans le Contrôleur. Ou elle va oublier comment on crée un utilisateur dans les tests… etc…

Donc, aussi cools que paraissent les modèles IAs, ce serait pas mal de les aider.

Diminuer la charge cognitive

Prenons un exemple dans le code du Jardin des Esperluettes.

Le module Gestion du Profil fait, à date, 3600 lignes de code. Le module est un tout petit peu central quand même, parce qu’il ne contient rien de moins que le nom et l’avatar de chaque utilisateur. Donc, chaque fois que j’ai besoin de l’information, il faut que je l’appelle. Le module contient plein de fonctionnalités : affichage de la page de profil, de l’édition, gestion de l’image, affichage d’onglets, règles de visualisation, gestion du cache pour les données récupérées fréquemment…

Vous me direz que je n’ai qu’à aller chercher la donnée en base direct. Mais il y a de la logique dans ce module. Des choses que vous n’avez pas le droit de voir suivant votre rôle. Il est hors de question que je duplique mes règles, ou, bien pire, que j’oublie de faire la vérif dans un des chemins de code. Donc, j’ai isolé tout ça dans un module.

Dans quels cas ai-je besoin de voir ces 3600 lignes ? Uniquement quand je bosse sur le profil. Le reste du temps ? C’est du bruit.

C’est pour ça que mon module exporte une unique API publique, la ProfilePublicApi (je vous enlève les commentaires pour plus de concision, mais évidemment, ils sont là ) :

interface ProfilePublicApi
{
    public function getFullProfile(int $userId): ?FullProfileDto;
    public function getPublicProfile(int $userId): ?ProfileDto;
    public function getPublicProfileBySlug(string $slug): ?ProfileDto;
    public function getPublicProfiles(array $userIds): array;
    public function searchDisplayNames(string $query, int $limit = 50, bool $includeInactive = false): array;
    public function searchPublicProfiles(string $query, int $limit = 25, bool $includeInactive = false): array;
}

Quand je réfléchis au module histoire, et que j’ai besoin des infos du profil, je n’ai besoin que de ce fichier. Je réduis ma charge cognitive.

Quand je demande à Brice (pour rappel, c’est comme ça que j’appelle mon IA) de développer une fonctionnalité dans le module histoire, et qu’il a besoin du profil, je lui pointe uniquement ce fichier. Je réduis son contexte, donc l’effet lost-in-the-middle.

Brice avec le pouce levé
Merci qui ? Généré par ChatGPT

J’ai réduit ma charge cognitive. J’ai réduit le contexte de mon IA. Tout le monde est gagnant. Le bonus ? Ces modules font d’excellentes frontières pour effectuer des tests.

Modules techniques, modules fonctionnels ?

C’est probablement une des notions les plus fondamentales, et les plus incomprises de l’informatique. Et pour cause ? Une bonne partie des frameworks vous induisent en erreur dès le départ.

Organisation de fichiers Laravel par défaut
L'organisation de fichiers Laravel par défaut

Beaucoup de frameworks partent du fait que vous n’aurez qu’un module fonctionnel. Tous les contrôleurs vont au même endroit (ici, dans app/Http/Controllers), tous les Services au même endroit (dans Services), etc…

Et ça, ça pose deux problèmes majeurs :

  • Quand vous voulez raisonner sur votre module Profil, vous vous retrouvez avec des contrôleurs qui ne vous intéressent pas (#Charge cognitive)
  • Naïvement, puisque le ProfileService va voir le ProfileRepository, on se dit que le StoryService, qui est au même endroit, peut aller voir le ProfileRepository. Non ? (#Couplage)

“Mais le framework me dit de faire comme ça !! 😤”

J’entends l’argument. Et je ne jette pas la pierre. Mais considérez une alternative :

Organisation de fichiers du Jardin
L'organisation de fichiers du Jardin

Vous sentez la différence ? Je n’ai jamais plus de deux ou trois dossiers d’ouvert, et jamais plus d’un dossier privé ouvert en même temps. Ça a plein d’avantages :

  • les tests sont maintenant avec leurs modules (ainsi que les migrations de base de données, tiens !)
  • j’ai un script (deptrac) qui vérifie qu’il n’y a pas de petit malin qui essaie de dépendre d’éléments privés (bon, on pourrait l’améliorer encore un peu)
  • si demain, j’ai un module tellement simple qu’il tient dans un fichier, je crée juste un dossier Controller. Pas de Service, pas de Repository.
  • à l’inverse, si demain, j’ai vraiment besoin d’une architecture hexagonale sur un module complexe… je change juste ce module ! On ne va quand même pas faire de l’hexagonal pour la FAQ… si ? Non ! 😅

Et Brice ? Il fait comme moi, il a 90% des dossiers fermés. Avec les README et les PublicApi, il sait ce qu’il se passe autour de lui.👍

Pour aller plus loin

Ces éléments d’architecture paraissent anodins, mais ils vous aident, vous et l’IA. En le couplant aux fichiers de règles (bientôt !) de votre IA, vous pouvez réduire le contexte, et ainsi augmenter la précision. Et garder des modules de taille raisonnable, c’est la meilleure façon de vous assurer que votre IA n’écrive pas du code que pour les machines. Parce qu’à un moment, souvent le pire, il faudra plonger pour trouver d’où vient ce bug étrange que l’IA n’arrive pas à résoudre.

Tous ces éléments ici sont liés aux notions de Couplage et de Cohésion que je vous invite à explorer en détail.

Et si vous voulez savoir comment délimiter vos modules, le Domain Driven Design(DDD) est une approche intéressante. Je peux vous conseiller cet article de Jakub Lambrych (en anglais)