Architetture di memoria
Progetto pratico per memoria AI durevole: tracce episodiche, richiamo semantico, recovery deterministica, compressione e valutazione del retrieval.
Perché conta
Il primo sistema che ho costruito con memoria AI a lunga durata è fallito in un modo noto: poteva recuperare qualcosa di plausibile, ma non riuscivo a spiegare perché quel record esistesse, quale versione della fonte lo avesse prodotto o se un riavvio avrebbe preservato lo stesso risultato. Il modello sembrava ricordare. Il sistema non disponeva di stato persistente.
Ora tratto la memoria come un insieme di prodotti dati con contratti diversi, non come una trascrizione della conversazione più un indice vettoriale. Un'architettura utile separa almeno quattro aspetti:
- Working memory conserva il contesto delimitato assemblato per una richiesta.
- Episodic memory registra eventi immutabili: osservazioni, risultati di tool, decisioni dell'utente e azioni del sistema.
- Semantic memory memorizza affermazioni o entità normalizzate che possono essere riutilizzate tra episodi.
- Derived retrieval state contiene chunk, embedding, indici lessicali, riepiloghi e feature di ranking.
Questa separazione supporta le proprietà che mi interessano di più: stato persistente, recovery deterministica, eval riproducibili e costi sotto controllo. Rende inoltre gestibili eliminazione, correzione e re-indexing. Posso rimuovere una rappresentazione derivata e ricostruirla dal suo record sorgente. Non posso farlo in sicurezza quando l'unico record è un riepilogo sovrascritto o un payload vettoriale opaco.
La memoria è particolarmente rilevante in una pipeline di retrieval-augmented generation in produzione. Uso la stessa disciplina descritta nella progettazione dei miei sistemi RAG in produzione: i dati sorgente restano autorevoli, gli artifact di retrieval sono versionati esplicitamente e l'output generato non diventa evidenza soltanto perché è stato riscritto nello storage.
Non presumo che una finestra di contesto più ampia risolva la memoria. Una finestra più ampia cambia quanto materiale posso presentare a un modello in una richiesta. Non fornisce provenienza dei record, regole di retention, cronologia delle correzioni o ricostruzione deterministica. Queste sono responsabilità dello storage e del data model.
Cosa fallisce in produzione
La modalità di fallimento più dannosa che ho osservato è confondere l'evento con la sua interpretazione. Consideriamo un messaggio utente che dice: “Usa la policy di retention rivista.” Un episodio dovrebbe preservare il messaggio grezzo, il suo timestamp, il suo ambito tenant e conversazione e la versione della policy disponibile in quel momento. Un record semantico può in seguito affermare che una determinata policy è preferita. Tale affermazione necessita di un puntatore all'episodio, di una versione di estrazione, di uno stato di confidenza o revisione e di un intervallo di validità. Senza questi campi, una correzione successiva diventa una sovrascrittura senza spiegazione.
Un altro errore consiste nell'usare il vector store come system of record. Un embedding è un artifact derivato. Dipende dal testo sorgente, dai confini dei chunk, dalla normalizzazione, dal modello di embedding, dalla configurazione del modello e talvolta dal comportamento lato provider. Se uno qualsiasi di questi elementi cambia, il retrieval può cambiare anche quando il contenuto leggibile dall'uomo non è cambiato. Memorizzo questi input con il record derivato in modo da poter identificare esattamente cosa deve essere ricostruito.
Evito anche una singola tabella “memory” indifferenziata. Tende ad accumulare oggetti incompatibili: turni grezzi, fatti estratti, riepiloghi, output di tool ed embedding. I requisiti di retention e controllo degli accessi diventano quindi difficili da applicare in modo coerente. Un risultato effimero di un tool può meritare un breve time-to-live, mentre una preferenza approvata dall'utente può richiedere una traccia di audit persistente.
La compressione crea un fallimento più silenzioso. I riepiloghi riducono il costo del retrieval, ma ogni riepilogo scarta distinzioni. Se riepilogo ripetutamente dei riepiloghi, il sistema può preservare una narrazione fluida perdendo date, eccezioni, incertezza e confini delle fonti. Tratto la compressione come una vista derivata con un insieme di input dichiarato e un prompt o algoritmo versionato. Conservo gli episodi sorgente salvo che la policy ne richieda la rimozione.
Infine, ho imparato a non valutare la memoria soltanto attraverso la qualità soggettiva della chat. Un modello può produrre una risposta utile dopo aver recuperato il record sbagliato. Questa è una questione di qualità dell'output, non una prova che il retrieval abbia funzionato. Misuro il retrieval in modo indipendente prima di chiedere a un modello di sintetizzare una risposta.
Un design della memoria persistente
Inizio con un event log append-only. Ogni evento riceve un identificatore stabile al momento dell'ingestion. Le correzioni sono nuovi eventi che sostituiscono un record precedente; non modifico in-place il payload storico. La vista corrente viene materializzata dalla sequenza di eventi.
Uso un relational store per i metadati autorevoli e i riferimenti ai payload. Per un deployment piccolo, SQLite può essere un ragionevole layer locale di durabilità; la sua documentazione sul write-ahead logging descrive lettori concorrenti con uno scrittore, osservando anche che può esserci un solo scrittore alla volta in modalità WAL. Questo vincolo influenza il mio design di ingestion: raggruppo le scritture, mantengo brevi le transazioni ed evito di trattare un database locale come una coda multi-writer illimitata.
Una forma di record semplificata è questa:
{
"event_id": "evt_01J7X6A7YV9ZX",
"tenant_id": "tenant_42",
"scope": {
"user_id": "user_18",
"conversation_id": "conv_993"
},
"kind": "user_message",
"occurred_at": "2025-03-08T10:14:21Z",
"payload_ref": "blob://events/evt_01J7X6A7YV9ZX.json",
"content_sha256": "a2d4...",
"schema_version": 3,
"ingest_run_id": "run_01J7X68N",
"supersedes": null,
"retention_class": "standard"
}
Uso l'identificatore dell'evento come ancora per ogni artifact successivo. Un chunk fa riferimento a uno o più identificatori di evento e registra i suoi confini in caratteri o token. Un embedding fa riferimento al chunk e registra l'identificatore del modello di embedding. Un'asserzione semantica fa riferimento agli eventi e ai chunk da cui è stata estratta. Un riepilogo fa riferimento all'esatto elenco ordinato degli input che ha compresso.
Questa lineage è più preziosa di un campo informale “created at”. Rende il recovery un attraversamento di grafi: partendo da un artifact, posso raggiungere gli eventi sorgente; partendo da un evento sorgente, posso identificare ogni artifact interessato da eliminazione o correzione.
Per il richiamo semantico, separo la candidate generation dall'assemblaggio finale del contesto. La candidate generation può combinare ricerca lessicale, similarità vettoriale, recency, filtri di scope e collegamenti espliciti tra entità. Mantengo distinti i metodi nei log anche se in seguito vengono fusi in un'unica lista classificata. Questo mi permette di capire se una memoria è stata trovata per formulazione esatta, similarità semantica, un vincolo locale alla conversazione o una relazione diretta.
Applico filtri rigorosi prima del ranking. Isolamento tenant, scope utente, stato di eliminazione, stato di retention e limiti temporali sono condizioni di idoneità, non feature di rilevanza soft. Un record molto simile proveniente dallo scope sbagliato non è un quasi errore; è un difetto di autorizzazione.
Per la compressione, mantengo diversi livelli invece di un unico riepilogo canonico:
- Episode summaries descrivono un'interazione delimitata e mantengono collegamenti a tutti gli eventi sorgente.
- Topic summaries combinano episode summaries approvati per un argomento e un intervallo temporale denominati.
- Working-memory briefs sono viste specifiche per richiesta generate da evidenze selezionate.
Persisto soltanto i primi due, ed entrambi sono ricostruibili. Considero il working-memory brief eliminabile. Il suo ruolo è rendere efficiente una singola invocazione del modello, non diventare verità a lungo termine.
Versiono anche le trasformazioni deterministiche. Quando mi serve una content key stabile, serializzo dati strutturati normalizzati secondo uno schema di canonicalization definito prima di calcolare l'hash. RFC 8785 specifica uno schema di JSON canonicalization destinato a produrre rappresentazioni JSON deterministiche adatte a operazioni crittografiche come l'hashing. Non sostengo che JSON canonico risolva l'equivalenza semantica; impedisce soltanto che differenze di serializzazione irrilevanti generino lavoro duplicato non necessario.
import hashlib
import json
def stable_key(document: dict) -> str:
canonical = json.dumps(
document,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
La funzione precedente è appropriata solo se il mio schema definisce il significato di campi mancanti, array, rappresentazioni numeriche e normalizzazione Unicode. Rendo esplicite queste decisioni nel contratto di ingestion. Altrimenti, un hash stabile può creare una falsa sensazione di determinismo.
Come lo verifico
Verifico il recovery prima di ottimizzare la qualità del retrieval. Il mio test di recovery inizia con un fixture di eventi noto, esegue ingestion e derivazione, elimina tutto lo stato derivato, lo ricostruisce dagli eventi immutabili e confronta i manifest risultanti. Mi aspetto identificatori stabili, collegamenti alle fonti, versioni delle trasformazioni e content key. Se gli embedding non sono riproducibili bitwise nell'ambiente che ho scelto, registro questo fatto e confronto le parti deterministiche: appartenenza alle fonti, confini dei chunk, filtri di idoneità e configurazione del modello.
Mantengo un set di valutazione separato dal traffico live. Ogni caso ha una query, uno scope, un cutoff time, identificatori di evidenza attesi, identificatori proibiti quando utili e una spiegazione del motivo per cui l'evidenza è rilevante. L'oggetto di test è la richiesta di retrieval, non soltanto la risposta finale.
{
"case_id": "mem_eval_014",
"query": "Which retention policy applies to export logs?",
"scope": { "tenant_id": "tenant_42", "user_id": "user_18" },
"as_of": "2025-03-01T00:00:00Z",
"relevant_event_ids": ["evt_policy_07", "evt_decision_12"],
"forbidden_event_ids": ["evt_other_tenant_03"],
"notes": "The later approval supersedes the draft."
}
Per ogni run, salvo il manifest completo del retrieval: versione dell'evaluator, identificatore dello snapshot del corpus, versione della normalizzazione della query, filtri, liste di candidati per retriever, input del ranking, contesto selezionato e random seed quando presente. Non faccio affidamento su una dashboard che conserva soltanto punteggi aggregati. Una regressione del punteggio senza un manifest riproducibile è difficile da diagnosticare.
Calcolo prima misure semplici. Il recall a un cutoff fisso mi dice se l'evidenza attesa appare nel set di candidati. La precision a quel cutoff mi dice quanto materiale non correlato sto chiedendo ai componenti downstream di ispezionare. Traccio anche una metrica di policy: il tasso con cui vengono restituiti record proibiti. Questa metrica ha una gravità diversa rispetto a un normale errore di rilevanza.
Poi ispeziono slice che rappresentano rischio operativo: record ingeriti di recente, record vecchi, conversazioni lunghe, fatti corretti, contenuti multilingue quando applicabile, record vicini ai confini di retention e query con nomi di entità ambigui. Le metriche aggregate possono nascondere una regressione concentrata in uno di questi slice.
Il controllo dei costi fa parte dello stesso ciclo di valutazione. Misuro record sottoposti a scansione, candidati prodotti, input del reranker, byte di contesto e lavoro di derivazione per ogni evento appena ingerito. Preferisco il retrieval a stadi: filtri economici e sparse retrieval restringono il set di candidati prima di lavoro più costoso su embedding o modelli. Le soglie esatte dipendono dal workload, quindi le regolo rispetto al corpus di valutazione fisso anziché adottare un numero universale.
Cosa non farei di nuovo
Non persistirei ogni pensiero generato dal modello come memoria. Il testo generato può essere un artifact di lavoro utile, ma memorizzarlo come fatto senza provenienza crea un ciclo di feedback in cui un'inferenza precedente sembra evidenza sorgente. Persisto gli output del modello soltanto quando il prodotto richiede un record di audit, e li etichetto come artifact generati con i loro input e la configurazione del modello.
Non eliminerei gli episodi grezzi immediatamente dopo aver generato riepiloghi. Se la policy di storage consente la retention, gli episodi sorgente sono ciò che mi permette di riparare un summarizer difettoso, ricostruire una strategia di chunking modificata o rispondere a una contestazione sulla provenienza. Se la policy richiede l'eliminazione, propago tale eliminazione attraverso il grafo di lineage e accetto che gli artifact downstream debbano essere rimossi o ricostruiti dai dati rimanenti consentiti.
Non nasconderei il retrieval dietro un unico metodo di convenienza come memory.search(query). Questa interfaccia è utile nel call site, ma l'implementazione deve esporre filtri, fonti, versioni e decisioni di ranking attraverso log e manifest di valutazione. L'observability non è opzionale quando ci si aspetta che il sistema ricordi nel tempo.
Soprattutto, non descriverei la memoria come una funzionalità del solo modello. Nei sistemi che costruisco, la memoria è dato persistente con regole esplicite di lifecycle, indici derivati che possono essere scartati e rigenerati e comportamento di retrieval che può essere riprodotto. Questo inquadramento ha reso i fallimenti più circoscritti, il recovery più prevedibile e il costo operativo più semplice da analizzare.
FAQ
Perché eventi e artifact di memoria derivati dovrebbero essere separati?
Mantengo gli eventi immutabili separati da chunk, embedding, riepiloghi e asserzioni semantiche perché le rappresentazioni derivate possono essere rimosse e ricostruite dai record sorgente. Questo rende gestibili correzione, eliminazione, re-indexing e ispezione della provenienza.
Perché un vector store non è un system of record?
Tratto un embedding come artifact derivato perché dipende dal testo sorgente, dai confini dei chunk, dalla normalizzazione, dalla configurazione del modello e talvolta dal comportamento del provider. Conservo questi input per poter identificare cosa deve essere ricostruito quando il retrieval cambia.
Come rendo deterministico il recovery della memoria?
Inizio con un event log append-only e identificatori di ingestion stabili. Versiono le trasformazioni e preservo la lineage da ogni artifact ai suoi eventi sorgente. Poi elimino lo stato derivato, lo ricostruisco e confronto i manifest per i campi deterministici.
Cosa dovrebbe includere una valutazione del memory retrieval?
Valuto la richiesta di retrieval usando un set fisso separato di query, scope, cutoff time, evidenze attese e record proibiti utili. Per ogni run, salvo filtri, liste di candidati, input del ranking, contesto selezionato, versioni e qualsiasi random seed.
Come controllo il costo del retrieval senza indebolire i filtri di policy?
Applico vincoli di tenant, utente, eliminazione, retention e tempo prima del ranking perché sono condizioni di idoneità. Poi uso retrieval a stadi, restringendo i candidati con filtri economici e sparse retrieval prima di lavoro più costoso su embedding o modelli.
Articoli correlati
Motori di workflow AI per sistemi durevoli e auditabili
Design pratico per motori di workflow AI: stato persistente, recovery deterministica, retry, audit trail, eval riproducibili e costi sotto controllo.
17 ago 202612 min di lettura#AI workflows#durable state#retries#audit trails#evals#orchestrationRAG: chunking e re-ranking prima degli embedding
Un framework per diagnosticare chunking, re-ranking ed embedding nelle pipeline RAG.
20 dic 202412 min di lettura#RAG#Retrieval#LLM#ProductionLezioni dalla gestione delle chiavi bancarie per gli harness di eval degli agenti
Cinque discipline della sicurezza bancaria: stato persistente, recovery deterministica, doppio controllo e audit trail per valutare agenti LLM.
18 apr 20267 min di lettura#Agent Evals#Verifiable Systems#LLM Production#Banking#MCP