Opinion

Mémoire immuable à cinq couches pour un agent IA : pourquoi une seule base vectorielle ne suffira jamais

Vouloir stocker toute la mémoire à long terme d'un agent IA dans un seul index vectoriel échoue, car la recherche exacte par mots-clés, la proximité sémantique, la traversée multi-saut des relations et l'historique des revirements de décision exigent des structures de données physiquement différentes. Un index incrémental SQLite FTS5 avec classement BM25, construit sur 66 995 sessions et 538 956 messages (1,35 Go), a mis fin aux crashs de l'environnement de développement et réduit le temps de démarrage d'une session de 15 secondes à quelques dixièmes de seconde. Pour protéger la mémoire contre la dégradation, trois mécanismes ont été mis en place : une sanitisation déterministe des clés via UUID5, une écriture bitemporelle qui ne supprime jamais les anciens faits, et un cycle de consolidation nocturne avec atténuation selon une distribution de Weibull.

Mémoire immuable à cinq couches pour un agent IA : pourquoi une seule base vectorielle ne suffira jamais
Hamidun News.
Écouter l'article
Fiche synthèse et chiffres clés
ParamètreDonnées
Rédacteur en chef
Volume de l'index épisodique66 995 sessions et 538 956 messages dans une base SQLite FTS5 de 1 353 Mo
Chiffre clé #1Base du « second cerveau » et des embeddings : 10 019 fiches de connaissances structurées et une base `brain.db` de 2,37 Go
Chiffre clé #2Vitesse de démarrage après l'abandon des hooks lourds : latence d'initialisation réduite de 5–15 s à < 0,2 s
Seuils de consolidation nocturnefusion des doublons à une similarité cosinus >= 0,70 et détecteur de contradictions avec un seuil de 0,40, sans appel à un LLM
  • Volume de l'index épisodique : 66 995 sessions et 538 956 messages dans une base SQLite FTS5 de 1 353 Mo
  • Base du « second cerveau » et des embeddings : 10 019 fiches de connaissances structurées et une base `brain.db` de 2,37 Go
  • Vitesse de démarrage après l'abandon des hooks lourds : latence d'initialisation réduite de 5–15 s à < 0,2 s
  • Seuils de consolidation nocturne : fusion des doublons à une similarité cosinus >= 0,70 et détecteur de contradictions avec un seuil de 0,40, sans appel à un LLM

Le point de départ de mon architecture de mémoire a été un crash sec de l'éditeur de code. En tentant d'ouvrir l'historique des conversations avec un agent IA, le processus Extension Host a dépassé sa limite de mémoire et s'est effondré, entraînant le terminal avec lui. En six mois d'utilisation, 2,45 Go de logs bruts JSONL de sessions s'étaient accumulés, et l'extension tentait de lire leurs en-têtes depuis le disque à chaque démarrage. Pire encore, le paramètre par défaut `cleanupPeriodDays=30` supprimait silencieusement les sessions de plus d'un mois — jusqu'à ce que je le remplace par `99999`, une partie de mes archives d'hiver avait déjà disparu.

Un audit a révélé que deux de mes quatre bases de mémoire étaient mortes, car elles nécessitaient un lancement manuel de démons. Mais la vraie découverte a été que la « mémoire d'un agent » recouvre en réalité cinq classes de questions distinctes, chacune nécessitant son propre moteur.

Pourquoi SQLite FTS5 a-t-il battu

Qdrant et grep sur un demi-million de messages ?

Pour la mémoire épisodique (« où avons-nous discuté de la configuration des webhooks il y a trois mois ? »), `grep` sur des gigaoctets de texte était trop lent, et lancer une instance locale de `Qdrant` avec `fastembed` au démarrage d'une session ralentissait le lancement de 5 à 15 secondes tout en consommant des centaines de mégaoctets de RAM.

C'est le moteur SQLite FTS5 intégré à Python, avec classement BM25 et mode `WAL`, qui l'a emporté. L'indexeur `search_chats.py` compare `file_mtime` à la table `sessions` : si l'écart est inférieur à une seconde, le fichier est ignoré.

Les logs bruts de plus de 14 jours et les dumps de plus de 20 Mo sont déplacés vers une archive froide — le répertoire de travail est passé de 2,45 Go à 527 Mo, les crashs ont cessé, et l'index a grossi jusqu'à 66 995 sessions et 538 956 messages (290 544 émis par l'humain et 248 412 par l'assistant) pour un poids de 1 353 Mo. Pour les documents de référence critiques (comme un guide en 20 chapitres compilé à partir de 2 240 messages et de 177 notes vocales transcrites via Deepgram), un système de sauvegarde à 4 niveaux s'applique : un résumé en mémoire de travail, un index dans `MEMORY.md` (strictement limité à 200 lignes et ~22 Ko), une règle de routage, et un instantané vectoriel.

Pourquoi un UUID5 déterministe dans la base vectorielle, et comment

fonctionne la bitemporalité ?

Quand la requête ne contient pas les mots exacts (quelqu'un demande « comment laver les fruits sans produits chimiques » alors que la base contient une note sur le mode alcalin d'un ioniseur à `pH 11,5`), c'est la couche sémantique à base d'embeddings qui prend le relais (dimension 384 en local, 1536 pour les livres). Dans la pratique, les identifiants de documents ressemblent à des chaînes comme `knowledge:42` ou `session/2026-04-10/msg-17`, parfois en cyrillique, mais le moteur vectoriel Qdrant exige comme `point_id` soit un entier, soit un UUID strict. Un `uuid4()` aléatoire génère des doublons à chaque réindexation ; nous avons donc introduit un `uuid5` déterministe basé sur l'espace de noms DNS :

