Aller au contenu principal

Ce qui entoure la boucle

La boucle est posée. L'épisode précédent l'a établie : un cycle sans intention propre, capable de servir trois agents distincts précisément parce qu'il ne connaît aucun d'eux. Mais cette ignorance, qui fait sa force, n'est pas un état stable. Elle réclame que quelqu'un, ailleurs, prenne en charge ce qu'elle refuse de savoir. Nommer ces responsabilités, c'est tracer les modules. Or l'ordre dans lequel on les trace change ce qu'on trouve.

De l'ignorance à la frontière

La boucle ne sait pas quel LLM appeler. Quelque part dans le système, une responsabilité attend : instancier le bon client selon le provider configuré, résoudre les clés API, exposer une interface uniforme quelle que soit la cible. Je l'ai appelée LLMModule.

La boucle ne sait pas quels tools sont disponibles. Une autre responsabilité tient ce registre : quels outils existent, comment les exécuter, quels paramètres ils acceptent. Elle s'appelle ToolsModule.

La boucle ne sait pas comment assembler le prompt qu'elle va envoyer. Une autre encore compile les templates, injecte le contexte cross-pass, génère la documentation des tools. Elle s'appelle PromptModule.

La boucle ne sait pas qu'elle tourne en rond. Une dernière responsabilité surveille les patterns de tool calls, détecte les répétitions, gère la chaîne de fallback pour l'extraction structurée. Elle s'appelle SafetyModule.

Il reste une ignorance : la boucle ne sait pas où chercher le code qu'un tool lui retourne. Or cette responsabilité-là ne peut pas vivre à l'intérieur d'AgentModule. Elle sert plus que la boucle. Indexation, recherche sémantique, cache partagé entre tous les agents et toutes les instances : VectorStoreModule vit à côté, non à l'intérieur.

Ces cinq modules ont émergé des ignorances de la boucle, non pas d'une liste de fonctionnalités. C'est la différence entre un découpage qui suit la forme du problème et un découpage qui suit la forme des fonctionnalités.

Cinq noms introduits en quelques paragraphes : il vaut la peine de les situer les uns par rapport aux autres avant d'aller plus loin. LLMModule, ToolsModule, PromptModule et SafetyModule sont les quatre sous-modules qui donnent sa substance à AgentModule. Ils vivent à l'intérieur, assemblés par lui, invisibles depuis l'extérieur. Ce n'est pas eux que le reste du système connaît : c'est AgentModule. VectorStoreModule est différent. Il ne vit pas dans AgentModule, il est un module à part entière que plusieurs parties du système peuvent consommer indépendamment.

Le sens des flèches

Nommer les modules ne suffit pas. Il faut décider qui connaît qui.

AgenticLayer ne se résume pas aux cinq modules qu'on vient de nommer. Deux autres sont déjà présents dans le système. EvaluationModule orchestre les évaluations de code : il récupère la configuration des passes et des règles, déclenche l'évaluation de chaque règle, agrège les verdicts pour produire un résultat global. CompetencyModule suit la progression de compétences dans le temps : plusieurs évaluations, un graphe de compétences, des patterns de progression sur la durée. Ces deux modules ne savent pas évaluer une règle eux-mêmes. Ils délèguent cette responsabilité à AgentModule. Ce sont les consommateurs du système. AgentModule est le moteur qu'ils partagent.

J'ai tracé les premières flèches par réflexe. EvaluationModuleAgentModule : évident. AgentModuleVectorStoreModule : logique. Puis je me suis posé la question inverse : est-ce qu'AgentModule pourrait avoir besoin de quelque chose d'EvaluationModule ?

La réponse peut sembler évidente mais la poser est nécessaire, car elle est révélatrice.

Si AgentModule connaît EvaluationModule, il ne peut plus servir l'agent d'assistance. Il ne peut plus servir l'agent de progression. Il est spécialisé. Il perd précisément ce qui lui donnait de la valeur : son ignorance de ce qu'il sert. Retourner une flèche, c'est spécialiser le module qui était générique, souvent sans que personne ne le remarque.

La règle s'est imposée : les dépendances vont du général vers le spécifique, jamais en sens inverse. EvaluationModule sait ce qu'est une évaluation. AgentModule sait ce qu'est une boucle. Le premier a besoin du second. Le second n'a aucune raison de savoir ce qu'est une évaluation.

