Accueil / Articles

Quatre types de mémoire nécessaires à votre agent de codage (et un seul est déjà implémenté).

Les graphes de code fournissent une structure aux agents, mais les ingénieurs ont également besoin de raisons, d’historique et de procédures. Une présentation de quatre types de mémoire, de niveaux de confiance et de la manière de les combiner.

Éditeur : REACTAPP.TOP11 min de lecture2627 mots

Sources: Your Coding Agent Is Always the New Guy on Monday.

Récemment, une famille d’outils est apparue dont l’objectif est d’empêcher les agents de codage basés sur l’IA de fouiller dans les dépôts. Ils analysent le code à l’aide de Tree-sitter, stockent les symboles et les liens dans SQLite et les fournissent via MCP, ou bien ils exécutent des serveurs de langage pour offrir à l’agent des sémantiques véritables d’IDE. Les gains rapportés sont importants et l’idée est solide.

Tous répondent au même type de questions : qui appelle cela, quel est ce type, qu’est-ce qui pourrait être perturbé par un changement.

Observons maintenant un ingénieur expérimenté face à un changement dans un domaine inconnu. Trouver les fonctions appelantes prend environ trente secondes. Ensuite, il passe une heure à se poser des questions qui ne relèvent pas de cette catégorie : pourquoi a-t-on construit cela de cette manière ? Que s’est-il passé la dernière fois que quelqu’un a essayé cela ? Quelle est la procédure habituelle ici ? Dois-je régénérer avant d’exécuter les tests ?

Quatre questions, quatre types différents de mémoire. Presque toutes les configurations d’agents actuelles en implémentent une et l’appellent contexte.

Le test qui les distingue

Avant la taxinomie vient le test qui la génère. Pour toute relation que vous souhaitez que l’agent connaisse, demandez si un artefact formel l’enregistre déjà et si un outil déterministe peut le lire.

Si c’est le cas, analysez-le. Cet artefact peut être un compilateur, un vérificateur de types, un lecteur de bytecode ou un suivi distribué. Utiliser un modèle de langage pour extraire des relations déjà connues par un analyseur est plus lent, plus coûteux et moins précis, et cela élimine les données de référence que vous possédez déjà.

Si ce n’est pas le cas, la relation n’existe que dans le texte et le jugement humain, et l’extraction par modèle est le seul moyen de la obtenir.

Il ne s’agit pas d’une séparation entre le code et les documents. Il s’agit plutôt de savoir s’une grammaire existe. La séparation est suffisamment claire pour que différents types de connaissances nécessitent une extraction différente, un rythme de mise à jour différent ainsi que des notions de fiabilité distinctes. Traiter tout cela à l’aide d’un seul pipeline signifie que au moins un élément sera traité de manière incorrecte.

1. Mémoire structurée : impact d’un changement

