Passa al contenuto principale

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 importanza
  • e = 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:

ImportanzaEtichettaEmivitaDecay al 50%Decay al 10%
1Bassa7 giorni1 settimana~23 giorni
2Normale14 giorni2 settimane~46 giorni
3Importante30 giorni1 mese~100 giorni
4Alta90 giorni3 mesi~299 giorni
5Critica365 giorni1 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:

  1. Itera su tutti i ricordi attivi
  2. Ricalcola il decay_score di ciascun ricordo usando la formula sopra
  3. Rimuove i ricordi il cui decay_score scende sotto una soglia minima (effettivamente dimenticati)
  4. Aggiorna il database in un'unica transazione
suggerimento

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:

SegnaleEffetto
Lunghezza del contenutoContenuti più lunghi e dettagliati ottengono un punteggio più alto
Parole chiave di specificitàTermini come "always", "never", "critical", "important" aumentano il punteggio
Peso della categoriaLe categorie decision e preference ricevono un bonus
Dati numericiLa presenza di numeri, date o misure aumenta il punteggio
Entità nominateParole con maiuscola (nomi, progetti) aumentano la specificità
Marcatori temporaliParole 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:

PunteggioFrequenzaEsempio
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

  1. 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)

  2. Al momento della ricerca: La query viene trasformata usando lo stesso modello, poi confrontata con tutti gli embedding salvati usando la similarità coseno

  3. 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

  1. Calcola la similarità coseno a coppie tra tutti gli embedding dei ricordi
  2. Identifica le coppie in cui la similarità supera la soglia (predefinita: 0.88)
  3. 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
  4. 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
warning

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:

TabellaScopo
memoriesStorage principale dei ricordi (contenuto, categoria, importanza, decay_score, timestamp)
embeddingsEmbedding vettoriali per la ricerca semantica
tagsMappatura tag-ricordo
relationsRelazioni tra ricordi (bidirezionali)
archiveRicordi eliminati temporaneamente
fts_indexIndice 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

OperazioneLatenza (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.