Introduzione: La Memoria come Fondamento dell'Intelligenza
Un Large Language Model senza memoria è come un esperto con amnesia: brillante nel rispondere a singole domande, ma incapace di costruire un rapporto continuativo, di imparare dalle interazioni passate o di accumulare conoscenza nel tempo. La memoria è ciò che trasforma un LLM stateless in un agente persistente, capace di ricordare chi sei, cosa avete discusso e quali decisioni avete preso insieme.
Senza un sistema di memoria, ogni interazione riparte da zero. L'agente non sa che ieri hai corretto un bug nel modulo di autenticazione, non ricorda che preferisci Python a JavaScript, non ha idea che il progetto su cui stai lavorando ha una deadline tra due settimane. Con la memoria, invece, l'agente diventa un vero collaboratore: accumula contesto, riconosce pattern ricorrenti e migliora le sue risposte nel tempo.
In questo articolo esploreremo i sistemi di memoria per agenti AI, dalle strategie più semplici come la conversation history fino agli approcci avanzati con vector embeddings, knowledge graphs e il pattern ibrido Mem0 che sta emergendo come best practice nel 2026. Analizzeremo i trade-off tra completezza e costo, tra accuratezza e latenza, fornendo pseudocodice e architetture concrete per implementare ogni approccio.
Cosa Imparerai in Questo Articolo
- I quattro tipi di memoria negli agenti AI: sensory, short-term, long-term ed episodic
- Strategie di gestione della conversation history: sliding window, summarization, importance sampling
- Come funzionano i vector embeddings e il pattern RAG (Retrieval-Augmented Generation)
- Knowledge graphs con Neo4j per il ragionamento relazionale
- Il pattern ibrido Mem0: vector store + knowledge graph combinati
- Ottimizzazione della context window e budget di token
- Benchmarking e trade-off tra i diversi approcci di memoria
Tipi di Memoria negli Agenti AI
La ricerca sui sistemi di memoria per agenti AI si ispira alla psicologia cognitiva umana, adattando concetti come memoria a breve e lungo termine al contesto dei modelli linguistici. Possiamo identificare quattro tipi fondamentali di memoria, ciascuno con caratteristiche e casi d'uso distinti.
I Quattro Tipi di Memoria
| Tipo | Descrizione | Durata | Esempio |
|---|---|---|---|
| Sensory Memory | Input immediato: il prompt corrente e i risultati dei tool appena eseguiti | Singola iterazione | Il messaggio dell'utente, l'output di una query SQL |
| Short-term Memory | Contesto della sessione corrente: la conversation history accumulata | Singola sessione | I messaggi precedenti nella chat, le decisioni prese |
| Long-term Memory | Conoscenza persistente tra sessioni diverse, salvata su storage esterno | Permanente | Preferenze utente, documenti aziendali, codice sorgente |
| Episodic Memory | Ricordi specifici di interazioni passate con contesto relazionale | Permanente | "La volta che abbiamo risolto il bug nel parser JSON" |
Sensory Memory: l'Input Immediato
La sensory memory rappresenta tutto ciò che l'agente "percepisce" nell'istante corrente: il messaggio dell'utente, i risultati dei tool appena eseguiti, le risposte delle API chiamate. E' il livello più basso e transitorio di memoria, che esiste solo per la durata di una singola iterazione del loop agente. Non richiede alcun meccanismo di persistenza perchè vive interamente nel prompt corrente.
In termini pratici, la sensory memory corrisponde ai parametri passati al modello in una singola chiamata: il system prompt, i messaggi dell'utente e dell'assistente, e i risultati delle tool call. E' limitata dalla context window del modello, che definisce il numero massimo di token processabili in una singola inferenza.
Short-term Memory: il Contesto della Sessione
La short-term memory mantiene il contesto all'interno di una singola sessione di conversazione. E' implementata come la lista dei messaggi scambiati tra utente e agente, inclusi i risultati delle tool call intermedie. Questa memoria consente all'agente di fare riferimento a ciò che è stato detto in precedenza, di risolvere ambiguità contestuali e di mantenere coerenza nel dialogo.
Il problema principale della short-term memory è la crescita lineare: più la conversazione prosegue, più token vengono consumati. Con modelli che hanno context window da 128K-200K token, una conversazione tecnica intensa può raggiungere il limite in poche ore di lavoro. Questo rende essenziale implementare strategie di compressione e prioritizzazione.
Long-term Memory: la Conoscenza Persistente
La long-term memory è ciò che permette all'agente di ricordare informazioni tra sessioni diverse. A differenza della short-term memory che vive nella conversazione, la long-term memory richiede uno storage esterno: un database vettoriale, un database relazionale, un file system strutturato. L'agente salva informazioni rilevanti durante le interazioni e le recupera quando servono nelle sessioni future.
Esempi di long-term memory includono: le preferenze dell'utente (linguaggio di programmazione preferito, stile di codice, convenzioni di naming), documenti aziendali indicizzati, la knowledge base di un progetto, le specifiche tecniche e i requisiti. Questo tipo di memoria trasforma l'agente da un assistente generico a un collaboratore personalizzato e contestualizzato.
Episodic Memory: i Ricordi Specifici
La episodic memory è il tipo più sofisticato: non si tratta solo di memorizzare fatti, ma di ricordare esperienze specifiche con il loro contesto relazionale. L'agente ricorda non solo che "esiste un bug nel parser", ma che "il 15 gennaio abbiamo diagnosticato un bug nel parser JSON causato da un encoding errato, che l'utente ha risolto aggiungendo una validazione UTF-8 nel pre-processing".
La episodic memory è tipicamente implementata con knowledge graphs, dove le entità (persone, progetti, bug, decisioni) sono nodi e le relazioni tra di esse (ha_risolto, ha_causato, dipende_da) sono archi. Questo consente all'agente di navigare le connessioni tra eventi e di fornire contesto ricco e pertinente.
Gestione della Conversation History
La conversation history è la forma più immediata di memoria: l'elenco completo dei messaggi scambiati nella sessione corrente. Gestirla in modo efficiente è cruciale perchè la context window ha un limite fisso, e superarlo significa perdere informazioni o, peggio, ricevere errori dal modello. Esistono tre strategie principali, ciascuna con i propri trade-off.
Strategia 1: Sliding Window
L'approccio più semplice: mantieni solo gli ultimi N messaggi e scarta quelli più vecchi. È facile da implementare, predicibile nel consumo di token e non richiede chiamate aggiuntive al modello. Lo svantaggio è che perde completamente il contesto iniziale della conversazione, il che può portare a inconsistenze quando l'agente fa riferimento a decisioni prese all'inizio del dialogo.
class SlidingWindowMemory:
def __init__(self, max_messages: int = 20):
self.max_messages = max_messages
self.messages: list[dict] = []
def add_message(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
# Mantieni sempre il system prompt (indice 0)
if len(self.messages) > self.max_messages:
system = self.messages[0]
self.messages = [system] + self.messages[-(self.max_messages - 1):]
def get_context(self) -> list[dict]:
return self.messages.copy()
def token_count(self) -> int:
return sum(len(m["content"]) // 4 for m in self.messages)
Strategia 2: Summarization Progressiva
Invece di scartare i messaggi vecchi, li riassumi progressivamente. Quando la conversazione supera una soglia di token, i messaggi più vecchi vengono compressi in un riassunto che preserva le informazioni chiave: decisioni prese, fatti importanti, preferenze espresse. Il riassunto viene preposto alla conversazione attiva, mantenendo il contesto senza consumare troppi token.
Questa strategia richiede chiamate aggiuntive al modello per generare i riassunti, il che aumenta il costo e la latenza. Tuttavia, preserva il contesto in modo molto più efficace rispetto allo sliding window, perchè le informazioni importanti non vengono mai completamente perse.
class SummarizationMemory:
def __init__(self, max_tokens: int = 4000, summary_threshold: int = 3000):
self.max_tokens = max_tokens
self.summary_threshold = summary_threshold
self.summary: str = ""
self.active_messages: list[dict] = []
def add_message(self, role: str, content: str):
self.active_messages.append({"role": role, "content": content})
if self._count_tokens(self.active_messages) > self.summary_threshold:
self._compress()
def _compress(self):
# Prendi i messaggi più vecchi (prima meta)
half = len(self.active_messages) // 2
old_messages = self.active_messages[:half]
self.active_messages = self.active_messages[half:]
# Genera un riassunto dei messaggi vecchi
prompt = f"Riassumi i punti chiave di questa conversazione:\n"
for msg in old_messages:
prompt += f"{msg['role']}: {msg['content']}\n"
new_summary = llm_call(prompt) # Chiamata al modello
self.summary = f"{self.summary}\n{new_summary}".strip()
def get_context(self) -> list[dict]:
context = []
if self.summary:
context.append({
"role": "system",
"content": f"Riassunto conversazione precedente:\n{self.summary}"
})
context.extend(self.active_messages)
return context
Strategia 3: Importance Sampling
L'approccio più sofisticato: ogni messaggio viene valutato per la sua importanza e solo quelli rilevanti vengono mantenuti nel contesto. L'importanza può essere determinata da diversi fattori: la presenza di decisioni esplicite, il riferimento a entità chiave del progetto, l'espressione di preferenze, la definizione di vincoli o requisiti.
Questa strategia produce i risultati migliori in termini di qualità del contesto, ma è anche la più complessa da implementare. Richiede un modello (o un'euristica) per valutare l'importanza di ogni messaggio, e il rischio di eliminare informazioni apparentemente irrilevanti ma cruciali e sempre presente.
class ImportanceSamplingMemory:
def __init__(self, max_tokens: int = 4000):
self.max_tokens = max_tokens
self.messages: list[dict] = []
self.importance_scores: list[float] = []
def add_message(self, role: str, content: str):
score = self._evaluate_importance(content)
self.messages.append({"role": role, "content": content})
self.importance_scores.append(score)
self._prune()
def _evaluate_importance(self, content: str) -> float:
score = 0.5 # Base score
# Decisioni esplicite
if any(kw in content.lower() for kw in ["deciso", "scelta", "usero", "implemento"]):
score += 0.3
# Vincoli e requisiti
if any(kw in content.lower() for kw in ["deve", "requisito", "vincolo", "deadline"]):
score += 0.2
# Errori e bug
if any(kw in content.lower() for kw in ["errore", "bug", "fix", "problema"]):
score += 0.2
# Messaggi recenti hanno un bonus
score = min(score, 1.0)
return score
def _prune(self):
while self._count_tokens() > self.max_tokens:
# Rimuovi il messaggio con l'importanza più bassa
# (escludi system prompt e ultimo messaggio)
min_idx = min(
range(1, len(self.importance_scores) - 1),
key=lambda i: self.importance_scores[i]
)
self.messages.pop(min_idx)
self.importance_scores.pop(min_idx)
Trade-off tra le Strategie
| Strategia | Complessità | Costo Aggiuntivo | qualità Contesto | Caso d'Uso |
|---|---|---|---|---|
| Sliding Window | Bassa | Zero | Media | Conversazioni brevi, prototipi |
| Summarization | Media | 1-2 chiamate LLM | Alta | Sessioni lunghe, task complessi |
| Importance Sampling | Alta | Euristica o LLM | Molto Alta | Agenti specializzati, production |
Vector Embeddings e Retrieval-Augmented Generation
I vector embeddings sono rappresentazioni numeriche dense di testo in uno spazio multidimensionale. Ogni frase, paragrafo o documento viene trasformato in un vettore di numeri (tipicamente 768 o 1536 dimensioni) dove la distanza geometrica tra vettori riflette la similarità semantica tra i testi corrispondenti. Due frasi con significato simile avranno vettori vicini nello spazio, anche se usano parole completamente diverse.
Come Funzionano gli Embeddings
Il processo di embedding utilizza un modello neurale specializzato (come text-embedding-3-small
di OpenAI o all-MiniLM-L6-v2 di Sentence Transformers) che prende in input una stringa
di testo e produce un vettore numerico. Questi modelli sono addestrati su enormi quantità di testo
con l'obiettivo di posizionare frasi semanticamente simili vicine nello spazio vettoriale.
La ricerca per similarità avviene calcolando la cosine similarity o la distanza euclidea tra il vettore della query e i vettori dei documenti indicizzati. I documenti con la distanza minore (o la similarità maggiore) sono i più rilevanti per la query.
import numpy as np
def cosine_similarity(vec_a: np.ndarray, vec_b: np.ndarray) -> float:
"""Calcola la similarità coseno tra due vettori."""
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot_product / (norm_a * norm_b)
# Esempio: embedding di due frasi
embedding_1 = embed("Come risolvere un bug nel parser JSON")
embedding_2 = embed("Fix di un errore nel parsing di file JSON")
embedding_3 = embed("Ricetta per la torta al cioccolato")
# Le prime due frasi avranno alta similarità (~0.85-0.95)
# La terza sarà molto distante (~0.05-0.15)
print(cosine_similarity(embedding_1, embedding_2)) # ~0.91
print(cosine_similarity(embedding_1, embedding_3)) # ~0.08
Vector Databases: le Opzioni Principali
Un vector database è un sistema di archiviazione ottimizzato per memorizzare e cercare vettori ad alta dimensionalità. A differenza dei database relazionali che usano indici B-tree, i vector database utilizzano algoritmi come HNSW (Hierarchical Navigable Small World) o IVF (Inverted File Index) per eseguire ricerche approximate in tempo sub-lineare.
Confronto Vector Databases
| Database | Tipo | Punto di Forza | Caso d'Uso |
|---|---|---|---|
| Pinecone | Managed (cloud) | Scalabilità, zero ops, indici serverless | Produzione su larga scala, team senza infra |
| Weaviate | Self-hosted / Cloud | Hybrid search (vector + BM25), GraphQL API | Search avanzata con filtri, e-commerce |
| Chroma | Embedded / Self-hosted | Leggero, API Python nativa, sviluppo locale | Prototipi, applicazioni locali, sviluppo |
| Qdrant | Self-hosted / Cloud | Performance, filtri avanzati, payload ricchi | Applicazioni ad alta performance |
| pgvector | Estensione PostgreSQL | Integrazione con stack esistente, SQL | Team che già usano PostgreSQL |
Pipeline RAG: Retrieval-Augmented Generation
Il pattern RAG (Retrieval-Augmented Generation) è la tecnica fondamentale per integrare la long-term memory negli agenti AI. L'idea è semplice: prima di rispondere a una domanda, l'agente cerca nella knowledge base i documenti più rilevanti e li include nel prompt come contesto aggiuntivo. Questo permette al modello di basare le sue risposte su informazioni specifiche e aggiornate, riducendo le allucinazioni.
class RAGPipeline:
def __init__(self, vector_db, embedding_model, llm):
self.vector_db = vector_db
self.embedding_model = embedding_model
self.llm = llm
def ingest(self, documents: list[str], metadata: list[dict] = None):
"""Indicizza documenti nella knowledge base."""
for i, doc in enumerate(documents):
# 1. Chunking: dividi il documento in frammenti
chunks = self._chunk_document(doc, chunk_size=512, overlap=50)
for chunk in chunks:
# 2. Embedding: genera il vettore
vector = self.embedding_model.embed(chunk)
# 3. Store: salva nel vector database
self.vector_db.upsert(
id=f"doc_{i}_{hash(chunk)}",
vector=vector,
text=chunk,
metadata=metadata[i] if metadata else {}
)
def query(self, question: str, top_k: int = 5) -> str:
"""Rispondi a una domanda usando RAG."""
# 1. Embedding della query
query_vector = self.embedding_model.embed(question)
# 2. Retrieval: cerca i documenti più simili
results = self.vector_db.search(
vector=query_vector,
top_k=top_k,
threshold=0.7 # Soglia minima di similarità
)
# 3. Context assembly: costruisci il contesto
context = "\n\n---\n\n".join([r.text for r in results])
# 4. Generation: genera la risposta
prompt = f"""Rispondi alla domanda basandoti SOLO sul contesto fornito.
Se il contesto non contiene la risposta, dillo esplicitamente.
Contesto:
{context}
Domanda: {question}
Risposta:"""
return self.llm.generate(prompt)
def _chunk_document(self, doc: str, chunk_size: int, overlap: int) -> list[str]:
"""Dividi un documento in chunk con overlap."""
words = doc.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = " ".join(words[i:i + chunk_size])
chunks.append(chunk)
return chunks
Best Practice per il Chunking
- Dimensione dei chunk: 256-1024 token. Troppo piccoli perdono contesto, troppo grandi introducono rumore
- Overlap: 10-20% della dimensione del chunk per evitare di spezzare concetti a meta
- Chunking semantico: preferisci dividere per paragrafi o sezioni logiche piuttosto che per numero di caratteri
- Metadata: associa a ogni chunk la sorgente, la data, il tipo di documento per filtrare i risultati
- Re-ranking: dopo il retrieval, usa un modello cross-encoder per riordinare i risultati per rilevanza
Knowledge Graphs per la Memoria Relazionale
I knowledge graphs rappresentano la conoscenza come una rete di entità (nodi) connesse da relazioni (archi), ciascuna con proprietà associate. A differenza dei vector database che eccellono nella ricerca per similarità semantica, i knowledge graphs brillano nel ragionamento relazionale: capire come le entità si collegano tra loro, seguire catene di relazioni e inferire nuova conoscenza dalle connessioni esistenti.
Struttura di un Knowledge Graph
Un knowledge graph è composto da tre elementi fondamentali:
- Nodi (Entità): rappresentano oggetti del dominio. Ogni nodo ha un tipo (label) è un insieme di proprietà. Esempio:
(:User {name: "Federico", role: "developer"}) - Archi (Relazioni): collegano due nodi e hanno un tipo e una direzione. Ogni arco può avere proprietà. Esempio:
[:RESOLVED {date: "2026-01-15", time_spent: "2h"}] - Proprietà: coppie chiave-valore associate a nodi o archi, che aggiungono dettagli e contesto
Neo4j è il Linguaggio Cypher
Neo4j è il database a grafo più diffuso, con un linguaggio di query dichiarativo
chiamato Cypher che rende intuitiva la navigazione dei grafi. Cypher usa una
sintassi visuale che rispecchia la struttura del grafo: i nodi sono rappresentati tra parentesi
tonde () e le relazioni tra parentesi quadre [] con frecce per la direzione.
// Creare nodi e relazioni per la memoria episodica
CREATE (u:User {name: "Federico", role: "developer"})
CREATE (p:Project {name: "E-commerce API", language: "Python"})
CREATE (b:Bug {id: "BUG-142", description: "Parser JSON crash on UTF-8"})
CREATE (s:Session {date: "2026-01-15", duration: "45min"})
// Relazioni con proprietà
CREATE (u)-[:WORKS_ON {since: "2025-11-01"}]->(p)
CREATE (b)-[:FOUND_IN]->(p)
CREATE (u)-[:RESOLVED {method: "UTF-8 validation", time: "2h"}]->(b)
CREATE (s)-[:DISCUSSED]->(b)
CREATE (s)-[:INVOLVED]->(u)
// Query: Trova tutti i bug risolti da Federico con il contesto
MATCH (u:User {name: "Federico"})-[r:RESOLVED]->(b:Bug)-[:FOUND_IN]->(p:Project)
RETURN b.description, r.method, r.time, p.name
// Query: Ricostruisci la timeline di una sessione
MATCH (s:Session {date: "2026-01-15"})-[:DISCUSSED]->(topic)
MATCH (s)-[:INVOLVED]->(participant)
RETURN s.date, collect(topic.description), collect(participant.name)
// Query: Trova bug correlati per progetto e tipo
MATCH (b1:Bug)-[:FOUND_IN]->(p:Project)<-[:FOUND_IN]-(b2:Bug)
WHERE b1 <> b2 AND b1.description CONTAINS "parser"
RETURN b1.description, b2.description, p.name
Vantaggi del Knowledge Graph per la Memoria
I knowledge graphs offrono vantaggi unici per la memoria degli agenti AI che i vector database non possono replicare:
- Ragionamento relazionale: l'agente può navigare le relazioni tra entità per scoprire connessioni non ovvie. "Federico ha risolto il bug BUG-142 che era nel progetto E-commerce, e lo stesso progetto ha un altro bug simile BUG-198"
- Context efficiente: invece di includere l'intera history nel prompt, l'agente estrae solo il sottografo rilevante per la query corrente, risparmiando token
- Inferenza: è possibile definire regole di inferenza che derivano nuova conoscenza dalle relazioni esistenti. Se A dipende da B e B dipende da C, allora A dipende transitivamente da C
- Aggiornamento incrementale: nuove informazioni vengono aggiunte come nuovi nodi e archi, senza dover ricostruire l'intero indice
- Spiegabilita: l'agente può tracciare il percorso di ragionamento attraverso il grafo, fornendo spiegazioni trasparenti
Approccio Ibrido: il Pattern Mem0
Il Pattern Mem0: Best Practice 2026
Il pattern emergente nel 2026 per i sistemi di memoria degli agenti AI combina vector store e knowledge graph in un unico sistema coeso. Il nome "Mem0" (pronunciato "mem-zero") si riferisce sia al progetto open source omonimo sia al pattern architetturale che ha introdotto: una memoria che parte da zero e cresce organicamente con ogni interazione.
Architettura del Pattern Mem0
L'idea fondamentale è semplice: quando l'agente riceve nuove informazioni, le salva contemporaneamente in entrambi i sistemi di storage. Quando deve recuperare contesto, esegue una ricerca ibrida che combina i risultati di entrambi i sistemi per massimizzare la qualità del retrieval.
class HybridMemory:
"""Pattern Mem0: Vector Store + Knowledge Graph combinati."""
def __init__(self, vector_db, graph_db, embedding_model, llm):
self.vector_db = vector_db
self.graph_db = graph_db
self.embedding_model = embedding_model
self.llm = llm
def store(self, interaction: str, metadata: dict):
"""Salva una nuova memoria in entrambi i sistemi."""
# 1. Estrai entità e relazioni dal testo
entities, relations = self._extract_knowledge(interaction)
# 2. Vector Store: salva l'embedding del testo completo
vector = self.embedding_model.embed(interaction)
self.vector_db.upsert(
id=metadata.get("id", str(hash(interaction))),
vector=vector,
text=interaction,
metadata=metadata
)
# 3. Knowledge Graph: salva entità e relazioni
for entity in entities:
self.graph_db.merge_node(entity.label, entity.properties)
for relation in relations:
self.graph_db.merge_relation(
relation.source, relation.type, relation.target,
properties=relation.properties
)
def retrieve(self, query: str, top_k: int = 5) -> dict:
"""Recupera contesto da entrambi i sistemi."""
# 1. Vector similarity search
query_vector = self.embedding_model.embed(query)
vector_results = self.vector_db.search(query_vector, top_k=top_k)
# 2. Estrai entità dalla query per il graph traversal
query_entities = self._extract_entities(query)
# 3. Graph traversal: espandi il contesto relazionale
graph_context = []
for entity in query_entities:
neighbors = self.graph_db.get_neighbors(
entity, depth=2, limit=10
)
graph_context.extend(neighbors)
# 4. Combina e deduplica i risultati
return {
"vector_results": vector_results,
"graph_context": graph_context,
"combined_context": self._merge_results(
vector_results, graph_context
)
}
def _extract_knowledge(self, text: str):
"""Usa un LLM per estrarre entità e relazioni."""
prompt = f"""Estrai le entità e le relazioni dal seguente testo.
Formato output:
ENTITIES: [tipo:nome:proprietà, ...]
RELATIONS: [source-TIPO_RELAZIONE->target, ...]
Testo: {text}"""
response = self.llm.generate(prompt)
return self._parse_extraction(response)
Flusso di Retrieval Ibrido
Il retrieval ibrido segue un processo a due fasi che sfrutta i punti di forza di entrambi i sistemi:
- Vector Similarity per il Narrowing: la ricerca vettoriale filtra rapidamente i documenti semanticamente rilevanti, riducendo lo spazio di ricerca da migliaia a poche decine di risultati
- Graph Traversal per l'Enrichment: partendo dalle entità trovate nei risultati vettoriali, il graph traversal espande il contesto seguendo le relazioni nel knowledge graph, aggiungendo informazioni correlate che la sola similarità semantica non avrebbe trovato
- Re-ranking e Fusione: i risultati combinati vengono riordinati per rilevanza complessiva, pesando sia la similarità semantica che la distanza nel grafo
- Context Assembly: il contesto finale viene assemblato rispettando il budget di token, con i risultati più rilevanti che hanno priorità
Ottimizzazione della Context Window
La context window è la risorsa più preziosa di un agente AI. Ogni token sprecato in contesto irrilevante e un token in meno per la risposta utile. L'ottimizzazione della context window richiede un approccio sistematico che bilanci la quantità di informazione disponibile con la qualità e la pertinenza del contesto selezionato.
Token Counting e Budget Allocation
Il primo passo è definire un budget di token per ogni componente del prompt. Un'allocazione tipica per un agente con 128K token di context window potrebbe essere:
Budget di Token per Componente
| Componente | Token Allocati | Percentuale | Note |
|---|---|---|---|
| System Prompt | 2.000 - 4.000 | 2-3% | Istruzioni, personalità, vincoli |
| Tool Definitions | 3.000 - 8.000 | 3-6% | Descrizione e schema dei tool disponibili |
| Long-term Memory | 5.000 - 15.000 | 5-12% | Risultati RAG, contesto dal knowledge graph |
| Conversation History | 20.000 - 50.000 | 15-40% | Messaggi recenti, riassunti |
| Reserved for Response | 8.000 - 16.000 | 6-12% | Spazio per la risposta del modello |
| Buffer di Sicurezza | 5.000 - 10.000 | 4-8% | Margine per tool results imprevisti |
Semantic Caching
Il semantic caching è una tecnica di ottimizzazione che evita di ricalcolare embeddings e query per domande simili a quelle già processate. Quando l'agente riceve una query, prima verifica se una query semanticamente simile è già stata processata di recente. Se la similarità supera una soglia (tipicamente 0.95), viene restituito il risultato dalla cache senza eseguire una nuova ricerca nel vector database.
Questo approccio riduce significativamente la latenza e il costo delle operazioni di retrieval, specialmente in scenari dove l'utente riformula la stessa domanda in modi diversi o dove le query successive sono variazioni dello stesso tema.
Hierarchical Retrieval
L'approccio hierarchical retrieval organizza la knowledge base su più livelli di granularità. Al livello più alto ci sono riassunti generali di interi documenti. Al livello intermedio ci sono riassunti di sezioni. Al livello più basso ci sono i chunk originali con tutti i dettagli.
La ricerca parte dal livello più alto: se i riassunti generali indicano che un documento e rilevante, si scende al livello di sezione, e poi al livello di chunk solo per le sezioni più pertinenti. Questo approccio riduce drasticamente il numero di embedding comparisons necessarie e migliora la qualità dei risultati eliminando il rumore.
Benchmarking e Trade-offs
Scegliere il sistema di memoria giusto dipende dal contesto specifico dell'applicazione. Non esiste una soluzione universalmente migliore: ogni approccio ha i propri punti di forza e di debolezza, e la scelta ottimale dipende dai requisiti di latenza, costo, accuratezza e complessità di implementazione.
Confronto Completo degli Approcci di Memoria
| Approccio | Latenza | Accuratezza | Costo | Complessità | Ideale Per |
|---|---|---|---|---|---|
| Sliding Window | ~0ms | Bassa | Zero | Minima | Chat semplici, prototipi |
| Summarization | 200-500ms | Media-Alta | Medio | Bassa | Sessioni lunghe, coding |
| Vector DB (RAG) | 50-200ms | Alta | Medio | Media | Knowledge base, documentazione |
| Knowledge Graph | 100-500ms | Molto Alta | Alto | Alta | Ragionamento relazionale |
| Ibrido (Mem0) | 200-800ms | Eccellente | Alto | Molto Alta | Agenti enterprise, produzione |
Quando Usare Cosa
- Solo Sliding Window: chatbot semplici, interazioni brevi, prototipi rapidi dove la persistenza non è necessaria
- Sliding Window + Summarization: sessioni di coding lunghe, assistenti personali dove il contesto della conversazione è sufficiente
- Vector DB (RAG): quando l'agente deve accedere a una knowledge base esterna (documentazione, codice sorgente, FAQ aziendali) e le query sono indipendenti
- Knowledge Graph: quando le relazioni tra entità sono importanti (project management, CRM, sistemi diagnostici) e serve ragionamento multi-hop
- Ibrido (Mem0): agenti enterprise di produzione dove servono sia la ricerca semantica che il ragionamento relazionale, e il budget lo consente
Implementazione Pratica: Considerazioni di Produzione
Implementare un sistema di memoria in produzione richiede attenzione a diversi aspetti che vanno oltre la semplice scelta dell'algoritmo. La gestione della concorrenza, la consistency dei dati, il monitoring e la privacy sono tutti aspetti critici.
- Privacy e GDPR: le memorie possono contenere dati personali. Implementa sempre meccanismi di cancellazione selettiva e consenso esplicito
- Decay temporale: le memorie vecchie perdono rilevanza. Implementa un sistema di decay che riduce progressivamente il peso delle memorie non accedute
- Conflitti e aggiornamenti: quando nuove informazioni contraddicono memorie esistenti, l'agente deve decidere quale versione mantenere. Privilegia sempre le informazioni più recenti con un log degli aggiornamenti
- Monitoring: traccia metriche come hit rate del retrieval, latenza media, distribuzione dei token per componente e qualità percepita delle risposte
- Fallback graceful: se il sistema di memoria fallisce (vector DB down, grafo corrotto), l'agente deve continuare a funzionare degradando alla sola conversazione corrente
Conclusioni
La memoria è il componente che trasforma un LLM stateless in un agente veramente intelligente e persistente. Dalla semplice conversation history ai sofisticati sistemi ibridi con vector database e knowledge graph, ogni approccio offre un diverso equilibrio tra complessità, costo e qualità del contesto.
Il pattern Mem0, che combina vector store e knowledge graph, sta emergendo come best practice per gli agenti di produzione nel 2026. La chiave non e scegliere un singolo approccio, ma comprendere i trade-off e selezionare la combinazione giusta per il proprio caso d'uso, partendo semplice e aggiungendo complessità solo quando necessario.
Nel prossimo articolo esploreremo il tool calling avanzato: come gli agenti integrano API REST, servizi web e tool personalizzati per agire nel mondo reale. Vedremo JSON Schema per la definizione dei tool, strategie di input validation e sanitization, e come costruire un framework di tool riutilizzabili.