```python import uuid

def to_deterministic_point_id(raw_doc_id: str) -> str: # Transforme toute chaîne contenant du cyrillique ou des caractères spéciaux en UUID5 ASCII stable return str(uuid.uuid5(uuid.NAMESPACE_DNS, raw_doc_id))

point_id = to_deterministic_point_id(doc_id) payload = {**metadata, "doc_id": doc_id, "content": text_chunk} ```

Une même chaîne produit toujours un UUID identique, ce qui transforme chaque réécriture en un `upsert` idempotent, tandis que la clé d'origine reste conservée dans le `payload`.

Pour que l'agent se souvienne de l'historique des revirements de décision (« pourquoi avons-nous changé de moteur au printemps ? »), nous avons repris une idée de Datomic et interdit tout `UPDATE` qui effacerait le passé. Chaque relation possède des champs `valid_from`, `valid_to` et `ingested_at`, et la base du second cerveau ajoute des compteurs d'utilisation `use_count` et `last_used`. Lorsqu'une décision devient obsolète, l'ancienne arête reçoit une valeur `valid_to`, et une nouvelle entrée est insérée à côté, reliée par un champ `supersedes`.

Comment un « sommeil algorithmique » basé sur une courbe de

Weibull nettoie-t-il la mémoire des contradictions ?

Pour éviter que la base ne se transforme en dépotoir, un pipeline de consolidation en arrière-plan (`dream` et les modules `sleeptime`) effectue périodiquement un nettoyage en trois étapes :

1. Clustering des doublons (SHMR) : une matrice de similarité cosinus sur des embeddings de dimension 1024 (`Qwen3-Embedding-0.6B` dans LanceDB) fusionne les entrées dont la similarité est `>= 0,70`. 2. Atténuation sélective selon une distribution de Weibull : les observations éphémères (`observation`) s'atténuent en 336 heures (~2 semaines), les faits opérationnels en 2 160 heures, tandis que les préférences personnelles (`preference`), les procédures et les schémas d'erreurs (`error_pattern`) sont protégés par une liste `EXEMPT_TYPES`. 3. Détection de contradictions sans LLM : un scanner déterministe recherche les conflits sur une fenêtre de 7 jours, à partir de signaux pondérés (`negation: 0,35`, `antonym: 0,25`, `numeric: 0,25`, `temporal: 0,15`), avec un seuil de 0,40.

Ce schéma de mémoire à cinq couches (SQLite FTS5, LanceDB et graphe temporel) est intégré au cœur de [Nomad AI](https://nomadnet.ai) et de la plateforme cloud [agent.nomadnet.ai](https://agent.nomadnet.ai). Une formation sur l'ingénierie de la mémoire est disponible sur [Hamidun Academy](https://academy.hamidun.com) (essai de 72 heures), et les scripts d'indexation sont hébergés dans la bibliothèque [Vibe Club](https://hamidun.com/club).

Dossier

Questions clés sur cet article

Pourquoi SQLite FTS5 a-t-il battu Qdrant et grep sur un demi-million de messages ?

Pour la mémoire épisodique (« où avons-nous discuté de la configuration des webhooks il y a trois mois ? »), `grep` sur des gigaoctets de texte était trop lent, et lancer une instance locale de `Qdrant` avec `fastembed` au démarrage d'une session ralentissait le lancement de 5 à 15 secondes tout en consommant des centaines de mégaoctets de RAM. C'est le moteur SQLite FTS5 intégré à Python, avec classement BM25 et mode `WAL`, qui l'a emporté. L'indexeur `search_chats.py` compare `file_mtime` à la table `sessions` : si l'écart est inférieur à une seconde, le fichier est ignor

Pourquoi un UUID5 déterministe dans la base vectorielle, et comment fonctionne la bitemporalité ?

Quand la requête ne contient pas les mots exacts (quelqu'un demande « comment laver les fruits sans produits chimiques » alors que la base contient une note sur le mode alcalin d'un ioniseur à `pH 11,5`), c'est la couche sémantique à base d'embeddings qui prend le relais (dimension 384 en local, 1536 pour les livres). Dans la pratique, les identifiants de documents ressemblent à des chaînes comme `knowledge:42` ou `session/2026-04-10/msg-17`, parfois en cyrillique, mais le moteur vectoriel Qdrant exige comme `point_id` soit un entier, soit un UUID strict. Un `uuid4()` aléat

ZK
Zhemal Khamidun — CPO d'Alpina Digital, product manager d'AlpinaGPT. Conçoit des produits IA depuis 2019. Hamidun News est un projet personnel devenu un agrégateur en production avec plus de 6 000 lecteurs.
✦ Autonomous AI Execution · Nomad Agent

Delegate Deep Research, Coding & Multi-Step Workflows to Nomad Agent

Deploy autonomous task execution with Nomad Agent (agent.nomadnet.ai) — automated web research, data synthesis, code generation, and MCP tool orchestration.

Launch Nomad Agent →Nomad AI Workspace
Qu'en pensez-vous ?
Chargement des commentaires…