
Lezione di apertura che inquadra il taglio del corso e presenta il docente. Si parte dal modello mentale di fondo - un LLM senza RAG è un esperto bravissimo ma con la memoria congelata, che non ha mai visto i tuoi documenti; il RAG è dargli le pagine giuste del manuale aperto sul tavolo prima di rispondere - restando a livello intuitivo, perché il ciclo retrieve-augment-generate ha una lezione dedicata nei Fondamenti. Si presenta chi insegna e si fissa la filosofia portante: costruire RAG personalizzabili componente per componente, vedendo ogni pezzo nel codice e non dentro una libreria a scatola nera. Si enuncia la tesi del corso (misura sul tuo dato e tieni solo ciò che paga, niente RAG frankenstein) e si introducono a livello intuitivo i due progetti, un RAG naive (il progetto vanilla, un sistema minimale in una schermata) e un motore enterprise configurabile a interruttori, sempre sulla stessa knowledge base reale di sicurezza sul lavoro basata sul D.Lgs 81/2008. Proprio la concretezza della KB fa emergere dal vivo i limiti del naive: non trova il valore limite dello xilene perché quel dato sta in un PDF e in un Excel che il RAG naive non legge. Gli obiettivi operativi, il metodo di misura before/after e la mappa completa del corso sono ripresi nella lezione successiva.
Seconda lezione di inquadramento, dedicata a obiettivi, metodo e mappa del corso. Si fissa il metodo di misura before/after - si confronta lo stesso caso prima e dopo aver acceso una tecnica, cambiando una sola variabile - mostrato per intero una sola volta sulla milestone dal naive all'enterprise e poi lasciato come esercizio su dati più ampi, perché su una knowledge base piccola e pulita il numero dice poco. Si chiariscono i destinatari e i prerequisiti pratici (Python di base e una chiave OpenAI, nessuna esperienza pregressa di RAG, tutto gira su CPU usando le API). Si percorre la mappa completa del corso sezione per sezione e l'approccio didattico: si alternano lezioni di teoria che fissano il modello mentale e lezioni di pratica che accendono un interruttore e ne osservano l'effetto, riusando sempre lo stesso motore invece di riscriverlo. Si anticipa in forma leggera la cornice capacità contro qualità (alcune tecniche aggiungono ciò che prima non c'era con un salto netto, altre rifiniscono in modo incrementale e su una KB piccola spostano poco), che sarà formalizzata nella sezione di valutazione, e si ribadisce il messaggio guida: niente RAG frankenstein, si tiene solo ciò che migliora davvero il proprio caso. Chiude con cosa ti porti a casa, la promessa del corso e una checklist operativa per partire
Lezione concettuale che fissa il modello mentale di base del corso. Si definisce il RAG (Retrieval-Augmented Generation) come il modo per dare a un LLM le informazioni giuste al momento giusto: invece di sperare che il modello sappia la risposta, gliela si mette davanti. Si percorre il ciclo in tre passi sempre uguali: retrieve (data la domanda, recupera dai documenti i pochi pezzi più pertinenti), augment (costruisci il prompt mettendo quei pezzi come contesto insieme alla domanda) e generate (l'LLM risponde usando solo quel contesto e cita le fonti). Si spiega perché conviene il RAG rispetto al fine-tuning: il fine-tuning insegna uno stile o un comportamento ma è costoso da rifare a ogni aggiornamento dei documenti, non cita le fonti e tende a inventare, mentre il RAG si aggiorna cambiando i documenti, è tracciabile e può dichiarare quando un dato non c'è. Criterio di correttezza da incidere: in un RAG gran parte della qualità sta nel retrieval e non nel prompt di generazione, ed è per questo che gran parte del corso lavora sul recupero. Si introducono i mattoni di base: embedding e similarità coseno, chunk, vector store con indice in memoria e citazioni verificabili tra parentesi quadre.
Lezione di teoria che apre il cofano sul mattone su cui poggia il retrieve: l'embedding. Si parte dall'architettura di un modello linguistico per inquadrare da dove nasce il vettore: il testo non viene letto come lettere ma trasformato dal tokenizer in una sequenza di token (spesso sotto-parole), ogni token diventa un vettore nello strato di embedding e da lì molti altri strati lo elaborano, parte che si sfuma di proposito perché qui interessa l'embedding. Si chiarisce poi la distinzione chiave per il RAG: il modello di embedding del corso (text-embedding-3-small) non produce un vettore per token ma un solo vettore per l'intero testo, ed è un modello diverso dal generativo (gpt-4o-mini), con un unico compito, da testo a un vettore. Si spiega in modo intuitivo cosa vuol dire che il vettore cattura il significato: testi che dicono cose simili finiscono vicini nello spazio. La lezione si ferma qui, costruendo il modello mentale e il lessico di base; la misura della vicinanza, le dimensioni e la normalizzazione passano alla lezione successiva dedicata alle metriche. Niente codice: prepara sia la theory successiva sia il notebook visuale sugli embedding.
Lezione di teoria che trasforma la metafora spaziale dell'embedding in una regola operativa di retrieval. Si riprende il fatto che ogni testo è un punto nello spazio e si chiarisce come si confrontano davvero due punti: similarità coseno (direzione), distanza euclidea (separazione assoluta) e prodotto scalare. Si introduce senza appesantire il tema delle 1536 dimensioni come spazio comune di confronto e, soprattutto, la normalizzazione L2: gli embedding OpenAI usati nel corso sono unit-length, quindi coseno, dot product ed euclidea producono lo stesso ordinamento dei vicini. Si chiariscono anche i limiti di questa equivalenza: vale per vettori normalizzati, non risolve i match esatti e non sostituisce il chunking. Niente codice: prepara le due lezioni pratiche sul notebook visuale degli embedding, dove questa geometria viene resa visibile con plot 2D/3D.
Prima parte della pratica su notebook che rende visibile la geometria degli embedding. Si esegue `progetto_visualizzare_embedding/solution.ipynb` fino alla tabella dei vicini per similarità coseno: setup con OpenAI e tiktoken, scelta di parole inglesi molto brevi verificate a singolo token, calcolo degli embedding con text-embedding-3-small, normalizzazione L2 esplicita e confronto numerico tra coseno, dot product ed euclidea. Si verifica che i ranking dei vicini coincidano e si legge la tabella dei vicini più prossimi. La separazione dei cluster, la proiezione MDS, i grafici 2D/3D e i frammenti di testo in italiano passano alle lezioni successive. Non usa la knowledge base del corso.
Seconda parte della pratica sullo stesso notebook `solution.ipynb` della lezione precedente, tutta sull'esperimento delle parole. Si parte dalla funzione `cluster_separation`, che misura in modo onesto quanto i tre gruppi semantici tengono davvero: la similarità coseno media interna contro quella tra gruppi diversi e le due coppie estreme, con i cluster che restano separati ma a bordi sfumati. Poi si introduce l'MDS (Multi-Dimensional Scaling), la proiezione che schiaccia gli embedding da 1536 dimensioni a mappe 2D e 3D conservando il più possibile le distanze coseno; si esegue tutta la pipeline sulle parole con la verifica finale dei ranking e si disegnano i grafici interattivi 2D e 3D dei tre gruppi, ricordando che gli assi non hanno significato e che ogni proiezione è una compressione con perdita. I frammenti di testo in italiano e i takeaway finali passano alla parte successiva. Notebook e materiale didattico restano nella cartella della prima lezione; qui si registra solo la continuazione del percorso.
Terza e ultima parte della pratica sullo stesso notebook `solution.ipynb`. Chiuso l'esperimento sulle parole, si passa alla Parte 2 del notebook sui frammenti di testo in italiano (animali, trasporti, cucina), input molto più vicini al chunk reale di un RAG rispetto alle parole isolate. Si riusa esattamente la stessa pipeline - embedding, normalizzazione, le tre metriche, verifica dei ranking, separazione dei cluster - e si ridisegnano i plot 2D/3D, stavolta con il testo completo nel tooltip. Si osserva che sui frammenti i cluster si separano in modo ancora più pulito che sulle parole, perché una frase intera dà più contesto al modello. Si chiude con il collegamento esplicito al retrieve denso del RAG e con i takeaway finali della pratica visuale. Notebook e materiale didattico restano nella cartella della prima lezione; qui si registra solo la continuazione del percorso.
Prima parte dell'ultima lezione di teoria dei Fondamenti, dedicata al primo dei tre mattoni: il chunk. Si riparte dal ciclo retrieve-augment-generate e si inquadrano i tre mattoni che lo rendono eseguibile (chunk, indice, citazioni), poi ci si concentra sul chunk: i documenti interi non stanno in un prompt, quindi si spezzano in pezzi di poche centinaia di parole; ogni chunk riceve il proprio embedding e si recupera e si cita singolarmente, perciò il chunk è l'unità che si embedda, si recupera e si cita. Si spiega il taglio a dimensione fissa, la baseline del progetto vanilla: si taglia il testo ogni tot caratteri con un po' di overlap perché un'informazione a cavallo del taglio resti intera; i numeri (?800 caratteri, ?100 di overlap) torneranno identici nel codice, e il limite (il taglio non guarda il significato) rimanda alla sezione dedicata al chunking. L'indice (vector store), le citazioni, il modello mentale completo e i malintesi tipici passano alla seconda parte. Niente codice.
Seconda parte della stessa lezione di teoria, sugli altri due mattoni e sulla ricomposizione del modello mentale. Vector store / indice: dove si tengono i chunk con testo, fonte e vettore, per cercarli in fretta confrontando per similarità coseno il vettore della domanda con quelli dei chunk; nel corso è un indice in memoria salvato in un solo file SQLite per progetto (vanilla.db per il naive, enterprise.db per il motore), senza servizi esterni né Docker. Citazioni: ogni risposta riporta tra parentesi quadre il documento da cui viene l'informazione, il che la rende verificabile ed è un vantaggio strutturale del RAG sul fine-tuning; si cita a livello di chunk e la fonte viaggia accanto al testo nel prompt. Si chiude con lo schema completo che distingue indicizzazione (una volta) e interrogazione (a ogni domanda), con i malintesi tipici (dimensione del chunk, database vettoriale dedicato non obbligatorio, citare il chunk e non il documento) e con la checklist dei Fondamenti. Slide e script restano nella cartella della prima parte; niente codice: prepara la lettura del progetto vanilla nella lezione successiva.
Prima parte della pratica sul notebook `progetto_naive_rag_vanilla/solution.ipynb`. Si apre e si legge pezzo per pezzo il sorgente `rag_project/vanilla/vanilla_rag.py`: ingestione (`load_text`, `chunk_fixed`, `build`), cache in `vanilla/.cache/vanilla.db`, ciclo retrieve-augment-generate in `answer`. Poi si esegue il notebook fino alla stampa di `answer` con `inspect.getsource`: setup, costruzione o ricarico dell'indice (151 chunk solo da HTML), tabella indicizzato/IGNORATO. Output atteso: indice pronto, ciclo RAG compreso, limite di ingestione già visibile nel codice. Criterio di completamento parte 1: si sa dove avviene l'ingestione, dove sta il db e dove retrieve, augment e generate vivono in `answer`.
Seconda parte della stessa pratica, sempre sullo stesso `solution.ipynb` con l'indice già in memoria. Si fanno due domande mirate: sorveglianza sanitaria videoterminali (risposta con citazione da HTML) e valore limite dello xilene (fallimento onesto: il dato sta in Excel/PDF ignorati da `load_text`). Si chiude con il metodo before/after del corso e la checklist dei limiti voluti della baseline. Criterio di completamento: la baseline funziona su HTML, fallisce correttamente su Excel, si sa spiegare che il problema è di ingestione non di retrieval, e before/after è il metro per le sezioni successive.
Lezione di teoria che apre la sezione Ingestione introducendo il motore enterprise, il sistema che si userà per tutto il resto del corso. Dopo aver visto dal vivo i limiti del RAG naive vanilla nei Fondamenti (legge solo testo semplice, ignora PDF ed Excel), si presenta il secondo deliverable: lo stesso ciclo RAG ma con tutte le tecniche del corso dietro interruttori (flag), con un solo database per progetto (vanilla.db per il naive, enterprise.db per il motore). Si fissa il modello mentale del motore a interruttori, il cuore pratico del corso: l'ingestione enterprise si fa una volta sola e produce un indice multi-rappresentazione (chunk in più versioni, embedding, riassunti gerarchici, immagini descritte, grafo, tabelle), poi a query-time si accende o spegne un flag per volta e si osserva l'effetto sulla stessa domanda senza ricostruire l'indice. Si spiega perché si cambia un solo interruttore alla volta (isolare la variabile per vedere il contributo reale di ciascuna tecnica, come in un esperimento pulito) e si mostra lo schema costruisci-una-volta / interroga-coi-flag. La lezione non contiene codice da eseguire: prepara la prima pratica enterprise della sezione, l'ingestione ricca che troverà il valore limite dello xilene che il naive non vedeva - la prima milestone before/after del corso.
Prima parte della pratica sul notebook `progetto_sorgenti_eterogenee_doc_sezione_chunk/solution.ipynb`, dedicata alla parte del motore enterprise (pacchetto `ragcourse`) che fa l'ingestione. Prima di eseguire il notebook si apre il sorgente `rag_project/enterprise/src/ragcourse/ingest.py` e si legge come è stato costruito l'indice: `build_index` smista i file per estensione, `_parse_pdf` legge il PDF pagina per pagina (PyMuPDF), `_parse_html` ripulisce le pagine web (BeautifulSoup), `_parse_xlsx` legge l'Excel riga per riga (openpyxl); ogni chunk porta i campi di tracciabilità del modello documento -> sezione -> chunk (`doc_id`, `source_type`, `section_path`, definiti in `schema.py`), il testo è tagliato da `_chunk_fixed` e l'indice è salvato e ricaricato da `enterprise.db`. Poi si esegue il setup del notebook: import del motore, localizzazione robusta della cartella `enterprise`, `get_index()` che ricarica l'indice già costruito (1956 chunk totali) e la `DOMANDA` sullo xilene, identica a quella del naive. Prerequisito una volta sola per tutto il corso: `py -3.11 -m pip install -e rag_project/enterprise` (il motore viene consegnato qui, in questa lezione, e le pratiche enterprise successive lo riusano). Criterio di completamento parte 1: si sa come ogni formato viene letto, cos'è il modello documento-sezione-chunk e dove vive l'indice; l'indice è caricato e pronto da interrogare.
Seconda parte della stessa pratica, sempre sullo stesso `solution.ipynb` con l'indice enterprise già in memoria. Si apre il cofano dell'indice (cella di ispezione): l'indice è multi-rappresentazione - 1956 chunk totali, di cui 624 nella vista più semplice (fissi, foglia, testuali) usata oggi - e la distribuzione per `source_type` prova che tutte e tre le sorgenti (pdf, html, xlsx) sono entrate, al contrario del naive dove PDF ed Excel risultavano IGNORATO; per ogni sorgente si stampa un chunk d'esempio con la sua tracciabilità (`doc_id`, `section_path`). Poi si esegue la stessa domanda dello xilene con la configurazione più spoglia (`retriever="dense"`, `top_k=5`, `use_sql` spento) e arriva la risposta corretta - 221 mg/m3 - con citazione al foglio Excel VLEP. Si chiude con la milestone before/after del corso (da "non trovato" a "221 con fonte", un salto di capacità da zero a uno dovuto alla sola ingestione ricca) e con il ponte alla lezione successiva: la riga-come-testo basta per un dato puntuale ma non per le aggregazioni, che richiederanno il text-to-SQL. Criterio di completamento: si sa leggere la struttura tracciabile dell'indice e si sa spiegare perché ora la domanda che il naive non poteva affrontare riceve la risposta giusta.
Lezione di pratica dedicata ai dati strutturati. Si parte dal principio che le tabelle non vanno spezzettate come testo: vanno caricate come tabelle e interrogate con SQL generato dall'LLM (text-to-SQL). Operativamente, nel motore enterprise le tabelle Excel sono scritte come tabelle xl_ durante il build dell'indice e si accende il flag use_sql: a tempo di interrogazione il sistema elenca le tabelle disponibili, fa generare all'LLM un SELECT a partire dalla struttura della tabella e lo esegue. Input atteso: una domanda di aggregazione sulla knowledge base. Output atteso: una risposta esatta calcolata via SQL con la riga corretta. Esempio reale guida: qual è l'agente chimico con il valore limite più basso. Questa è un'aggregazione (ordinare e prendere il minimo) che la sola ricerca vettoriale non sa fare, mentre una query SQL la risolve in modo esatto restituendo la silice. Il punto didattico chiave: il vettoriale non aggrega, il SQL sì. Criterio di completamento: la domanda di aggregazione, prima senza risposta, ora restituisce il valore corretto leggendo la tabella come tabella e non come testo.
Lezione di teoria che apre la sezione Chunking e fissa il modello mentale prima delle pratiche (solo slide, niente codice). La tesi: il chunking è una scelta a monte del retrieval, decide quali pezzi esistono per essere recuperati, quindi decide cosa il RAG riesce a trovare. Si parte dal limite del taglio naive usato finora (pagina = sezione + finestra fissa di caratteri): cieco ai confini, spezza frasi a metà, trascina rumore (menu, toolbar HTML), tracciabilità povera (p.42). Va bene come baseline didattica, non come scelta enterprise. Si introduce il chunking gerarchico: usare la struttura del documento (Titolo > Capo > Articolo > comma) come confine, così le foglie nascono dai confini veri e portano un section_path tracciabile, con input pulito. Emerge il problema che la gerarchia da sola non risolve: non limita la dimensione delle foglie. Si introduce quindi il sotto-chunking: semantico (taglia dove cambia il significato) e un fallback duro (a capo, al punto, a finestra) che garantisce lo spezzamento anche quando il semantico non trova un confine. Si inquadrano le tre leve impilabili della sezione: dove tagli (fisso/semantico/gerarchico), come rappresenti la foglia (grezza o contextual, qualità), che gerarchia di sintesi metti sopra (RAPTOR, capacità); si combinano, e il taglio buono a monte migliora anche contextual e RAPTOR a valle. Infine la novità organizzativa, da spiegare bene: l'indice è multi-rappresentazione, si costruisce una volta e si aggiungono viste in modo incrementale; per il parsing gerarchico il motore del corso passa dalla versione 0.1 (ingestione) alla 0.2, ed è lo STESSO pacchetto ragcourse in versione più avanzata: si reinstalla con lo stesso comando (pip install -e rag_project/enterprise), non è un pacchetto diverso, e ragcourse.__version__ diventa 0.2.0. Chiude con la mappa della sezione: Chunking gerarchico (pratica, consegna v0.2) -> Contextual Retrieval (qualità) -> RAPTOR (capacità).
Lezione di pratica che apre la sezione Chunking rifacendo il parsing del motore enterprise da grezzo a professionale. Finora il chunking era il più rozzo possibile: per i PDF una pagina era una sezione e dentro la pagina il testo veniva tagliato a finestra fissa di caratteri, cieco ai confini di argomento. Qui si ricostruisce la gerarchia reale del documento: per il Testo Unico il percorso normativo Titolo > Capo > Articolo (riconoscendo i marcatori riga per riga e scartando gli header di pagina ricorrenti), per gli HTML l'articolo preso dall'header e dal contenitore di contenuto, scartando menu e toolbar. I chunk-foglia nascono dai confini veri del documento, non si spezzano più a metà frase e portano un section_path tracciabile, popolando finalmente i campi dello schema rimasti inutilizzati (section_path strutturale, level SECTION/DOC, parent_id, page). Emerge poi il problema che la gerarchia da sola non risolve: non garantisce un limite di dimensione. Le foglie troppo lunghe si sotto-dividono con il chunking semantico (confini dove cambia il significato); e poiché il semantico non spezza se le frasi non superano mai la soglia di dissimilarità, un fallback duro (a capo, al punto, a finestra di caratteri) garantisce comunque lo spezzamento sotto il budget. Si accende il flag chunker con il nuovo valore hierarchical. La lezione mostra anche la robustezza per i documenti del mondo reale a gerarchia ambigua (titoli senza numero, numerazione incoerente, marcatori misti), dove la regex sbaglia il livello: il motore estrae gli heading e fa classificare a un LLM il livello gerarchico di ciascuno, per significato e non per numerazione (classify_heading_levels in hierarchical.py), ricostruendo l'annidamento corretto. La KB include apposta un documento sporco (Manuale_SGSL_Acme.pdf) per illustrare il confronto regex contro LLM, ed e' la stessa idea che corregge i casi che un'euristica sbaglierebbe. Punto chiave operativo: la nuova vista si aggiunge all'indice con un upgrade incrementale (upgrade_index_hierarchical) che ri-embedda solo i chunk nuovi e lascia intatte le viste già costruite (fixed, semantic, contextual, RAPTOR, immagini), mostrando dal vivo come si aggiorna un indice senza ricostruirlo da capo: i chunk passano da 1956 a circa 2570. Questa lezione consegna la versione aggiornata del motore (modulo ragcourse/hierarchical.py): si reinstalla in editable da questo progetto e vale per il resto del corso. Criterio di completamento: l'upgrade aggiunge la sola vista gerarchica, lo stesso articolo è più pulito e tracciabile in vista gerarchica che in vista fixed, nessuna foglia supera il budget anche partendo da articoli lunghi, e sul documento sporco l'LLM ricostruisce la gerarchia dove la regex fallisce.
Lezione di pratica sul Contextual Retrieval di Anthropic, un asse ortogonale al taglio. Dopo aver sistemato dove tagliare con il chunking gerarchico, qui si affronta un problema diverso: un chunk preso da solo può non dire di cosa parla (la regola sui videoterminali, isolata, non nomina i videoterminali), e il suo embedding non sa di riguardarli. La tecnica non sposta i confini: antepone a ogni chunk una frase generata dall'LLM che lo situa nel documento (di cosa tratta, a quale parte appartiene), prima di calcolarne l'embedding e i termini per la ricerca a parole (contextual embeddings + contextual BM25). Si vede la frase di contesto generata dal vivo su una foglia gerarchica dell'Art. 176: lo sfondo passato all'LLM include il percorso strutturale (section_path), quindi la frase nomina l'articolo: la gerarchia alimenta il contesto. Poi si confronta la stessa domanda sulle viste fixed, hierarchical e contextual: fisso e gerarchico danno una risposta generica, il contextual fa emergere la regola specifica dell'Art. 176 e risponde correttamente (quinquennale; biennale sopra i 50 anni o per idonei con prescrizioni). Si accende il flag chunker col valore contextual e si nomina la vista professionale hierarchical_contextual, che impila taglio gerarchico (confini puliti, tracciabili) e contextual (foglie auto-descrittive). Costi e benefici: una chiamata LLM per chunk al build. Onestà didattica: è una tecnica di qualità e non di capacità, su una KB piccola e pulita il guadagno aggregato è marginale (benchmark off 1.0 -> on 1.0); il valore emerge su corpora grandi e rumorosi, e la misura su dati più ampi è lasciata come esercizio.
Lezione di pratica su RAPTOR, che risponde alle domande d'insieme dove i chunk-foglia non bastano. Il problema: i chunk-foglia rispondono bene alle domande puntuali (qual è il valore X) ma male a quelle d'insieme (di cosa parla complessivamente questa normativa), perché la risposta non sta in un pezzo solo. RAPTOR costruisce un albero ricorsivo di riassunti: raggruppa i chunk simili tra loro con un clustering (k-means via faiss.Kmeans), riassume ogni gruppo, poi raggruppa e riassume i riassunti e così via fino a pochi nodi in cima, su livelli L1 e L2. Il risultato è una gerarchia: in basso i dettagli, salendo sintesi sempre più ampie. Filo conduttore della sezione: l'albero qui viene ricostruito sulle foglie gerarchiche pulite delle lezioni precedenti (con upgrade_index_raptor_hierarchical, incrementale: sostituisce solo i nodi-riassunto), non sui tagli fissi grezzi; RAPTOR clusterizza per similarità, ma foglie ben delimitate danno riassunti più coerenti. Poi si accende il flag use_summaries. In recupero si usa il collapsed tree: tutti i livelli sono cercabili insieme, così una domanda di dettaglio colpisce una foglia e una domanda d'insieme colpisce un riassunto. Input atteso: una domanda d'insieme sulla normativa. Output atteso: il recupero di un nodo-riassunto invece di un singolo dettaglio. Criterio di completamento: una domanda d'insieme che prima restava senza risposta utile (0) ora viene soddisfatta (1) grazie ai riassunti gerarchici. RAPTOR è un riassunto cross-documento costruito per similarità, non per indice del documento.
Prima parte (di due) della pratica su ricerca ibrida e reranking: si fissano i concetti e si legge il codice del motore, senza ancora eseguire il notebook (l'esecuzione dal vivo e' la parte 2). I due modi complementari di cercare: la ricerca densa (semantica, via FAISS sugli embedding) trova per significato e recupera sorveglianza sanitaria anche se la domanda dice controlli medici dei lavoratori, ma puo' mancare i match esatti come codici e sigle; la ricerca lessicale BM25, che nel motore e' BM25Okapi di rank_bm25, e' l'evoluzione del TF-IDF: pesa la frequenza di un termine per la sua rarita' nel corpus, con due raffinamenti (la saturazione della frequenza, parametro k1, e la normalizzazione per lunghezza, parametro b), ed e' fortissima sulle parole esatte ma cieca ai sinonimi. Poiche' sbagliano in modi diversi si coprono: la ricerca ibrida le fonde con il Reciprocal Rank Fusion (RRF, k=60), che assegna punti in base alla posizione nelle due liste e non al punteggio grezzo non confrontabile. A valle, il reranking riordina i candidati con un modello piu' accurato ma piu' lento. Si legge il codice nel motore: dense_search e bm25_search in index.py, reciprocal_rank_fusion e rerank in retrieve.py. Notebook, script docente e progetto scaricabile sono nella cartella di questa parte.
Seconda parte: l'esecuzione del notebook sulla query difficile del benchmark, il valore limite di esposizione dello xilene. Si confrontano i tre valori di retriever sulla stessa domanda: la densa mette in cima la riga giusta dell'Excel VLEP e risponde 221 mg/m3, la lessicale BM25 pesca solo rumore (valori limite riferiti ad altri rischi) e l'ibrida eredita quel rumore facendo scivolare fuori dai primi cinque il chunk giusto. Poi il prima/dopo del reranking: con reranker=True la riga dello xilene torna in posizione 1 e la risposta torna corretta e citata, il top-1 raddrizzato (sul benchmark off 0.0 verso on 1.0). Si nomina il backend Google Vertex (managed, opzionale, richiede un service account) e si usa il reranker LLM senza cloud. Contro-esempio sulla query facile, il codice CAS 100-42-5: la densa gia' trova, off 1.0 verso on 1.0, nessun guadagno. Lettura finale con la distinzione capacita' contro qualita'. Notebook, script docente e progetto scaricabile restano nella cartella della parte 1; questa cartella contiene solo il video della seconda parte.
Lezione di pratica sulle tecniche di trasformazione della query, perché la domanda dell'utente spesso non è la query migliore per cercare. Si accende il flag query_transform scegliendo tra le varianti disponibili. Rewriting: riformula la domanda in modo più chiaro e completo. Multi-query: genera più varianti della domanda e cerca con tutte, fondendo i risultati (più reti, più pesci). HyDE (Hypothetical Document Embeddings): fa inventare all'LLM una risposta plausibile e poi cerca i chunk simili a quella risposta, perché spesso una risposta ipotetica assomiglia ai documenti veri più della domanda stessa. Step-back e decomposizione: si pone una domanda più generale per inquadrare il tema, oppure si spezza una domanda complessa in sotto-domande più semplici. Input atteso: una domanda da trasformare con una o più tecniche. Output atteso: il diverso insieme di chunk recuperato. Onestà didattica, da insegnare apertamente: su un corpus piccolo e pulito queste tecniche aiutano poco perché la ricerca densa già trova, e danno il meglio su corpora grandi, rumorosi o con domande difficili. È un buon esempio della distinzione capacità contro qualità: qui siamo nella qualità, quindi la misura dell'effetto è lasciata come esercizio su dati più ampi.
Prima parte: l'architettura agentica e la lettura del codice del motore, senza ancora eseguire il notebook. Si trasforma concettualmente la pipeline lineare in un vero sistema agentico su LangGraph, lo standard per orchestrare grafi di stato di passi; il flag è pattern=agentic. L'architettura segue un pattern supervisore plan-and-execute con map-reduce e reflection: un planner con router scompone la domanda in una-tre sotto-query e per ognuna sceglie la fonte giusta (vettoriale per il testo, SQL per i numeri e le tabelle, grafo per le relazioni); un fan-out parallelo manda le sotto-query in parallelo, una per worker di ricerca, usando la Send API di LangGraph (map-reduce); un grader CRAG filtra i chunk recuperati; un editor o synthesizer ricompone le risposte parziali in una risposta unica e citata (reduce); un verifier Self-RAG giudica se la risposta è sostenuta dal contesto. Si apre poi il sorgente ragcourse/agentic_graph.py e lo si legge funzione per funzione: lo stato condiviso AgentState e il reducer che fonde i risultati paralleli, il nodo plan, il fan-out con la Send API, il worker retrieve_one, il synthesize, il verify e l'arco after_verify, fino al montaggio dello StateGraph e all'entry point answer_agentic_graph. Si introduce la terminologia: StateGraph (il grafo di stato), nodo (un passo), arco condizionale (instradamento dinamico), reducer (fonde gli aggiornamenti dei worker paralleli). Notebook, script docente e progetto scaricabile sono in questa cartella; l'esecuzione dal vivo dell'agente è nella seconda parte.
Seconda parte: l'esecuzione del notebook. Si accende il flag pattern=agentic sulla domanda multi-fonte (l'agente chimico col valore limite più basso e gli obblighi di sorveglianza sanitaria) e si confronta lineare contro agentico: il lineare ammette di non sapere qual è l'agente col valore più basso, l'agentico risponde in modo completo e citato (silice cristallina, 0,025 mg/m3, più gli obblighi dell'Art. 229). Si legge il piano dal trace - le sotto-query con la fonte scelta dal router, una [sql] e una [vector] - il fan-out parallelo e la risposta unica grounded. Si rivede il diagramma dello StateGraph e la terminologia (nodo, arco condizionale, reducer), e si chiude con l'onestà del corso: l'agente non va sempre meglio (più chiamate LLM, più lento), conviene sulle domande davvero multi-fonte, e il verifier qui è spento (grounded=True di default) perché l'auto-correzione è la lezione successiva. Notebook, script docente e progetto scaricabile restano nella cartella della parte 1; questa cartella contiene solo il video della seconda parte.
Lezione di pratica sull'auto-correzione dell'agente, che si controlla da solo. Si accende il flag self_correction, che attiva il loop CRAG + Self-RAG. Corrective RAG (CRAG) agisce prima di generare: valuta i documenti recuperati e scarta quelli irrilevanti, evitando che spazzatura recuperata sporchi la risposta. Self-RAG agisce dopo aver generato: l'LLM giudica la propria risposta chiedendosi se è davvero sostenuta dal contesto (groundedness) e, se non lo è, riprova con una query riscritta in modo limitato oppure si astiene invece di inventare. La lezione importante da fissare: un retry alla cieca può peggiorare le cose, il retry giusto scatta quando manca il contesto (has_evidence) e non per una semplice rifinitura. Input atteso: una domanda-trabocchetto la cui risposta non sta nella knowledge base. Output atteso: un'astensione onesta invece di un importo o un dato inventato; un buon retry quando il contesto manca davvero. Criterio di completamento: nessuna allucinazione quando il dato non c'è, e un retry mirato solo in assenza di contesto. Si collega questa lezione all'architettura agentica della lezione precedente, dove grader e verifier sono nodi del grafo.
Lezione di pratica che chiude la sezione RAG agentico portandola all'architettura oggi considerata standard per il RAG agentico multi-fonte con memoria. Si accende il flag pattern=react. Due framework intrecciati: LangGraph regge il grafo di sessione con stato condiviso (storia dei messaggi e documenti recuperati) e checkpointer, che persiste la memoria fra i turni per thread_id; PydanticAI regge gli agenti tipati. La spina dorsale è il pattern supervisore/orchestrator-workers (Anthropic, Building Effective Agents), realizzato come ReAct (REasoning+ACTing) a tutti i livelli via tool-calling: il supervisore è a sua volta un agente ReAct i cui STRUMENTI sono i sotto-agenti specialisti (agents-as-tools, l'agent delegation di PydanticAI), e a ogni turno tool-calla l'agente giusto - Documenti (search_documents, ricerca ibrida con reranking), Tabelle (query_table, text-to-SQL), Grafo (search_graph) - ognuno a sua volta un agente ReAct PydanticAI con i propri strumenti e dependency injection, che riusano il retrieval del motore. A inizio turno una decontextualization riscrive la domanda follow-up in forma autonoma usando la storia (così un riferimento come 'quell'agente' diventa cercabile). A valle, un editor sintetizza una risposta unica e citata e un judge Self-RAG (evaluator-optimizer) verifica la groundedness, con retry e astensione esplicita. A inizio notebook un diagramma dell'architettura, spiegato nello script che passa per l'esplorazione del motore (ragcourse/react.py). Si usa con ReactChat (sessione multi-turno: .ask, .history, .documents). Dipendenze nell'extra [react] del motore (LangGraph già base, PydanticAI), importate solo da react.py: le lezioni registrate non ne dipendono. Input atteso: una conversazione di più turni, con un follow-up che usa un riferimento. Output atteso: la query riscritta dal decontextualizer (la memoria resa visibile), la risposta grounded e citata, e lo stato di sessione persistito (storia + documenti). Onestà didattica: è il pattern più costoso della sezione, conviene quando servono competenze diverse coordinate e una conversazione che ricorda. Esercizio: gestione della chat multimessaggio, checkpointer su disco, troncatura della storia; più avanti router adattivo e grading pre-generazione CRAG.
Lezione di pratica che rende le immagini contenuto recuperabile, accendendo il flag multimodal che fonde i chunk-immagine. Molti documenti hanno immagini (segnaletica di sicurezza, schemi) e la strategia usata è il caption-and-index: un modello visione-linguaggio (VLM, qui gpt-4o-mini vision) scrive una descrizione testuale di ogni immagine e si embedda la descrizione, non l'immagine. Così una domanda sulla segnaletica recupera il chunk-immagine giusto attraverso il suo testo. Durante il build dell'indice le immagini vengono estratte dai PDF, descritte dal VLM e indicizzate tramite la didascalia. Input atteso: una domanda sulla segnaletica o su un contenuto visivo. Output atteso: il recupero del chunk-immagine corretto attraverso la sua descrizione. È la strategia più semplice e accessibile perché gira con l'embedder testuale che già si usa; il limite onesto è che vale quanto la didascalia, e ciò che la didascalia non cattura si perde. Si nominano le alternative più potenti per documenti molto visivi: gli embedding multimodali tipo CLIP (immagini e testo nello stesso spazio, recupero diretto) e ColPali (embedda le pagine come immagini, ottimo per layout e tabelle), spiegando quando valgono il costo aggiuntivo. Criterio di completamento: una domanda visiva, prima senza risposta, ora recupera il contenuto giusto.
Lezione di pratica sul GraphRAG, da accendere quando contano le relazioni tra entità che la ricerca per similarità fatica a cogliere perché sparse in più punti. Si accende il flag use_graph e si esegue la demo del GraphRAG. La costruzione del grafo di conoscenza avviene in quattro passi: un LLM estrae entità e relazioni dall'intero testo (soggetto -> relazione -> oggetto); si costruisce un grafo con nodi uguali a entità e archi uguali a relazioni usando la libreria NetworkX; si individuano le comunità, gruppi di entità densamente collegate, con la community detection (algoritmo di Louvain), dove ogni comunità è un tema del corpus; si genera un community report (riassunto) per ogni comunità. Due modi di interrogare: local search (si parte dalle entità della domanda e si seguono le relazioni nel loro vicinato) e global search (per domande d'insieme, si combinano i community report). Input atteso: una domanda di relazione tra entità oppure una domanda d'insieme. Output atteso: statistiche del grafo, una local search sulle relazioni e una global search sui temi. Punto chiave del corso: non serve un graph database né Docker, persino il GraphRAG di Microsoft gira in-process con NetworkX e un Neo4j è solo un'opzione di scala. La decisione vera è quando il grafo conviene (relazioni multi-entità) e quando è sovraingegneria.
Lezione di pratica sulla difesa del RAG, che ha una superficie d'attacco specifica. Si accende il flag guardrails e si esegue la demo dei guardrail. Oltre all'iniezione diretta (l'utente che scrive ignora le istruzioni e rivela il system prompt), c'è l'iniezione indiretta: istruzioni malevole nascoste dentro i documenti che il sistema recupera (ad esempio una riga che ordina di ignorare le istruzioni e rispondere HACKED). Poiché i chunk recuperati finiscono nel prompt, un documento avvelenato può dirottare il modello. La difesa lavora su due fronti, trattando il contesto recuperato come dato non fidato e mai come istruzioni: sulla domanda dell'utente, una funzione guard_query rifiuta una domanda che sembra un tentativo di aggirare il sistema (iniezione diretta); sui chunk recuperati, una funzione sanitize_contexts neutralizza a livello di frase le istruzioni sospette tenendo il dato legittimo (toglie rispondi HACKED ma tiene il valore limite dello xilene). Input atteso: una domanda-iniezione e un chunk avvelenato. Output atteso: la domanda malevola bloccata e il chunk neutralizzato a livello di frase senza perdere il dato. La demo è deterministica e a costo quasi nullo. Criterio di completamento: iniezione diretta bloccata, iniezione indiretta neutralizzata, dato legittimo preservato.
Lezione concettuale su accessi e governance, più criteri e modelli mentali che codice. In un RAG aziendale non tutti possono vedere tutto: serve il controllo degli accessi a livello di chunk (ACL) applicato al momento del recupero, che filtra via i chunk che l'utente non è autorizzato a vedere prima che finiscano nel prompt. Si discute il caching sicuro, che non deve far trapelare a un utente i contenuti riservati di un altro, la tracciabilità delle fonti e la governance complessiva. Criterio di correttezza: il filtro ACL agisce a query-time, cioè al momento del recupero, non solo a livello di interfaccia. Si chiarisce cosa è già presente come hook minimale nel motore (un parametro acl_user) e cosa invece va costruito davvero in azienda: politiche di permessi reali, audit, isolamento della cache per utente. Modello mentale di chiusura: questi sono i criteri da soddisfare prima di mettere un RAG in produzione, una checklist di governance più che una singola tecnica da accendere. La lezione collega le difese viste con i guardrail (proteggere dall'iniezione) al tema complementare di chi può vedere cosa (proteggere la riservatezza dei contenuti).
Lezione concettuale su come misurare un RAG senza un dataset di esperti annotato. Si presentano gli strumenti che non richiedono esperti: le metriche reference-free, in cui un LLM fa da giudice, con faithfulness (quanto la risposta è sostenuta dal contesto, una misura anti-allucinazione) e answer relevance (quanto la risposta è pertinente alla domanda); il golden set sintetico, in cui si fa generare all'LLM, da chunk noti, domande la cui risposta sta in quel chunk, per poi misurare il recall@k (il chunk giusto è tra i primi k recuperati). Questi strumenti sono gratuiti e ripetibili. Si insegnano apertamente i limiti: il giudice-LLM è rumoroso, soprattutto sulle astensioni, perché un non lo so è difficile da valutare; il set sintetico ha un bias verso domande facili derivate dal chunk; l'answer correctness vera richiede dati di esperti. Quindi si misura per mostrare un effetto, non per costruire una classifica che deve sempre salire. Si fissa la cornice concettuale chiave del corso: capacità contro qualità. Le tecniche di capacità (RAPTOR, multimodale, text-to-SQL, GraphRAG, reranking, guardrail) aggiungono qualcosa che prima non c'era e mostrano un salto netto da 0 a 1; le tecniche di qualità (hybrid, query enhancement, contextual, Self-RAG) migliorano in modo incrementale e su una KB piccola e pulita non spostano l'ago, e il loro valore emerge su corpora grandi e rumorosi.
Lezione di pratica su DSPy, che ottimizza i prompt in modo dichiarativo invece di limarli a mano. Si lavora su un notebook a parte (workflow dspy_optimize) rispetto al motore. L'idea: si dichiara una Signature (cosa entra e cosa esce, ad esempio domanda più contesto -> risposta), si incapsula un Module, si sceglie una metrica e un compilatore (ad esempio BootstrapFewShot) prova da solo gli esempi e seleziona i migliori few-shot da mettere nel prompt. È come programmare con i pesi del prompt invece di ingegnerizzare frasi a mano. Input atteso: un piccolo set di esempi e una metrica obiettivo. Output atteso: il confronto di accuratezza prima e dopo l'ottimizzazione. Si mostra il prima/dopo su un piccolo set. Onestà didattica, coerente con la cornice capacità contro qualità: come le altre tecniche di qualità, il guadagno di DSPy è situazionale e va misurato sul proprio caso. Criterio di completamento: si esegue il workflow DSPy end-to-end e si legge la differenza di accuratezza prodotta dal compilatore, comprendendo che il valore va verificato sul proprio dato e non dato per scontato.
Lezione di pratica conclusiva in cui si mette insieme tutto il corso. Si configura il motore enterprise scegliendo quali interruttori accendere (chunker, retriever, reranker, query_transform, use_summaries, pattern, self_correction, multimodal, use_graph, use_sql, guardrails) per uno scenario reale, decidendo per ciascuna tecnica se serve davvero e perché, senza adottare tutto a prescindere: niente RAG frankenstein. Si applica la tesi del corso: si misura sul proprio dato e si tiene solo ciò che paga. Input atteso: uno scenario reale con le sue domande tipiche e i suoi vincoli (costo, latenza, tipo di documenti, presenza di tabelle o immagini o relazioni). Output atteso: una configurazione motivata del motore, con la lista degli interruttori accesi e la ragione di ciascuno. Si ragiona su come comporre le tecniche viste: ingestione ricca e text-to-SQL per i dati strutturati, hybrid e reranking dove il recupero è rumoroso, RAPTOR per le domande d'insieme, agentico con CRAG e Self-RAG dove servono più fonti e auto-controllo, multimodale e GraphRAG quando il dominio lo richiede, guardrail e ACL prima della produzione. La lezione chiude con una roadmap post-corso per portare il sistema in produzione, ribadendo il metodo: sostituire la knowledge base, ricostruire l'indice una volta e rivalutare quali interruttori accendere.
Un corso pratico per chi vuole costruire sistemi RAG enterprise in Python, vedendo ogni componente nel codice e non dentro una libreria-scatola nera.
Si parte da un RAG naive in una schermata e, una tecnica alla volta, si arriva a un motore enterprise agentico, sempre sulla stessa knowledge base realistica.
Capirai il ciclo retrieve-augment-generate e quando il RAG conviene rispetto al fine-tuning, poi costruirai una baseline misurabile e ne osserverai i limiti. Da lì affronterai l'ingestione di sorgenti eterogenee con il modello documento-sezione-chunk e il text-to-SQL sulle tabelle, le strategie di chunking (fisso, semantico, Contextual Retrieval) e i riassunti gerarchici con RAPTOR. Implementerai la ricerca ibrida con vettori densi, BM25 e Reciprocal Rank Fusion, il reranking, e le tecniche di query enhancement come rewriting, multi-query, HyDE e decomposizione. Trasformerai la pipeline lineare in un sistema agentico su LangGraph con planner, router, fan-out parallelo, editor e verifier, e aggiungerai l'auto-correzione CRAG e Self-RAG.
Renderai il sistema multimodale con il caption-and-index e relazionale con un GraphRAG realistico su NetworkX. Difenderai il RAG dal prompt injection diretto e indiretto, imposterai ACL e governance, e imparerai a valutare senza un dataset di esperti con metriche reference-free, golden set sintetico e recall@k, conoscendone i limiti.
Chiuderai ottimizzando i prompt con DSPy e con un capstone in cui configuri il motore per uno scenario reale, scegliendo cosa accendere e perché.
La tesi del corso è una sola: misura sui tuoi dati e tieni solo ciò che paga, senza adottare tutto a prescindere. Disclaimer: eventuali nomi, loghi e marchi citati nel corso appartengono ai rispettivi proprietari e sono usati a puro scopo didattico.