Le graphe qui suit fait apparaître un module pas encore mentionné : DocumentProcessingModule. Son rôle est d'ingérer les dépôts de code des candidats, de les découper et de les indexer dans VectorStoreModule. Il ne consomme pas AgentModule : il prépare les données que les tools de la boucle vont lire. C'est pourquoi ses flèches vont vers VectorStoreModule et shared/, non vers AgentModule.

dependencies

Les flèches ne sont pas cosmétiques. Elles disent qui peut changer sans avertir qui. VectorStoreModule peut migrer d'implémentation, modifier sa stratégie de cache, restructurer ses indexes, sans qu'AgentModule en soit informé, tant que les interfaces restent stables. L'isolement n'est pas une conséquence du découpage : c'est la direction des flèches qui le produit.

EvaluationModule et CompetencyModule sont au même niveau d'usage : deux domaines qui consomment AgentModule indépendamment, sans se connaître l'un l'autre. Ils communiquent à travers le micro-service graph (arangoDB), pas à travers des interfaces NestJS.

Retour de R&D : en NestJS, un module déclare ce qu'il exporte : les providers qu'il rend accessibles aux modules qui l'importent. Un provider non exporté est invisible de l'extérieur, même si le module est importé. Cette encapsulation n'est pas une convention : elle est vérifiée par le framework au démarrage. Tenter d'injecter un provider d'un module qui ne l'exporte pas lève une erreur immédiate. C'est ce qui rend les règles de dépendance non seulement souhaitables, mais contraignantes.

L'assemblage dans l'assemblage

AgentModule est lui-même un assemblage. Il agrège quatre sous-modules aux responsabilités distinctes, avec leurs propres règles de dépendance internes. Le graphe qui suit ne montre que l'intérieur d'AgentModule : EvaluationModule et CompetencyModule ont disparu du champ. Ce qu'on voit ici, c'est ce qu'AgentModule cache au reste du système. Les flèches obéissent aux mêmes règles qu'au niveau supérieur : elles vont toujours du consommateur vers ce qu'il consomme, jamais en sens inverse.

agent_deep

LLMModule et SafetyModule n'ont aucune dépendance interne : ils sont les feuilles du graphe. ToolsModule importe VectorStoreModule : les tools qui lisent du code passent par l'interface IVectorStoreCache avant d'atteindre Qdrant.

La dépendance PromptModuleToolsModule est la moins évidente. PromptModule n'exécute pas de tools : il en génère la documentation. L'agent doit savoir quels outils il peut appeler, et cette description est compilée au moment de l'assemblage du prompt. PromptModule ne peut pas inventer ce texte : il le demande à ToolsModule, qui le produit à partir des enregistrements réels.

Ce couplage dit quelque chose d'important sur ce que "contexte" signifie dans une boucle agentique : comment un prompt n'est pas un template statique mais une composition dynamique dont les tools sont une composante à part entière. C'est un sujet qui mérite peut-être son propre épisode (je m'en réserve encore la réflexion).

Ce qu'un nom interdit

Un graphe de dépendances bien orienté ne se contente pas d'organiser le code. Il rend certaines choses impossibles.

Il est impossible d'injecter accidentellement un provider d'AgentModule dans VectorStoreModule (ce dernier ne connaît pas AgentModule). Il est impossible de créer une dépendance circulaire sans que NestJS le détecte au démarrage. Il est impossible d'ignorer le cycle de vie d'un service : le scope est déclaré, visible, et ses conséquences sont vérifiées par le framework.

EvaluationModule sait qu'il dépend d'AgentModule. Il ne sait pas ce qu'AgentModule contient. C'est exactement ce qu'il doit savoir.

Un module nommé avec précision a une frontière claire. Et une frontière claire génère des contraintes naturelles sur ce qui peut la traverser. Ce n'est pas un effet secondaire du nommage. C'en est le but.


La prochaine fois : La structure est posée : modules nommés, frontières tracées, dépendances orientées. Mais la structure ne dit rien sur ce qui se passe à l'intérieur quand plusieurs règles tournent en parallèle. AgentService porte l'historique d'une boucle : si deux évaluations partagent la même instance, l'une contamine l'autre. La prochaine fois, on entre dans les scopes, et dans ce que signifie concrètement "une instance par règle".