Quando ho iniziato a lavorare sulla memoria di GiuliaAI, il problema sembrava abbastanza semplice: salvare le conversazioni e recuperare i ricordi più pertinenti prima di generare una risposta.
In pratica, una live Twitch rende questo approccio insufficiente quasi subito.
Non esiste un solo interlocutore. In pochi secondi possono scrivere persone diverse, arrivare eventi della piattaforma, comparire riferimenti a messaggi precedenti e nascere conversazioni che coinvolgono più spettatori. Una frase può essere un fatto personale, una battuta, una domanda su un altro utente oppure il seguito di qualcosa accaduto due ore prima.
Per questo non tratto la memoria di GiuliaAI come un unico archivio da interrogare con una ricerca semantica. Ho preferito separarla in livelli, ciascuno con una funzione precisa, un diverso grado di autorità e un proprio ciclo di vita.
L’obiettivo non è ricordare più cose possibile. È recuperare l’informazione corretta, relativa alla persona corretta, nel momento in cui serve e senza rallentare la live.
Il problema da risolvere
Una memoria destinata a una chat pubblica deve risolvere almeno quattro problemi.
- Il primo è l’attribuzione. Se Alice scrive che Bob vive a Milano, il sistema non deve trasformare quella frase in un fatto personale di Alice. Non deve neppure considerarla automaticamente vera per Bob.
- Il secondo è la separazione temporale. Quello che è successo pochi secondi fa, ciò che è avvenuto all’inizio della live e un ricordo consolidato mesi prima non possono avere lo stesso peso.
- Il terzo è la latenza. Il retrieval avviene mentre Giulia deve rispondere. Una memoria molto completa ma lenta sarebbe poco utile in diretta.
- Il quarto è la possibilità di correggere o cancellare un’informazione. Un ricordo può essere sbagliato, superato oppure non più desiderato dal suo proprietario.
Da questi vincoli deriva l’intera architettura.
Struttura generale

Figura 1. Flusso semplificato del retrieval, della generazione della risposta e del consolidamento asincrono della memoria.
Il percorso principale parte da un batch di messaggi della live. Il server recupera la cronologia recente, analizza il contenuto del batch e decide quali sorgenti interrogare.
Le sorgenti disponibili non sono equivalenti. Possono includere il profilo di uno spettatore, ricordi mirati relativi a quella persona, episodi precedenti della live, informazioni canoniche su Giulia e dati strutturati dell’applicazione.
I risultati vengono recuperati in parallelo, deduplicati e ridotti entro un budget massimo. Solo a quel punto vengono aggiunti al prompt usato per generare la risposta.
La scrittura segue un percorso separato. La conversazione viene salvata in modo durevole e il consolidamento semantico avviene in background. La risposta non resta bloccata in attesa che tutta la memoria venga elaborata.
1. La cronologia recente come working memory
Il primo livello è la cronologia recente della live.
Ogni trasmissione possiede un identificatore di sessione. Quando il server dispone dell’identificatore reale della live usa quello; negli altri casi crea un’identità temporanea valida per la sessione corrente.
Questa separazione evita che due live differenti condividano accidentalmente la stessa cronologia.
La history recente rappresenta il presente della conversazione. Ha priorità sui ricordi semantici perché contiene le informazioni più vicine al turno corrente.
Se un vecchio ricordo indica che una persona vive a Roma, ma durante la live la stessa persona comunica di essersi trasferita a Torino, il messaggio recente deve prevalere. La memoria semantica viene trattata come una fonte utile ma potenzialmente non aggiornata, non come una verità assoluta.
La working memory serve anche a evitare retrieval inutili. Se un riferimento a qualcosa avvenuto poco prima è già coperto dalla cronologia caricata nel prompt, non ha senso cercare lo stesso episodio in un archivio esterno.
2. Una memoria separata per ogni spettatore
Ogni viewer dispone di uno spazio di memoria isolato, identificabile concettualmente in questo modo:
viewer-nomeutente
Prima di creare questo spazio, l’identità viene normalizzata. Lo username viene ripulito e uniformato, mentre l’identificatore della piattaforma e gli identificatori dei messaggi rimangono associati come metadati di provenienza.
Nel contenitore personale vengono inviati soltanto i messaggi scritti direttamente dal viewer. Non includo le risposte di Giulia, i messaggi degli altri utenti, i bot o gli eventi sintetici del sistema.
Questa scelta riduce il rischio di contaminazione tra identità.
Un singolo turno può contenere, per esempio:
>Alice: Io lavoro come grafica.
>Bob: Alice vive a Roma.
>GiuliaAI: Quindi Alice crea loghi dalla cima del Colosseo?
Nel documento personale di Alice entra soltanto la prima frase. La seconda è un’affermazione di Bob su Alice e non viene considerata una dichiarazione personale di Alice. La risposta di Giulia non viene usata per creare nuovi fatti.
Il motore semantico riceve quindi documenti già separati per identità e accompagnati da regole di consolidamento. Deve privilegiare affermazioni durevoli fatte dall’utente su se stesso e ignorare, per quanto possibile, domande, saluti, sarcasmo, roleplay e reazioni momentanee.

