Aller au contenu principal

L'autre côté du contrat

Une interface est une promesse. Elle ne dit rien de ce qu'il faut traverser pour la tenir.

Depuis l'épisode cinq, IVectorStoreCache traverse le journal comme une évidence. ToolsModule l'injecte, AgentService en bénéficie sans le savoir, les tools get_file, get_files et search_code l'interrogent avant d'aller chercher dans Qdrant. La frontière a été décrite du côté qui consomme — propre, stable, sans surprise.

Or une interface a deux côtés. Le premier est celui qui appelle. Le second est celui qui répond — celui qui a construit ce qu'on interroge, qui maintient la cohérence de ce qu'on lit, qui garantit que la promesse tient même quand dix agents l'interrogent simultanément. C'est ce second côté que VectorStoreModule habite. Et ce qu'il y a derrière est moins simple que ce que la frontière laissait deviner.

Ce qu'il faut construire avant d'interroger

Avant qu'un agent puisse chercher du code dans Qdrant, il faut que ce code y soit. Ce geste — transformer un dépôt git en collection vectorielle interrogeable — est le premier métier de VectorStoreModule, celui qu'aucun des épisodes précédents n'avait eu besoin de regarder en face.

IndexRepositoryUseCase orchestre ce pipeline. Il reçoit un chemin de dépôt et un identifiant de collection. Ce qu'il produit, c'est une collection Qdrant peuplée — chaque fichier du dépôt découpé en chunks, chaque chunk transformé en vecteur par OpenAIEmbeddingRepository, chaque vecteur inséré dans Qdrant par QdrantVectorStoreRepository. Le dépôt est devenu une mémoire interrogeable.

Il y a un court-circuit dans ce flux que le schéma rend visible mais que je n'ai pas posé par hasard. ContentHashService calcule une signature du dépôt avant d'indexer. CollectionCacheService vérifie si une collection portant cet identifiant existe déjà dans Qdrant avec un hash identique. Si c'est le cas, l'indexation ne se fait pas — la collection est déjà là, déjà valide, déjà prête. Ce n'est pas une optimisation de performance. C'est la fondation sur laquelle repose tout le reste.

L'invariant qui rend tout possible

La cohérence entre plusieurs agents qui lisent la même collection simultanément n'est pas le résultat d'un mécanisme de synchronisation. Elle est le résultat d'une décision de conception plus radicale : une collection Qdrant ne change pas pendant la durée d'une session.

Un dépôt est indexé une fois. Tant que le hash du dépôt ne change pas, la collection reste celle qu'elle était au moment de l'indexation. Aucun agent ne modifie une collection — ils lisent tous depuis le même état figé. La question "est-ce que ce que je lis est cohérent avec ce que l'autre agent lit en ce moment même ?" a une réponse triviale : oui, parce que personne ne modifie quoi que ce soit.

C'est cet invariant d'immutabilité qui rend la stratégie de cache agressive possible. Le TTL de vingt-quatre heures sur les clés Redis n'a pas besoin d'une logique d'invalidation explicite — il n'y a rien à invalider. Un fichier lu par un agent et mis en cache sera le même fichier lu par un autre agent dix minutes plus tard. Le cache ne peut pas devenir incohérent parce que la source elle-même ne change pas.

Pour un développeur junior : dans un système distribué, garantir que plusieurs processus lisent des données cohérentes est l'un des problèmes les plus difficiles. La solution habituelle passe par des verrous, des versions, des mécanismes d'invalidation de cache. Ici, la solution est différente : on évite le problème en rendant les données immuables après indexation. C'est ce qu'on appelle une stratégie immutability-first — plutôt que de gérer la cohérence dynamiquement, on s'arrange pour que les données ne puissent pas devenir incohérentes. C'est plus simple, plus fiable, et bien plus facile à raisonner que n'importe quel protocole de synchronisation. Le compromis : si le dépôt change pendant une évaluation, la collection ne le reflétera pas. Pour une session d'évaluation d'une durée finie, ce compromis est acceptable.

Ce que le cache expose et ce qu'il cache

VectorStoreCacheService implémente IVectorStoreCache — la même interface que ToolsModule injette depuis l'épisode cinq. Mais l'implémenter n'est pas la même chose que l'appeler.

Du côté consommateur, l'interface est deux méthodes : getFile et setFile, getSearchResult et setSearchResult. Simple, direct, sans état visible. Du côté fournisseur, VectorStoreCacheService gère Redis Sentinel — la connexion, les clés structurées, le TTL, le fallback transparent vers Qdrant si Redis est indisponible. Il gère aussi la sérialisation des résultats de recherche — des objets SearchResult sérialisés en JSON dans Redis, désérialisés à la lecture, validés avant d'être retournés.

Le fallback transparent mérite d'être nommé pour ce qu'il est : une décision qui sacrifie la performance pour la disponibilité. Si Redis est indisponible, les agents continuent à fonctionner — plus lentement, chaque lecture allant directement dans Qdrant, mais sans s'arrêter. J'aurais pu choisir de traiter l'indisponibilité Redis comme une erreur fatale. Or Qdrant reste accessible — couper le service pour une dégradation de cache aurait été disproportionné.

La lecture que tout le monde partage

SearchCodeUseCase est le second use case de VectorStoreModule — l'interface de recherche que ToolsModule utilise via IVectorStoreRepository. Il reçoit une requête en langage naturel, la transforme en vecteur via OpenAIEmbeddingRepository, interroge Qdrant, et retourne les chunks les plus proches sémantiquement.

Ce use case est sans état. Il ne sait pas quel agent l'appelle, quelle évaluation est en cours, quelle règle est en train d'être évaluée. Il reçoit une requête et une collection — il retourne des résultats. Cette ignorance est délibérée : SearchCodeUseCase n'a pas à connaître le contexte pour faire son travail. Le contexte est dans l'appelant.

C'est ce que la frontière IVectorStoreRepository garantit dans l'autre sens — non pas seulement que le consommateur est isolé de l'implémentation, mais que le fournisseur est isolé du contexte du consommateur. Les deux côtés du contrat s'ignorent mutuellement, et c'est précisément ce qui rend chacun remplaçable.


La prochaine fois : VectorStoreModule sait indexer un dépôt et en servir la mémoire. Ce qui reste, c'est le pipeline qui prépare ce qu'il indexe — DocumentProcessingModule, l'extraction, et la question de ce qu'on fait des documents qui ne sont pas du code.