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.

- 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).
- 27 oct. 2026OpenAI détaille le travail de ses FDE : 90% du code sur mesure a disparu au profit des skills Codex
- 27 oct. 2026Anthropic : les experts suivent l'avis de l'agent dans 86% des choix litigieux
- 27 oct. 2026Anthropic détaille comment elle relit un code écrit à 90% par Claude
- 27 oct. 2026Anthropic appelle à replier les sous-agents : une seule boucle avec des skills bat les orchestrateurs
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
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.
L'essentiel de l'IA — une fois par semaine
Sept actus qui ont vraiment compté, choisies à la main. Sans bruit ni communiqués.
C'est fait ! Vérifiez votre boîte mail pour la confirmation.