Figura 2. Lo stesso batch viene diviso per autore prima dell’ingestione. Ogni memoria personale contiene soltanto i messaggi prodotti dal relativo viewer.
La memoria personale viene poi letta in due forme.
La prima è un profilo sintetico con un numero ristretto di informazioni stabili o recenti. La seconda è una ricerca mirata costruita sul messaggio corrente.
Il profilo risponde alla domanda generale “chi è questa persona?”. La ricerca mirata risponde invece a “quali ricordi di questa persona sono utili adesso?”.
3. La memoria episodica della live
Non tutto ciò che accade durante una live appartiene a un singolo spettatore.
Una discussione collettiva, una decisione presa da Giulia, una gag nata tra più persone o un evento della piattaforma fanno parte della storia della trasmissione. Per conservare questa continuità utilizzo una memoria episodica separata per ogni stream:
stream-identificatore-sessione
In questo spazio viene salvato il turno pubblico completo, con i messaggi del batch, i partecipanti, i timestamp e la risposta generata da Giulia.
La memoria episodica entra in gioco quando qualcuno richiama qualcosa che non è più presente nella finestra recente, per esempio:
>Cosa era successo prima con quel gioco?
oppure:
>Ti ricordi cosa avevi detto all'inizio della live?
Il server cerca prima di ricavare il soggetto della richiesta. Controlla poi se i termini principali sono già presenti nella history recente. Se il riferimento è coperto, usa la working memory. Se sembra appartenere a una parte più lontana della stessa live, abilita la ricerca nel contenitore episodico.
In questo modo la finestra temporale può essere estesa senza inserire continuamente nel prompt l’intera trasmissione.
4. Lore e conoscenza canonica
La storia di Giulia, le sue origini, i rapporti canonici e gli elementi editoriali del personaggio vivono in un’area globale separata.
Anche questa memoria non viene interrogata a ogni messaggio. Il pianificatore cerca riferimenti alla lore, alle entità principali del progetto o a domande sulla storia e sulla personalità di Giulia.
La decisione avviene localmente attraverso regole deterministiche. Non viene effettuata una seconda chiamata al modello soltanto per decidere se cercare nella lore.
Questa soluzione ha due vantaggi. Riduce latenza e costo, ma soprattutto impedisce che una frase casuale della chat venga trattata come parte del canone ufficiale.
5. Dati strutturati e ricordi semantici non sono la stessa cosa
Uno degli aspetti che considero più importanti è la separazione tra memoria semantica e stato applicativo.
Richieste degli spettatori, relazioni tra utenti, note, dati del canale e inside joke possono avere uno stato preciso. Una richiesta può essere aperta o completata. Una nota può avere una scadenza. Una relazione può essere aggiornata oppure eliminata.
Queste informazioni vengono conservate nel database relazionale e modificate attraverso strumenti dedicati.
La memoria semantica è adatta a recuperare contenuti approssimativi, contestuali e formulati in linguaggio naturale. Il database relazionale deve invece rappresentare lo stato corrente e verificabile.
Nel prompt questa gerarchia viene resa esplicita. Quando il modello deve conoscere lo stato di un dato strutturato, non deve dedurlo da un vecchio ricordo ma interrogare la fonte canonica.
Questa distinzione evita molti conflitti. Un record aggiornato non deve competere con una frase semanticamente simile ma ormai superata.
6. Il pianificatore del retrieval
Prima di interrogare la memoria, il server analizza il batch corrente e costruisce un piano.
Non tutti gli utenti presenti hanno la stessa rilevanza. Ogni viewer riceve un punteggio basato su segnali osservabili, tra cui:
- una richiesta esplicita di ricordare qualcosa;
- un evento importante della piattaforma;
- una menzione diretta a Giulia;
- la presenza di una domanda;
- un contenuto multimediale;
- una menzione ricevuta da un altro viewer;
- la partecipazione attiva nel batch.
Le richieste esplicite hanno la priorità maggiore. Seguono gli eventi importanti e i messaggi direttamente rivolti a Giulia.
Il numero di viewer selezionabili è limitato. La configurazione predefinita ne considera tre, con un limite massimo di cinque. Questo impedisce che un batch molto affollato produca decine di ricerche contemporanee.
Il piano finale può richiedere, per esempio:
profilo di Alice
ricordi mirati di Alice
ricordi mirati di Bob
memoria della live corrente
lore di Giulia
In un altro turno può non essere necessario interrogare alcuna memoria semantica.