C’est la connaissance de l’agent concernant le code lui-même. C’est la mémoire que tout le monde construit, et l’erreur courante consiste à la nourrir uniquement à partir d’une seule source.

  • Tree-sitter offre une portée large : un inventaire des nœuds et des segments, prise en charge de plus de 40 langages, tolérance aux erreurs et mesure du temps en millisecondes par fichier. Il ne résout pas les types. Il peut indiquer que la ligne 3 contient une appel à une fonction nommée execute, mais pas laquelle. La correspondance des noms ne constitue pas une analyse au niveau d’un compilateur, et ce défaut se fait particulièrement sentir dans les codes polymorphes, hiérarchisés ou utilisant l’injection de dépendances.
  • LSP assure la correction grâce à textDocument/definition, references et implementation, soutenu par un véritable vérificateur de types. Son démarrage est lent, il nécessite une compilation fonctionnelle et doit être exclu du chemin des requêtes, car un serveur de langage non préchauffé détruit tout éventuel budget de latence. Il doit être exécuté en arrière-plan en tant qu’outil d’enrichissement.
  • Bytecode révèle du code qui n’existe que lorsque la compilation a eu lieu : les requêtes dérivées de Spring Data, les proxies CGLIB, les mappeurs MapStruct, tout ce que l’outil de traitement d’annotations a généré. Le code source ne montre rien de tout cela. target/classes en montre tout.
  • Sortie AOT ou /actuator/beans montrent comment l’injection de dépendances est organisée, en d’autres termes quelle classe concrète remplit quelle interface dans quel profil. La lecture du code source ne permet pas de le savoir, car la réponse n’y figure pas.
  • Traces distribuées permettent de suivre les flux entre services et les limites asynchrones : du contrôleur via HTTP vers un autre service, du producteur vers un sujet vers le consommateur. Chaque serveur de langage s’arrête à la frontière d’un module, et les systèmes distribués ne consistent en rien d’autre. Si vous utilisez OpenTelemetry, vous collectez déjà un graphe des appels entre services avant de l’effacer.
  • La décision de conception la plus importante est d’attribuer un niveau à chaque arête :

    • resolved : un vérificateur de type l’a confirmé, il est donc sûr d’agir en conséquence.
    • observed : une exécution réelle l’a généré, ce qui constitue la vérité fondamentale pour les chemins concernés.
    • inferred : correspondance basée uniquement sur le nom. Probablement correct mais non vérifié.
    • ambiguous : plusieurs candidats existent. Retournez-les tous sans en choisir un.
    • dynamic : calculé en temps de exécution et indécidable, il faut donc le consigner comme indécidable sans l’ignorer.

    Si l’on donne à un agent un graphe sans niveaux, il traitera chaque arête comme une vérité absolue et procédera ensuite à des modifications sur une simple supposition. En lui indiquant qu’une arête est inferred, il la vérifiera avant d’agir. Cet élément constitue la différence entre un système qui induit en erreur discrètement et un système qui gère les erreurs de manière plus appropriée.

    Une conséquence que peu d’implémentations mentionnent explicitement : dans une base de code en couches Spring ou Django, l’analyse des impacts fournit une borne inférieure, jamais une évaluation complète. Affirmer « 17 appels, 3 dispatchs d’interfaces non résolus, 1 méthode générée » est fiable. Se contenter d’écrire « 17 appels » est trompeur.

    Le taux de désabonnement est mesuré par heure et une reconstruction ne prend que quelques secondes.

    2. Mémoire des connaissances : les raisons derrière le code

    Cette mémoire contient les raisonnements, et elle se trouve dans des documents que aucun analyseur ne peut lire : des comptes rendus de décision, des analyses post-mortem d’incidents, des threads de demandes de fusion, des tickets, des RFC, des documents de conception ainsi que des messages de commit contenant du contenu réel.

    Considérez un compteur de tentatives de 3 dans un fichier de configuration. Son explication est répartie entre un ADR de 2023, une analyse post-mortem sur des instructions de règlement dupliquées et un thread dans lequel quelqu’un a décrit le SLA d’un partenaire. Le lien entre eux, à savoir une politique mise en place en raison d’un incident qui restreint une intégration, est mentionné dans trois documents, chacun formulé différemment.

    GraphRAG est un bon outil pour cela, et il est utile de savoir ce qu’il fait. Il existe six étapes :

    1. Chunking divise les documents en unités de texte de manière déterministe.
    2. Extraction relève du travail des LLM : un modèle lit chaque chunk et produit des entités et des relations.
    3. Description summarisation relève également du travail des LLM, en fusionnant les mêmes entités décrites de cinquante manières différentes en une seule description.
    4. Le clustering de Leiden ne nécessite aucun modèle. Il divise le graphe en communautés imbriquées en se basant sur la modularité, c’est-à-dire en regroupant des éléments dont les arêtes internes sont nettement plus nombreuses que celles qui les relient à l’extérieur.
    5. Rapports de communautés : il s’agit de résumés générés par des modèles de langage grand public pour chaque communauté, rédigés de bas en haut, les niveaux supérieurs étant construits à partir des rapports inférieurs.
    6. Embedding : il produit des vecteurs qui servent de points d’entrée pour les requêtes.

    Deux termes méritent une attention plus approfondie. Un arête dans ce contexte n’est pas un pointeur textuel ; elle contient une description en prose fournie par le modèle, ainsi qu’un poids et des références sources, de sorte que chaque arête enregistre l’interprétation du modèle d’une phrase. De même, une communauté n’est pas simplement un sous-graphe arbitraire, car un ensemble aléatoire de nœuds peut ne pas partager du tout d’arêtes, laissant des points isolés sans rien à résumer. Leiden sélectionne des clusters dont les arêtes induites sont denses, ce qui permet d’obtenir un rapport cohérent.

    Le avantage réside dans la recherche globale. Au lieu de récupérer les fragments les plus proches de la requête, le système pose la question à chaque rapport de communauté individuellement et combine les réponses partielles. Les propriétés de l’ensemble du corpus deviennent ainsi accessibles : une question concernant les modes de défaillance récurrents dans le processus de règlement trouve une réponse qui n’existe dans aucun rapport d’analyse post-mortem isolé. Le RAG vectoriel classique ne peut pas offrir cela, peu importe à quel point les fragments sont ajustés.

    Le renouvellement a lieu mensuellement, et les reconstitutions sont coûteuses : trois des six étapes consistent en appels à des LLM, de sorte que le coût d’indexation augmente avec la taille du corpus, le nombre de passes de collecte de données et le prix du modèle. Cela est acceptable, car le corpus ne change presque jamais.

    3. Mémoire épisodique : ce qui s’est passé lors des tentatives précédentes

    Celui-ci enregistre l’historique propre de l’agent dans ce répertoire particulier. La dernière modification de SettlementService a provoqué l’échec de trois tests d’allocation. Modifier cette interface sans régénérer les mappers entraîne l’échec de la compilation. Les deux dernières propositions de modification qui ont ignoré la couche de validation ont été rejetées.

    Il possède également une propriété que ni l’index sémantique ni l’autre ne peuvent revendiquer : sa vérité de base est déterministe. Un rapport d’analyse post-mortem contient une affirmation humaine tirée d’un modèle. « L’agent a modifié X et CI est passé en rouge » est un résultat évalué, décidé par le système de compilation. Pour les questions qu’il aborde, cela fait de la mémoire épisodique la source la plus fiable dans ce système.

    Il existe trois points de conception qui sont facilement sujets à erreur :

    • Ignorez GraphRAG ici. Les épisodes suivent une chronologie et ont des résultats. Regrouper par densité des liens est la mauvaise structure ; ce qui est nécessaire, c’est une pondération en fonction de la récente date, de la fréquence ainsi que du succès ou de l’échec.
    • L’éviction est indispensable. Les index sémantiques peuvent se passer de cela, mais pas celui-ci. Une leçon tirée d’une refonte effectuée il y a deux architectures n’est pas seulement dépassée, elle oriente l’agent dans la mauvaise direction.
    • Les erreurs s’accumulent. Supposons que l’agent ait raisonné de manière erronée et ait enregistré le résultat. Celui-ci devient un fait mémorisé que chaque session ultérieure hérite. Conservez le résultat (le CI a échoué, le PR a été rejeté) avec une haute confiance, tandis que la conclusion de l’agent elle-même doit être considérée avec une confiance bien plus faible ; laissez ensuite la conclusion perdre de sa pertinence tout en maintenant le résultat.

    Le taux de désabonnement se calcule par session.

    4. Mémoire procédurale : ce n’est pas du tout un index

    La mémoire procédurale concerne la manière dont les tâches sont accomplies : la commande de construction, la liste de contrôle de révision, l’ordre de migration, le style de l’organisation. L’impulsion première des gens est de rendre ces informations recherchables, mais il convient de résister à cette tendance.

    Tout ce qui est toujours pertinent est mal géré par des systèmes de recherche. Il devrait figurer dans le prompt du système ou dans un fichier AGENTS.md, chargé à chaque session sans aucune recherche préalable. Le placer derrière un système de recherche entraîne des erreurs, car l’agent ne sait pas où chercher, et compter sur lui pour retrouver ces informations ne constitue pas un mécanisme fiable.

    Tout ce qui n’a d’importance que dans certaines situations doit être un outil doté d’un déclencheur. Par exemple, l’exécution du jeu d’intégration devrait être quelque chose que l’agent peut appeler directement, et non un document qu’il doit chercher par lui-même.

    Le cas à surveiller concerne le flux de travail appris. Supposons que l’agent découvre que, dans ce dépôt, les mappeurs doivent être régénérés avant l’exécution des tests, et que cet ordre continue de fonctionner. Il s’agit d’une procédure dérivée d’épisodes. Enregistrez-la en tant que règle candidate avec un compteur de succès, promouvez-la parmi les instructions toujours chargées une fois qu’elle s’est avérée efficace, et conservez les épisodes d’origine afin que la promotion puisse être auditée.

    Ainsi, la mémoire procédurale n’est pas un quatrième index. C’est ce qui reste après le troisième, retiré de la récupération et intégré dans les instructions.

    Le joint : un espace de noms de symboles

    Les trois souvenirs récupérés se rejoignent autour de l’identité d’un symbole, et personne n’a résolu cette partie. Les nœuds structurels utilisent des noms pleinement qualifiés. Les entrées épisodiques font référence au symbole que l’agent a réellement manipulé, ce qui permet une association précise. Les entités de l’index de connaissances sont des chaînes de caractères que le modèle a extraites du texte, telles que "SettlementService", "the settlement service", "the settlement reconciliation path" ou "the counterparty SLA constraint", et leur correspondance avec des symboles réels relève d’une opération de correspondance floue.

    Faire semblant du contraire est précisément l’échec que les différents niveaux existent pour éviter, c’est pourquoi l’association dispose de sa propre hiérarchie :

    • exact : le nom apparaît mot pour mot et fait référence à un seul symbole.
    • strong : une phrase normalisée indique clairement une classe donnée.
  • faible : un concept englobant plusieurs classes dont les limites sont floues. Un ensemble de symboles réapparaît et l’agent les vérifie.
  • non résolu : une contrainte réelle pour laquelle aucun symbole correspondant n’existe nulle part. « L’SLA du partenaire » est une véritable contrainte architecturale que le code ne représente pas.
  • Pour ce dernier niveau, conservez-le, renvoyez-le et ne forcez pas de correspondance. Une contrainte non résolue lisible vaut mieux qu’un lien avec un symbole incorrect, et c’est en forçant des correspondances qu’un agent modifie à tort une classe.

    Ce que l’agent demande réellement

    Une surface de requête peut ressembler à ceci :

    what_breaks(symbol)        → structural: resolved callers, tiered
    why_is_this(symbol)        → knowledge: decisions, incidents, constraints
    what_happened_before(sym)  → episodic: past attempts and their outcomes
    related_by_intent(concept) → resolve a phrase to a symbol set, tiered
    

    Alors se pose une question à laquelle aucun des trois ne peut répondre seul : « Je vais modifier la politique de tentative de résolution. Qu’est-ce qui se brise, pourquoi trois fois, et quelqu’un a-t-il déjà essayé cela ? »

    Les cartes de connaissances relient « politique de tentative » à SettlementService.retry au niveau strong et renvoient l’ADR ainsi que l’analyse post-mortem. Structural prend ce symbole et renvoie ses appels, y compris deux services visibles uniquement dans les traces. Episodic ajoute qu’une modification similaire au dernier trimestre a perturbé les tests d’allocation.

    C’est donc une réponse construite à partir de trois types de preuves, chacune générée par le mécanisme capable de la produire, et chacune étiquetée en fonction du degré de fiabilité attendu.

    Quel en est le coût

    • La plupart des collections de documents sont insuffisantes. Neuf ADR et des commits indiquant « corriger une erreur » ne fournissent rien au index de connaissances sur quoi travailler. Examinez ce que vous avez avant de commencer à développer.
    • La résolution des entités n’est jamais parfaite. L’échelle vous aide à faire avec, mais ne la résout pas. Le niveau weak sera probablement le plus chargé, il faut donc prévoir des vérifications plutôt que de compter sur la fiabilité.
    • Commencez de manière ciblée. Démarrez avec tree-sitter et LSP. Ajoutez le bytecode lorsque le code généré commence à produire des résultats incorrects, ainsi que des traces lorsque les questions traversent plusieurs services. N’ajoutez l’index de connaissances que lorsque le corpus mérite d’être indexé.
  • Personne n’a développé tout cela, ce qui peut être à la fois une opportunité et un avertissement. Les différentes composantes se trouvent au sein de communautés distinctes : les spécialistes de l’intelligence artificielle ne lisent pas les articles sur GraphRAG, ceux qui travaillent avec GraphRAG ne se préoccupent pas des vérificateurs de types, et les experts en architecture mémoire se concentrent principalement sur les assistants de conversation plutôt que sur les bases de code.
  • Pensez à l’intégration d’un nouveau collaborateur

    Disons qu’un ingénieur senior rejoint l’équipe un lundi et obtient uniquement l’accès au répertoire de code, rien de plus. Il peut lire chaque ligne, suivre chaque appel et exécuter les tests. À la fin de la semaine, il est encore plus lent qu’un collègue embauché un an plus tôt, et ce n’est pas dû à ses compétences.

    Le collègue de l’année dernière dispose de trois éléments qui n’ont jamais fait partie du répertoire de code, alors que le nouveau venu ne les a pas.

    La première raison concerne les choix étranges. La tentative de réessai est effectuée trois fois en raison d’un incident survenu en mars, et la solution de contournement pour la réconciliation existe parce que l’API d’un partenaire ne fonctionne pas correctement les vendredis. Aucun commentaire ne l’indique explicitement ; cela figure dans un ADR, un rapport d’analyse post-incident et des discussions.

    Une autre raison est l’histoire du projet. Quelqu’un a tenté de simplifier cette interface au deuxième trimestre, ce qui a provoqué des dysfonctionnements des tests pendant une semaine, ce qui a poussé les gens à la laisser telle quelle.

    La troisième raison est la manière de travailler locale. Il faut régénérer les mappeurs avant d’exécuter les tests. Les propositions de modification qui omettent la validation sont rejetées. Personne ne consulte de document à ce sujet ; c’est simplement ainsi que l’équipe travaille.

    Un agent ne disposant que d’un graphique de code est un nouvel employé embauché un lundi : il ne connaît pas le contenu complet du répertoire, ne comprend pas pourquoi les choses sont faites de cette manière, n’a aucun souvenir des tentatives précédentes et ne connaît pas les particularités locales. Chaque session marque le début d’un nouveau lundi.

    C’est là le problème, et aucun outil n’en est responsable. Un graphe de code répond bien à sa propre question, et les implementations actuelles sont solides. Ce qui manque, c’est que trois des quatre éléments sur lesquels compte un ingénieur compétent ne font absolument pas partie de cet ensemble d’outils, ce qui oblige le quatrième élément à assumer tout le travail.

    La version en une ligne

    En bref : le compilateur gère la structure, car celle-ci est déterministe. Un modèle exprime l’intention, car celle-ci correspond à du texte non structuré. Le système de construction s’appuie sur l’expérience. La procédure est tirée de l’expérience et chargée à l’avance, sans jamais être récupérée ultérieurement.

    Aucun outil de cette catégorie ne commet d’erreur de mémoire. Chacun construit quelque chose puis s’arrête, et rien ne les relie entre eux. Un meilleur graphe n’est pas la solution. La solution réside en quatre mémoires, chacune étant honnête quant à son niveau de fiabilité, mappées sur un espace de noms commun de symboles.

    Les termes de GraphRAG ainsi que les étapes du pipeline suivent l’implémentation de Microsoft Research. Les coûts varient fortement en fonction de la taille du corpus, des paramètres de collecte des données et du choix du modèle ; il est donc conseillé de réaliser des tests sur vos propres données avant de prendre une décision.

    Vérification en quinze minutes

    Choisissez une interface dans votre propre base de code qui dispose de cinq ou plus d’implémentateurs connus, comme PaymentProcessor ou NotificationChannel, dont l’un est connecté via @Autowired et non construit directement. Demandez à votre graphe de code qui l’appelle et comptez les résultats.

    Les benchmarks publiés pour cette catégorie mesurent le nombre de tokens, le nombre d’appels d’outils et la latence. À ce jour, aucun ne vérifie si le graphe d’appels retourné est correct. Dans une base de code dotée d’interfaces déclarées et d’implémentateurs connus, une vérification peut être effectuée en cinq minutes en comparant avec des données de référence déjà disponibles.