Architettura
Questa pagina spiega il design interno di Kore Memory: come funziona il motore di decay di Ebbinghaus, come viene calcolata l'importanza senza un LLM, come opera la ricerca semantica in locale e come la compressione della memoria unisce le conoscenze ridondanti.
Panoramica del sistema
+-----------------+
| REST API |
| (FastAPI) |
+--------+--------+
|
+--------------+--------------+
| | |
+--------v---+ +------v------+ +----v--------+
| Decay | | Embedding | | Importance |
| Engine | | Engine | | Scorer |
+--------+---+ +------+------+ +----+--------+
| | |
+--------------+--------------+
|
+--------v--------+
| SQLite |
| (FTS5 + WAL) |
+-----------------+
Tutti i componenti girano in un singolo processo. Non ci sono servizi esterni, code di messaggi o worker in background. Il database è SQLite in modalità WAL per letture concorrenti durante le scritture.
Motore di decay di Ebbinghaus
Il motore di decay è l'elemento differenziante di Kore Memory. Modella l'oblio umano utilizzando la curva dell'oblio di Hermann Ebbinghaus (1885), adattata per l'uso computazionale.
La formula
decay = e^(-t * ln(2) / half_life)
Dove:
t= tempo trascorso dall'ultimo accesso al ricordo (in giorni)half_life= emivita in giorni, determinata dal livello di importanzae= numero di Eulero (~2.71828)ln(2)= logaritmo naturale di 2 (~0.693)
Questo significa che dopo esattamente un'emivita, il punteggio di decay scende a 0.5 (50% di ritenzione). Dopo due emivite scende a 0.25, e così via.
Emivite della memoria
Ogni livello di importanza corrisponde a un'emivita diversa:
| Importanza | Etichetta | Emivita | Decay al 50% | Decay al 10% |
|---|---|---|---|---|
| 1 | Bassa | 7 giorni | 1 settimana | ~23 giorni |
| 2 | Normale | 14 giorni | 2 settimane | ~46 giorni |
| 3 | Importante | 30 giorni | 1 mese | ~100 giorni |
| 4 | Alta | 90 giorni | 3 mesi | ~299 giorni |
| 5 | Critica | 365 giorni | 1 anno | ~3.3 anni |
Punteggio di decay nel tempo (Importanza 3)
Day 0: decay = 1.000 ████████████████████ 100%
Day 7: decay = 0.851 █████████████████ 85%
Day 14: decay = 0.724 ██████████████ 72%
Day 30: decay = 0.500 ██████████ 50%
Day 60: decay = 0.250 █████ 25%
Day 90: decay = 0.125 ██ 13%
Day120: decay = 0.063 █ 6%
Effetto della ripetizione dilazionata
Ogni volta che un ricordo viene recuperato (tramite ricerca, timeline o accesso diretto), il suo punteggio di decay viene rinforzato:
decay_score += 0.05
Inoltre, ogni recupero estende l'emivita effettiva del ricordo del +15%. Questo rispecchia l'effetto della ripetizione dilazionata nell'apprendimento umano: i ricordi a cui si accede regolarmente diventano più duraturi nel tempo.
Ad esempio, un ricordo di importanza 3 (emivita di 30 giorni) a cui si accede 5 volte avrebbe un'emivita effettiva di:
30 * (1.15)^5 = 30 * 2.011 = ~60 giorni
Quando viene eseguito il decay
Il motore di decay si attiva quando chiami POST /decay/run o il tool MCP memory_decay_run. Esegue le seguenti operazioni:
- Itera su tutti i ricordi attivi
- Ricalcola il
decay_scoredi ciascun ricordo usando la formula sopra - Rimuove i ricordi il cui
decay_scorescende sotto una soglia minima (effettivamente dimenticati) - Aggiorna il database in un'unica transazione
Pianifica l'esecuzione del decay periodicamente (es. giornalmente tramite cron) per ottenere i migliori risultati. La dashboard web dispone anche di un pulsante one-click.
Punteggio di importanza automatico
Quando importance è impostato a 1 (o omesso), Kore calcola automaticamente il punteggio del ricordo su una scala 1--5 usando euristiche locali. Nessun LLM è necessario.
Segnali di valutazione
Il valutatore analizza il testo del contenuto e assegna l'importanza in base a molteplici segnali:
| Segnale | Effetto |
|---|---|
| Lunghezza del contenuto | Contenuti più lunghi e dettagliati ottengono un punteggio più alto |
| Parole chiave di specificità | Termini come "always", "never", "critical", "important" aumentano il punteggio |
| Peso della categoria | Le categorie decision e preference ricevono un bonus |
| Dati numerici | La presenza di numeri, date o misure aumenta il punteggio |
| Entità nominate | Parole con maiuscola (nomi, progetti) aumentano la specificità |
| Marcatori temporali | Parole come "deadline", "by Friday", "Q3" aumentano il punteggio |
| Pattern di negazione | "Do not", "never", "avoid" indicano vincoli importanti |
Distribuzione dei punteggi
In pratica, i ricordi con punteggio automatico seguono una distribuzione naturale:
| Punteggio | Frequenza | Esempio |
|---|---|---|
| 1 (Basso) | ~5% | Osservazioni banali, conversazione leggera |
| 2 (Normale) | ~35% | Fatti generali, informazioni di routine |
| 3 (Importante) | ~40% | Dettagli del progetto, preferenze, decisioni |
| 4 (Alto) | ~15% | Vincoli critici, relazioni chiave |
| 5 (Critico) | ~5% | Regole di sicurezza, decisioni architetturali |
Puoi sempre sovrascrivere il punteggio automatico impostando importance a 2--5 esplicitamente.
Ricerca semantica
La ricerca semantica utilizza modelli sentence-transformer locali per trovare ricordi concettualmente simili, anche quando le parole esatte differiscono.
Come funziona
-
Al momento del salvataggio: Il contenuto del ricordo viene trasformato in un vettore a 384 dimensioni usando il modello sentence-transformer configurato (predefinito:
paraphrase-multilingual-MiniLM-L12-v2) -
Al momento della ricerca: La query viene trasformata usando lo stesso modello, poi confrontata con tutti gli embedding salvati usando la similarità coseno
-
Ordinamento: I risultati sono ordinati per punteggio effettivo:
effective_score = cosine_similarity * decay_score * (importance / 5)
Questa formula garantisce tre proprietà:
- Rilevanza -- I ricordi semanticamente simili si posizionano più in alto
- Attualità -- I ricordi recenti si posizionano più in alto di quelli vecchi
- Importanza -- I ricordi critici si posizionano più in alto di quelli banali
Supporto multilingue
Il modello predefinito (paraphrase-multilingual-MiniLM-L12-v2) supporta oltre 50 lingue. Un ricordo salvato in italiano può essere trovato con una query in inglese, e viceversa:
# Salva in italiano
curl -X POST http://localhost:8765/save \
-d '{"content": "L'\''utente preferisce risposte concise"}'
# Cerca in inglese
curl "http://localhost:8765/search?q=user+response+preferences&semantic=true"
# Trova il ricordo in italiano
Fallback FTS5
Quando l'extra semantic non è installato, la ricerca ricade sulla ricerca full-text SQLite FTS5. FTS5 è basata su parole chiave e funziona bene per corrispondenze esatte, ma non comprende sinonimi o query cross-language.
Compressione della memoria
Il motore di compressione identifica e unisce i ricordi ridondanti per prevenire il sovraccarico di conoscenza.
Algoritmo
- Calcola la similarità coseno a coppie tra tutti gli embedding dei ricordi
- Identifica le coppie in cui la similarità supera la soglia (predefinita: 0.88)
- Per ogni coppia:
- Mantiene il ricordo con importanza più alta
- Aggiunge le informazioni uniche dal ricordo con importanza più bassa
- Archivia o elimina il ricordo ridondante
- Rigenera l'embedding del ricordo unificato
Esempio
Prima della compressione:
- Ricordo A (importanza 3): "React 19 supports server components natively"
- Ricordo B (importanza 2): "React version 19 has built-in server component support"
Similarità coseno: 0.94 (sopra la soglia di 0.88)
Dopo la compressione:
- Ricordo A (importanza 3): "React 19 supports server components natively" (mantenuto)
- Ricordo B: archiviato
Regolazione della compressione
Modifica KORE_SIMILARITY_THRESHOLD per controllare l'aggressività:
# Conservativo: unisce solo quasi-duplicati
KORE_SIMILARITY_THRESHOLD=0.95 kore
# Aggressivo: unisce ricordi vagamente simili
KORE_SIMILARITY_THRESHOLD=0.80 kore
Impostare la soglia troppo bassa (sotto 0.80) potrebbe unire ricordi che contengono informazioni distinte. Il valore predefinito di 0.88 rappresenta un buon equilibrio tra deduplicazione e preservazione delle informazioni.
Archiviazione dei dati
SQLite con WAL
Kore utilizza SQLite in modalità Write-Ahead Logging (WAL), che consente:
- Letture concorrenti durante le scritture
- Recupero dopo crash senza perdita di dati
- Database a file singolo senza dipendenze esterne
Panoramica dello schema
Il database contiene queste tabelle principali:
| Tabella | Scopo |
|---|---|
memories | Storage principale dei ricordi (contenuto, categoria, importanza, decay_score, timestamp) |
embeddings | Embedding vettoriali per la ricerca semantica |
tags | Mappatura tag-ricordo |
relations | Relazioni tra ricordi (bidirezionali) |
archive | Ricordi eliminati temporaneamente |
fts_index | Indice di ricerca full-text FTS5 |
Thread safety
Le connessioni SQLite sono gestite tramite un pool di connessioni thread-safe. Ogni richiesta ottiene la propria connessione, prevenendo race condition in scenari concorrenti.
Ciclo di vita di un ricordo
Un ricordo attraversa il seguente ciclo di vita:
Creato (decay=1.0)
│
├── Cercato/acceduto → rinforzato (decay += 0.05, half-life *= 1.15)
│
├── Passaggio di decay → decay ricalcolato
│ │
│ ├── decay > soglia → il ricordo sopravvive
│ │
│ └── decay < soglia → il ricordo viene rimosso
│
├── Compressione → unito con un ricordo simile
│
├── Archiviazione → eliminazione temporanea (ripristinabile)
│
├── TTL scaduto → rimosso dalla pulizia
│
└── Eliminazione → rimosso definitivamente
Flusso delle richieste
Un tipico flusso salvataggio-poi-ricerca:
1. Il client invia POST /save
2. Il server valida l'input (Pydantic v2)
3. Il valutatore di importanza assegna il punteggio
4. Il sentence-transformer genera l'embedding
5. SQLite salva ricordo + embedding in una transazione
6. L'indice FTS5 viene aggiornato
7. La risposta viene restituita con l'ID del ricordo
8. Il client invia GET /search?q=...&semantic=true
9. La query viene trasformata in embedding con lo stesso modello
10. La similarità coseno viene calcolata contro tutti gli embedding
11. I risultati vengono filtrati per namespace dell'agente
12. I punteggi effettivi vengono calcolati (similarity * decay * importance)
13. I risultati vengono ordinati e paginati
14. I punteggi di decay vengono rinforzati per i ricordi acceduti
15. La risposta viene restituita
Caratteristiche prestazionali
| Operazione | Latenza (tipica) |
|---|---|
| Salvataggio (con embedding) | 10--50 ms |
| Salvataggio (solo FTS5) | 1--5 ms |
| Ricerca (semantica, 1000 ricordi) | 20--100 ms |
| Ricerca (FTS5) | 1--10 ms |
| Passaggio di decay (1000 ricordi) | 50--200 ms |
| Compressione (1000 ricordi) | 200--1000 ms |
| Salvataggio batch (100 ricordi) | 100--500 ms |
Tutti i benchmark su una CPU di laptop moderna (nessuna GPU). Le prestazioni scalano linearmente con il numero di ricordi.