Figura 3. Il pianificatore sceglie le sorgenti in base al contenuto del batch, alla rilevanza dei viewer e alla copertura già offerta dalla cronologia recente.
Questa fase è il centro dell’architettura. Il problema non è soltanto trovare un testo simile. Bisogna decidere prima dove cercarlo.
7. Retrieval parallelo e tempo massimo
Una volta costruito il piano, le letture vengono eseguite in parallelo.
Per ogni viewer selezionato possono partire sia il recupero del profilo sia una ricerca mirata. Le due operazioni condividono lo stesso scope logico e possono riutilizzare risultati già disponibili.
Le cache hanno durate differenti in base al tipo di informazione. I profili restano validi più a lungo delle query legate al singolo messaggio, mentre la lore può utilizzare una finestra ancora maggiore.
Il sistema evita anche di avviare più volte la stessa richiesta se un’operazione identica è già in corso.
Ogni sorgente dispone di un timeout e l’intero stage della memoria ha un budget complessivo. Nella configurazione corrente il valore predefinito è circa un secondo.
Se una sorgente non risponde in tempo, il turno non viene bloccato. Il server usa i risultati già disponibili e prosegue in modalità degradata.
In una live è preferibile una risposta con meno memoria rispetto a una risposta completa che arriva quando la conversazione è già andata avanti.
Nel caso di una richiesta esplicita come “cosa ricordi di me?”, un errore di retrieval viene comunicato al modello. La risposta deve ammettere di non avere dati disponibili, senza inventare dettagli per riempire il vuoto.
8. Deduplicazione e budget del prompt
I risultati recuperati non vengono aggiunti direttamente al prompt.
Prima vengono normalizzati e deduplicati. Il sistema confronta i fatti del profilo con i risultati della ricerca mirata, controlla le sovrapposizioni tra sorgenti e rimuove ciò che è già presente nella cronologia recente.
La deduplicazione ignora differenze non significative di maiuscole, punteggiatura e spaziatura.
Il blocco dinamico della memoria ha inoltre una dimensione massima. L’obiettivo non è trasferire al modello tutto ciò che il sistema conosce, ma una selezione ridotta e pertinente.
Nel prompt mantengo separati almeno due concetti:
Profilo viewer pertinente
Richiamo memoria mirato
Il primo contiene informazioni generali sulla persona. Il secondo raccoglie ricordi scelti per il turno corrente.
La separazione rende più chiaro al modello il ruolo di ciascuna informazione e riduce il rischio che un fatto generico venga interpretato come risposta diretta alla domanda.
9. Persistenza e consolidamento asincrono
La generazione della risposta e il salvataggio della memoria non fanno parte dello stesso percorso sincrono.
Dopo il turno viene creato un job durevole associato a un identificatore univoco. Il primo passaggio salva la conversazione e i relativi metadati nel database relazionale.
Successivamente viene accodato un secondo lavoro dedicato all’ingestione semantica.
Questa coda svolge anche la funzione di outbox. Se il processo si interrompe dopo aver risposto ma prima di completare il consolidamento, il job rimane registrato e può essere recuperato.
Le chiavi dei job dipendono dall’identificatore del turno. L’operazione è quindi idempotente e un retry non dovrebbe creare copie multiple dello stesso documento.
Il worker distingue gli errori temporanei da quelli permanenti. I primi vengono riprovati con un ritardo progressivo. Gli errori di validazione portano invece il job in uno stato terminale. Sono previsti anche il recupero dei job rimasti bloccati e un numero massimo di tentativi.

