Il problema: un agente che ogni volta ricomincia da zero
Un modello linguistico non ricorda nulla tra una chiamata e l'altra: ciò che «sa» della tua conversazione è ciò che gli rimandi nel prompt. La memoria di un agente è quindi un problema di scelta, non di archiviazione: che cosa rimettere nel prompt, da dove prenderlo, quando smettere di portarselo dietro. Questo articolo dà tre risposte a tre domande diverse.
Per la sessione in corso: una finestra di messaggi recenti e, se serve, un riassunto di quelli vecchi. Per i documenti e le note: la ricerca per somiglianza su blocchi di testo. Per le relazioni e per i fatti che cambiano: un grafo, con una validità nel tempo. Alla fine un quarto listato mette insieme i risultati dentro un budget di token.
I listati girano con la sola libreria standard di Python 3.14.4 (SQLite compreso) e nessun modello: i conteggi di token sono una stima dichiarata di una parola per token, il riassuntore e l'«embedding» sono giocattoli a regola, e dove un risultato dipende da un modello vero lo scrivo. Se ti serve prima il quadro del coordinamento tra agenti, parti da Orchestrazione Multi-Agente: lo stato condiviso lì è un registro di eventi, qui è ciò che l'agente porta con sé tra un turno e l'altro.
Che cosa si intende per memoria
La documentazione di LangChain distingue due ambiti. La memoria a breve termine è legata a una conversazione (un «thread»): in LangGraph è parte dello stato dell'agente, e viene salvata su un database tramite un checkpointer perché il thread possa essere ripreso; ne parla l'articolo su LangChain e LangGraph. La memoria a lungo termine conserva dati di un utente o dell'applicazione tra sessioni, è condivisa tra thread e vive in «spazi dei nomi» scelti da te.
Quanto al contenuto, la stessa documentazione usa la tripartizione della psicologia: memoria semantica (fatti), episodica (esperienze), procedurale (regole su come eseguire un compito). Il lavoro che ha organizzato il campo, «Cognitive Architectures for Language Agents» (CoALA), descrive un agente come dotato di componenti di memoria modulari, con un insieme strutturato di azioni per leggerli e scriverli.
| Tipo | Che cosa conserva | Esempio | Come si usa di solito |
|---|---|---|---|
| Semantica | fatti e concetti | «preferisce Python» | un profilo dell'utente aggiornato nel tempo, o una raccolta di documenti |
| Episodica | esperienze passate | una sessione in cui un bug è stato risolto | nella documentazione LangChain, spesso come esempi nel prompt (few-shot) |
| Procedurale | regole su come fare | «rispondi sempre con un esempio» | secondo la documentazione, una combinazione di pesi del modello, codice dell'agente e prompt |
La tabella corregge un'abitudine diffusa: far coincidere la memoria episodica con un grafo di conoscenza. Nella documentazione citata la memoria episodica sono esperienze, spesso reintrodotte come esempi nel prompt; il grafo è uno strumento per rappresentare fatti e relazioni, che può servire a più tipi. Anche la classificazione «sensoriale, breve, lunga, episodica» che si trova in molti tutorial è un'analogia con la psicologia, non una struttura dei framework. Nel resto dell'articolo la domanda utile è un'altra: per ogni informazione, quanto deve vivere, e come la ritrovo quando serve.
La sessione: finestra e riassunto
La storia di una conversazione cresce a ogni turno e ogni messaggio che rimandi costa token e latenza. La soluzione più semplice è la finestra scorrevole: tieni i messaggi più recenti che stanno in un limite e scarta il resto. È prevedibile e non richiede altre chiamate al modello, ma ha un difetto che si vede subito nel listato: le istruzioni date all'inizio sono le prime a sparire.
Il riassunto compensa: i messaggi scartati vengono condensati in una voce che si antepone alla finestra. Nel listato il riassuntore è una regola (tiene i messaggi che contengono parole come «budget» o «DECISIONE»), perché un riassunto vero richiede un modello, cioè una chiamata in più da pagare.
def token(testo):
"""Stima volutamente rozza: una parola = un token. Un conteggio vero dipende dal modello."""
return len(testo.split())
STORIA = [
("utente", "Il budget massimo del progetto e' 500 euro, non superarlo."),
("agente", "Capito. Parto dal confronto tra le due librerie."),
("utente", "Preferisco una soluzione senza dipendenze esterne."),
("agente", "Allora propongo la libreria standard."),
("utente", "DECISIONE: usiamo SQLite per i dati locali."),
("agente", "Registrato. Preparo lo schema delle tabelle."),
("utente", "Aggiungi anche un indice sulla colonna data."),
("agente", "Fatto. L'indice e' sulla colonna data."),
]
def finestra(storia, limite):
"""Tiene i messaggi piu' recenti finche' stanno nel limite di token."""
tenuti, usati = [], 0
for ruolo, testo in reversed(storia):
if usati + token(testo) > limite:
break
tenuti.append((ruolo, testo))
usati += token(testo)
return list(reversed(tenuti))
def riassumi(messaggi):
"""Riassuntore finto a regola: tiene i messaggi con vincoli o decisioni."""
chiavi = ("budget", "DECISIONE", "senza dipendenze")
return " | ".join(t for _, t in messaggi if any(c in t for c in chiavi))
def con_riassunto(storia, limite):
recenti = finestra(storia, limite)
vecchi = storia[: len(storia) - len(recenti)]
return ([("riassunto", riassumi(vecchi))] if vecchi else []) + recenti
for nome, contesto in (("solo finestra", finestra(STORIA, 20)),
("finestra + riassunto", con_riassunto(STORIA, 20))):
totale = sum(token(t) for _, t in contesto)
ricorda_budget = any("budget" in t for _, t in contesto)
print(f"{nome}: {len(contesto)} voci, {totale} token, ricorda il budget: {ricorda_budget}")
print("storia intera:", sum(token(t) for _, t in STORIA), "token")
print(con_riassunto(STORIA, 20)[0])
# Output:
# solo finestra: 3 voci, 19 token, ricorda il budget: False
# finestra + riassunto: 4 voci, 44 token, ricorda il budget: True
# storia intera: 55 token
# ('riassunto', "Il budget massimo del progetto e' 500 euro, non superarlo. | Preferisco una soluzione senza dipendenze esterne. | DECISIONE: usiamo SQLite per i dati locali.")
La sola finestra tiene tre messaggi per 19 token e ha perso il vincolo di budget, che l'utente aveva dichiarato per primo: se l'agente propone una soluzione da 800 euro, non c'è nulla che lo fermi. Con il riassunto il vincolo resta, a un costo di 44 token contro i 55 della storia intera.
Il risparmio è piccolo perché la storia di esempio è corta, e il riassunto qui è una copia letterale: con un modello vero dovresti anche pagare la chiamata che lo produce e accettare che possa omettere un dettaglio. Per questo conviene salvare a parte (fuori dal riassunto) i vincoli che non devono mai perdersi, e rimetterli in cima a ogni prompt.
Il lungo termine: blocchi, vettori e ricerca
Quando le informazioni non stanno in una conversazione (documentazione, note, codice) si usa il recupero: si cercano i blocchi di testo più vicini alla domanda e si mettono nel prompt. L'idea ha un lavoro d'origine, «Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks» (Lewis et al.), che combina la memoria nei parametri di un modello con una memoria esterna, un indice vettoriale di Wikipedia consultato da un recuperatore.
Un modello di embedding trasforma un testo in un vettore, e la vicinanza tra vettori (di solito il coseno) è la misura di somiglianza. Il numero di dimensioni dipende dal modello: la scheda di all-MiniLM-L6-v2 dichiara 384 dimensioni e dice che i testi oltre 256 «word pieces» vengono troncati per impostazione predefinita.
Quest'ultimo dettaglio decide come spezzare i documenti: un blocco più lungo del limite del modello viene troncato in silenzio, e la coda del blocco non partecipa alla ricerca. Il listato spezza un testo in blocchi di dodici parole con tre di sovrapposizione, e usa un «embedding» giocattolo, un conteggio di parole, per far vedere la meccanica senza un modello: il modello vero produce un vettore denso e coglie sinonimi, quello giocattolo no, ed è proprio il limite che il secondo esperimento rende visibile.
import math
from collections import Counter
def blocchi(testo, dimensione, sovrapposizione):
"""Spezza in blocchi di `dimensione` parole; ogni blocco riprende `sovrapposizione` parole del precedente."""
parole = testo.split()
passo = dimensione - sovrapposizione
return [" ".join(parole[i:i + dimensione])
for i in range(0, max(len(parole) - sovrapposizione, 1), passo)]
def vettore(testo):
"""Embedding giocattolo: conteggio di parole. Un modello vero dara' un vettore denso."""
return Counter(p.strip(".,?").lower() for p in testo.split() if len(p) > 3)
def coseno(a, b):
prodotto = sum(a[k] * b[k] for k in a if k in b)
norma = math.sqrt(sum(v * v for v in a.values())) * math.sqrt(sum(v * v for v in b.values()))
return prodotto / norma if norma else 0.0
DOC = ("Il certificato del sito si rinnova ogni novanta giorni con il comando di rinnovo. "
"Il backup del database parte ogni notte alle undici e cinquantanove. "
"Per cambiare la password dell'amministratore usa il pannello utenti.")
for i, b in enumerate(blocchi(DOC, 12, 3)):
print(i, b)
archivio = blocchi(DOC, 12, 3)
for domanda in ("quando si rinnova il certificato?", "come rinnovo la licenza del sito?"):
punteggi = sorted(((coseno(vettore(domanda), vettore(b)), i) for i, b in enumerate(archivio)),
reverse=True)
print(f"{domanda!r} -> blocco {punteggi[0][1]}, similarita' {punteggi[0][0]:.2f}")
# Output:
# 0 Il certificato del sito si rinnova ogni novanta giorni con il comando
# 1 con il comando di rinnovo. Il backup del database parte ogni notte
# 2 parte ogni notte alle undici e cinquantanove. Per cambiare la password dell'amministratore
# 3 la password dell'amministratore usa il pannello utenti.
# 'quando si rinnova il certificato?' -> blocco 0, similarita' 0.44
# 'come rinnovo la licenza del sito?' -> blocco 1, similarita' 0.19
La sovrapposizione si vede nei blocchi 0 e 1: la frase sul rinnovo viene spezzata a metà dal primo taglio, ma il blocco 1 la riprende con «con il comando di rinnovo», così il significato non va perso. Il primo quesito trova il blocco giusto (similarità 0,44).
Il secondo usa «licenza» invece di «certificato» e «rinnovo» invece di «rinnova»: le parole non coincidono, e il sistema sceglie il blocco 1, che contiene per caso «rinnovo», invece del blocco 0, che risponde ma condivide con la domanda solo «sito»; il punteggio è basso (0,19). Un modello di embedding vero dovrebbe avvicinare questi termini, ma non l'ho verificato qui, e la morale resta: il punteggio di un recupero va guardato, e sotto una soglia (da scegliere sui tuoi dati) conviene rispondere «non lo so» e non passare al modello un blocco irrilevante.
Cercare tra milioni di vettori confrontandoli tutti è lento, e per questo i database vettoriali usano indici approssimati. L'indice HNSW, descritto da Malkov e Yashunin, costruisce un grafo a più livelli e dichiara una complessità che scala in modo logaritmico; l'estensione pgvector per PostgreSQL offre sia la ricerca esatta sia gli indici approssimati HNSW e IVFFlat, e il suo README precisa che gli approssimati scambiano un po' di richiamo (recall) con la velocità, quindi gli stessi dati possono dare risultati diversi dopo aver creato l'indice. Prima di scegliere un prodotto, quattro decisioni valgono più della marca:
- la dimensione dei blocchi, sotto il limite del modello di embedding che usi;
- la sovrapposizione, che costa spazio ma evita di spezzare un concetto;
- i metadati (fonte, data, tipo), che permettono di filtrare e di citare;
- il modo di cancellare un documento, che deve eliminare anche i suoi vettori.
Le quattro voci non hanno un ordine di importanza, ma la prima e l'ultima sono quelle che si scoprono tardi: la prima quando le risposte ignorano la fine dei documenti, l'ultima quando qualcuno chiede di cancellare i propri dati. Non do una misura «giusta» di dimensione o sovrapposizione: dipendono dal modello e dai documenti, e si decidono confrontando la qualità del recupero su domande tue (come in Testing e Evaluation).
Il grafo: relazioni e fatti che cambiano
La ricerca per somiglianza trova testi simili, non risponde a «quali altri bug ci sono nel progetto di questo» né a «che cosa è vero adesso». Per queste domande serve una struttura di fatti: soggetto, relazione, oggetto. Il listato la costruisce in una sola tabella SQLite e risponde a tre domande.
La prima è un salto (un join della tabella con sé stessa). La seconda segue la catena delle dipendenze con una query ricorsiva (WITH RECURSIVE), che SQLite supporta. La terza gestisce il cambiamento: un fatto nuovo che contraddice il vecchio non lo cancella, ma ne chiude la validità scrivendo la colonna al.
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("""CREATE TABLE fatti (soggetto TEXT, relazione TEXT, oggetto TEXT,
dal INTEGER, al INTEGER)""") # al NULL = ancora vero
def aggiungi(s, r, o, t):
db.execute("INSERT INTO fatti VALUES (?, ?, ?, ?, NULL)", (s, r, o, t))
def sostituisci(s, r, o, t):
"""Un fatto nuovo che contraddice il vecchio lo chiude, non lo cancella."""
db.execute("UPDATE fatti SET al = ? WHERE soggetto = ? AND relazione = ? AND al IS NULL",
(t, s, r))
aggiungi(s, r, o, t)
for s, r, o in [("BUG-142", "nel_progetto", "Shop"), ("BUG-198", "nel_progetto", "Shop"),
("BUG-142", "risolto_da", "Marta"), ("Shop", "dipende_da", "Auth"),
("Auth", "dipende_da", "Redis"), ("Redis", "dipende_da", "Rete")]:
aggiungi(s, r, o, 1)
aggiungi("Marta", "preferisce", "JavaScript", 1)
sostituisci("Marta", "preferisce", "Python", 5)
# 1. Un salto: altri bug nello stesso progetto del BUG-142
print(db.execute("""SELECT b.soggetto FROM fatti a JOIN fatti b
ON a.oggetto = b.oggetto AND b.relazione = a.relazione
WHERE a.soggetto = 'BUG-142' AND a.relazione = 'nel_progetto'
AND b.soggetto != a.soggetto""").fetchall())
# 2. Piu' salti: tutto da cui dipende Shop, con la distanza
print(db.execute("""WITH RECURSIVE dip(nodo, distanza) AS (
SELECT oggetto, 1 FROM fatti WHERE soggetto = 'Shop' AND relazione = 'dipende_da'
UNION
SELECT f.oggetto, d.distanza + 1 FROM fatti f JOIN dip d ON f.soggetto = d.nodo
WHERE f.relazione = 'dipende_da')
SELECT nodo, distanza FROM dip ORDER BY distanza""").fetchall())
# 3. Che cosa e' vero adesso, e che cosa lo era prima
print("adesso:", db.execute("""SELECT oggetto FROM fatti WHERE soggetto = 'Marta'
AND relazione = 'preferisce' AND al IS NULL""").fetchall())
print("storia:", db.execute("""SELECT oggetto, dal, al FROM fatti WHERE soggetto = 'Marta'
AND relazione = 'preferisce' ORDER BY dal""").fetchall())
# Output:
# [('BUG-198',)]
# [('Auth', 1), ('Redis', 2), ('Rete', 3)]
# adesso: [('Python',)]
# storia: [('JavaScript', 1, 5), ('Python', 5, None)]
Il primo risultato dice che nello stesso progetto del BUG-142 c'è il BUG-198. Il secondo elenca tutto da cui dipende Shop con la distanza (Auth a 1, Redis a 2, Rete a 3): una ricerca per somiglianza non ricostruisce una catena, perché nessun blocco di testo la contiene per intero. Il terzo separa il presente (Python) dalla storia, in cui JavaScript valeva fino al tempo 5.
Quella storia è utile e rischiosa: se i fatti riguardano una persona, il Regolamento (UE) 2016/679 all'articolo 17 le dà il diritto di ottenere la cancellazione dei dati personali che la riguardano, a certe condizioni, quindi il progetto deve prevedere anche un percorso che elimina davvero le righe, non solo le chiude. Stabilire se e quando quel diritto si applica al tuo caso non è una domanda tecnica.
In un grafo vero, come quelli interrogati con il linguaggio Cypher di Neo4j, la stessa prima domanda si scrive descrivendo il disegno del grafo: nodi tra parentesi tonde, relazioni tra quadre con una freccia. Il listato seguente usa lo schema della documentazione (un pattern MATCH con RETURN) applicato ai bug; è uno schema da adattare, non l'ho eseguito perché servirebbe un'istanza Neo4j.
// Schema da adattare, non eseguito: serve un'istanza Neo4j con questi nodi e relazioni.
// Gli altri bug del progetto in cui si trova BUG-142.
MATCH (:Bug {id: 'BUG-142'})-[:NEL_PROGETTO]->(p:Progetto)<-[:NEL_PROGETTO]-(altro:Bug)
RETURN altro.id AS bug, p.nome AS progetto
Il pattern legge da sinistra a destra: un nodo Bug con quell'identificativo, la relazione verso un Progetto, e poi, risalendo la stessa relazione in senso inverso, gli altri bug. Quando l'insieme dei fatti è piccolo e le domande sono poche, una tabella come quella di sopra basta e non aggiunge un servizio da gestire; un database a grafo diventa interessante quando i percorsi sono lunghi e variabili, ed è una decisione da prendere con una misura sui tuoi dati, non con una regola.
Mem0: che cosa dice la fonte
Mem0 è un progetto open source (licenza Apache 2.0, installabile con pip install mem0ai secondo il suo README) e un articolo di ricerca, «Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory». Gli autori descrivono un'architettura che estrae, consolida e recupera dinamicamente le informazioni salienti da conversazioni in corso, e una variante con memoria a grafo.
Sul benchmark LOCOMO dichiarano un miglioramento relativo del 26% nella metrica «LLM-as-a-Judge» rispetto a uno dei sistemi di confronto (OpenAI), un punteggio circa 2% più alto per la variante a grafo rispetto alla base, il 91% in meno di latenza al 95° percentile e oltre il 90% di costo di token in meno rispetto a passare l'intera storia nel contesto.
Sono risultati degli autori sul loro benchmark, ed è per questo che li attribuisco: non li ho riprodotti, perché richiederebbero un modello, un modello di embedding e un archivio. La lettura onesta è che il paper sostiene un vantaggio rispetto al passare tutto il contesto, ed è un argomento a favore della memoria selettiva; non dice che «combinare vettori e grafo» sia la pratica da adottare ovunque, e la differenza tra base e grafo (circa 2%) è piccola rispetto al costo di gestire un componente in più. Non ho trovato una fonte primaria per l'idea che sia lo standard del 2026, e non la ripeto.
Assemblare il contesto: budget, doppioni, posizione
Una volta ottenuti candidati da più fonti, bisogna decidere che cosa entra nel prompt. Tre regole sono facili da codificare: un budget di token, l'eliminazione dei doppioni (la ricerca spesso restituisce due volte lo stesso fatto) e un ordine. Per l'ordine c'è una fonte:
«Lost in the Middle» (Liu et al.) mostra che le prestazioni dei modelli sono spesso più alte quando l'informazione rilevante è all'inizio o alla fine del contesto e peggiorano, anche nei modelli a contesto lungo, quando sta nel mezzo. Il listato combina due punteggi dichiarati (somiglianza e grafo, costanti inventate per l'esempio), prende i candidati in ordine di punteggio finché stanno nel budget, e li dispone con i più rilevanti agli estremi.
def token(testo):
return len(testo.split()) # stima rozza: una parola = un token
# Candidati: (testo, punteggio dalla ricerca per somiglianza, punteggio dal grafo).
# I punteggi sono costanti dichiarate: qui conta l'assemblaggio, non la ricerca.
CANDIDATI = [
("Il certificato si rinnova ogni novanta giorni.", 0.80, 0.10),
("Il rinnovo usa il comando del gestore dei certificati.", 0.60, 0.30),
("Il backup parte ogni notte.", 0.15, 0.00),
("Il sito dipende dal servizio di autenticazione.", 0.10, 0.70),
("Il certificato si rinnova ogni novanta giorni.", 0.70, 0.00), # duplicato
]
def assembla(candidati, budget, peso_grafo=0.5):
visti, scelti, usati = set(), [], 0
ordinati = sorted(candidati, key=lambda c: (c[1] + peso_grafo * c[2]), reverse=True)
for testo, vett, graf in ordinati:
if testo in visti or usati + token(testo) > budget:
continue # niente doppioni, niente sforamenti
visti.add(testo)
scelti.append((len(scelti) + 1, testo)) # (rango di rilevanza, testo)
usati += token(testo)
# i piu' rilevanti agli estremi, i meno rilevanti al centro
davanti, dietro = scelti[0::2], scelti[1::2]
return davanti + dietro[::-1], usati
for budget in (40, 14):
contesto, usati = assembla(CANDIDATI, budget)
print(f"budget {budget}: {usati} token, {len(contesto)} voci")
for rango, riga in contesto:
print(f" [rango {rango}] {riga}")
# Output:
# budget 40: 28 token, 4 voci
# [rango 1] Il certificato si rinnova ogni novanta giorni.
# [rango 3] Il sito dipende dal servizio di autenticazione.
# [rango 4] Il backup parte ogni notte.
# [rango 2] Il rinnovo usa il comando del gestore dei certificati.
# budget 14: 14 token, 2 voci
# [rango 1] Il certificato si rinnova ogni novanta giorni.
# [rango 2] Il sito dipende dal servizio di autenticazione.
Con budget 40 entrano quattro voci, e quella al rango 2 finisce in fondo: il rango 1 apre il contesto e il rango 2 lo chiude, mentre i meno rilevanti stanno in mezzo. Il duplicato sul certificato è stato scartato.
Con budget 14 il secondo candidato per punteggio (9 token) non entra, perché lo spazio rimasto è 7, e passa avanti uno meno rilevante: è la scelta golosa (greedy) del listato, e un'alternativa sarebbe fermarsi alla prima che non sta. Quale sia meglio dipende dal fatto che per te sia peggio un contesto incompleto o uno che include materiale di secondo piano. Anche il peso del grafo (0,5) è un valore di esempio, da calibrare su domande tue.
Restano cinque aspetti di produzione che i tutorial lasciano alla fine:
- la privacy: ciò che l'agente ricorda può contenere dati personali, e serve un percorso di cancellazione verificato;
- l'invecchiamento: un fatto vecchio non scompare da solo, va chiuso (come nel grafo) o deprezzato;
- i conflitti: due fatti che si contraddicono richiedono una regola di precedenza, per esempio vince il più recente, con un registro dei cambi;
- l'osservabilità: quanti recuperi arrivano sotto soglia, quanti token occupa ogni parte del prompt;
- il ripiego: se la memoria non risponde, l'agente deve continuare con la sola conversazione corrente, dichiarandolo.
Sono indipendenti tra loro, ma i primi tre cambiano il modello dei dati e sono costosi da aggiungere dopo, mentre gli ultimi due si aggiungono in qualsiasi momento: per questo conviene decidere subito come si cancella e come si invecchia un ricordo. La difesa dagli abusi, per esempio testo ostile che qualcuno riesce a far entrare nella memoria perché l'agente lo rilegga più tardi, rientra nei rischi trattati in Security e Safety. Come l'agente usa gli strumenti che leggono e scrivono la memoria è invece il tema di Tool Calling Avanzato.
Domande frequenti
Qual è la differenza tra memoria a breve e a lungo termine in un agente?
Secondo la documentazione di LangChain la memoria a breve termine è legata a una conversazione e fa parte dello stato dell'agente, mentre quella a lungo termine conserva dati tra sessioni, condivisi tra conversazioni diverse e organizzati in spazi dei nomi.
Serve un database vettoriale per dare memoria a un agente?
Non sempre. Per una sessione bastano finestra e riassunto; per pochi fatti strutturati può bastare una tabella. Un indice vettoriale serve quando devi trovare, tra molti testi, quelli vicini a una domanda; allora conta la scelta tra ricerca esatta e approssimata, che scambia richiamo con velocità.
Che cosa è Mem0 e conviene usarlo?
È una libreria open source e un articolo di ricerca su una memoria a lungo termine per agenti. Gli autori dichiarano vantaggi di latenza e di costo rispetto al passare tutta la storia, misurati sul loro benchmark; se ti conviene dipende dal tuo carico, e va misurato sui tuoi dati.
Che cosa sai fare adesso
Sai distinguere memoria di sessione e memoria tra sessioni, e i tre tipi di contenuto che la documentazione chiama semantico, episodico e procedurale. Sai tenere una finestra di conversazione senza perdere i vincoli, spezzare un documento in blocchi con sovrapposizione sapendo perché il limite del modello conta, leggere il punteggio di un recupero e rifiutare quello troppo basso.
Sai rappresentare fatti e relazioni in un grafo con una validità nel tempo, rispondere a domande a più salti, e assemblare il contesto dentro un budget mettendo i risultati migliori agli estremi. E sai che le cifre di un progetto come Mem0 vanno lette come misure dei suoi autori.
Decidere che cosa ricordare lascia aperte due questioni: quanto costa portarsi dietro il contesto, che confrontiamo in FinOps: Cost Optimization per Agenti e Token LLM, e dove vive lo stato quando il processo viene fermato e riavviato, come nel deployment in produzione.
Fonti
- Memory overview (documentazione LangChain) - memoria a breve e lungo termine, checkpointer, spazi dei nomi, memoria semantica, episodica e procedurale; consultata il 2026-11-12
- Cognitive Architectures for Language Agents (Sumers et al., arXiv:2309.02427) - componenti di memoria modulari in un agente; abstract letto il 2026-11-12
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (Lewis et al., arXiv:2005.11401) - memoria non parametrica come indice vettoriale; abstract letto il 2026-11-12
- all-MiniLM-L6-v2 (scheda del modello, Hugging Face) - 384 dimensioni, troncamento oltre 256 word pieces; consultata il 2026-11-12
- Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs (Malkov, Yashunin, arXiv:1603.09320) - indice HNSW a livelli, complessità logaritmica dichiarata; abstract letto il 2026-11-12
- pgvector (README del repository) - ricerca esatta e approssimata, indici HNSW e IVFFlat, scambio tra richiamo e velocità; consultato il 2026-11-12
- WITH clause (documentazione SQLite) - query ricorsive; i listati sono eseguiti con SQLite 3.46.1, consultata il 2026-11-12
- Basic queries (Neo4j Cypher Manual) - forma di MATCH e RETURN e dei pattern con relazioni; consultato il 2026-11-12
- Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory (Chhikara et al., arXiv:2504.19413) - architettura e risultati su LOCOMO dichiarati dagli autori; abstract letto il 2026-11-12
- mem0ai/mem0 (README del repository) - pacchetto
mem0ai, licenza Apache 2.0; consultato il 2026-11-12 - Lost in the Middle: How Language Models Use Long Contexts (Liu et al., arXiv:2307.03172) - prestazioni più alte con l'informazione a inizio o fine del contesto; abstract letto il 2026-11-12
- Regolamento (UE) 2016/679, articolo 17 (EUR-Lex) - diritto alla cancellazione; consultato il 2026-11-12

Commenti
Caricamento commenti...
Accedi per commentare