Figura 4. La risposta viene restituita prima del consolidamento. La coda durevole gestisce salvataggio, retry, idempotenza e recupero dei job interrotti.
10. Due rappresentazioni dello stesso turno
Durante l’ingestione, uno stesso turno produce due famiglie di documenti.
La prima è personale. I messaggi vengono raggruppati per identità e ogni documento contiene soltanto ciò che ha scritto uno specifico viewer.
La seconda è episodica. Il documento conserva il turno pubblico completo e viene associato alla live corrente.
La stessa conversazione viene quindi osservata da due prospettive:
memoria personale: cosa ha detto questo viewer su se stesso
memoria episodica: cosa è successo durante questa live
Questa duplicazione è intenzionale. Non si tratta di due copie intercambiabili, perché rispondono a domande differenti.
Un unico documento contenente tutti i partecipanti sarebbe più semplice da salvare, ma renderebbe molto più difficile attribuire correttamente i fatti personali.
11. Evitare doppie fonti di verità
Un problema delicato compare quando durante il turno viene modificato un dato strutturato.
Immaginiamo che Giulia registri nel database una relazione tra due viewer. Gli stessi messaggi potrebbero essere inviati anche al consolidamento semantico e produrre una seconda versione testuale della relazione.
Da quel momento esisterebbero due fonti concorrenti:
record strutturato aggiornabile
ricordo semantico derivato dalla conversazione
Se il record venisse modificato, il ricordo precedente potrebbe continuare a comparire nei risultati.
Per ridurre questo rischio, dopo l’esecuzione degli strumenti il server costruisce una barriera di ingestione. La barriera identifica i domini strutturati modificati durante il turno e può escludere dalla memoria personale i viewer coinvolti o l’intero batch interessato.
Il motore semantico non aggiorna direttamente lo stato canonico. Quando necessario, evita semplicemente di creare una copia concorrente.
12. Correzione, oblio e cancellazione
Una memoria utile deve poter essere modificata.
Il sistema permette di correggere un singolo ricordo partendo dal suo identificatore o dal contenuto originale. Quando la provenienza è nota, il documento sorgente può essere eliminato e sostituito con un documento di correzione collegato al turno originale.
La correzione conserva i riferimenti necessari per ricostruire la provenienza e assegna priorità alla nuova informazione.
È inoltre possibile dimenticare un singolo ricordo, eliminare i documenti prodotti da uno specifico turno oppure cancellare l’intero spazio personale di un viewer.
Dopo ogni modifica, le cache relative all’identità coinvolta vengono invalidate. Questo evita che una copia locale continui a restituire un ricordo appena corretto o eliminato.
La cancellazione completa è un’operazione amministrativa, mentre le correzioni effettuate durante una conversazione vengono autorizzate soltanto quando il viewer interessato è effettivamente presente nel batch.
Perché non uso un unico archivio semantico
Salvare tutta la chat in un solo contenitore sarebbe più rapido da implementare, ma introdurrebbe diversi problemi.
Aumenterebbe il rischio di contaminazione tra utenti, renderebbe più rumorosa la ricerca e mescolerebbe fatti personali, episodi della live, lore e stato applicativo.
Un risultato semanticamente simile non è necessariamente corretto. Può appartenere alla persona sbagliata, a una live diversa o a una versione ormai superata di un dato.
Per questo la memoria di GiuliaAI non è un semplice archivio vettoriale. È un sistema di routing tra fonti diverse, con regole di priorità e cicli di vita separati.
Limiti e valutazione futura
L’architettura è già operativa, ma non considero il lavoro concluso.
Il routing si basa principalmente su regole deterministiche. Questa scelta rende il comportamento veloce, osservabile e prevedibile, ma può perdere riferimenti formulati in modo molto indiretto.
Anche il consolidamento semantico non è infallibile. L’isolamento dei documenti riduce gli errori di attribuzione, ma il sistema deve comunque distinguere tra fatti durevoli, ironia, roleplay e affermazioni momentanee.
Il passaggio successivo sarà una valutazione quantitativa su un dataset annotato. Le metriche più utili saranno:
- precisione del retrieval;
- tasso di attribuzione errata;
- percentuale di ricordi non pertinenti inseriti nel prompt;
- capacità di recuperare episodi non più presenti nella history recente;
- latenza media e percentile 95 dello stage memoria;
- comportamento in caso di timeout;
- confronto tra l’architettura completa e versioni prive dei singoli livelli.
L’ablation test sarà particolarmente importante. Togliendo, uno alla volta, il profilo sintetico, la memoria episodica, il ranking dei viewer o la deduplicazione sarà possibile misurare quanto ogni componente contribuisce davvero al risultato finale.

Figura 5. Piano di valutazione dell’architettura attraverso precisione, attribuzione, latenza e ablation test.
Conclusione
La parte difficile della memoria non è conservare molte conversazioni. È stabilire quale informazione abbia senso recuperare in uno specifico turno.
Nel sistema che ho costruito, la cronologia recente rappresenta il presente. La memoria episodica mantiene la continuità della live. Gli spazi personali raccolgono i ricordi dei viewer. La lore conserva il canone di Giulia. Il database relazionale gestisce lo stato strutturato e aggiornabile.
Il retrieval avviene soltanto quando serve e resta entro limiti precisi di tempo e dimensione. La persistenza viene separata dalla risposta e completata in background attraverso una coda durevole.
È un’architettura più complessa rispetto a un unico archivio semantico, ma riflette meglio la natura di una live Twitch: pubblica, veloce, multiutente e soggetta a continui cambiamenti di contesto.
