From 638ba17629d5cf0c6a3a5b40600a1b88f68425c6 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 12 Apr 2026 23:53:13 +0200 Subject: [PATCH] Inital commit --- README.md | 658 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..21b24a6 --- /dev/null +++ b/README.md @@ -0,0 +1,658 @@ +# RAG from Scratch — Singolo PDF Generico + +Sistema RAG (Retrieval-Augmented Generation) costruito da zero, senza framework di alto livello. +Funziona su qualsiasi PDF digitale. Gira interamente in locale, senza GPU, senza cloud. + +**Stack:** Python · Ollama · nomic-embed-text · Qwen3 8B · ChromaDB +**Compatibile con:** Linux · macOS · Windows (WSL2) · CPU Only · ~8 GB RAM libera + +--- + +## Indice + +- [Panoramica](#panoramica) +- [Struttura del progetto](#struttura-del-progetto) +- [Gli step](#gli-step) + - [Step 0 — Scegli il PDF](#step-0--scegli-il-pdf) + - [Step 1 — Ispezione automatica](#step-1--ispezione-automatica) + - [Step 2 — Conversione in Markdown grezzo](#step-2--conversione-in-markdown-grezzo) + - [Step 3 — Rilevamento struttura](#step-3--rilevamento-struttura) + - [Step 4 — Revisione manuale](#step-4--revisione-manuale) + - [Step 5 — Chunking adattivo](#step-5--chunking-adattivo) + - [Step 6 — Verifica chunk](#step-6--verifica-chunk) + - [Step 7 — Installazione ambiente](#step-7--installazione-ambiente) + - [Step 8 — Vettorizzazione](#step-8--vettorizzazione) + - [Step 9 — Pipeline RAG](#step-9--pipeline-rag) + - [Step 10 — Test automatici](#step-10--test-automatici) +- [Principi di progettazione](#principi-di-progettazione) + +--- + +## Panoramica + +``` +PDF + └─► STEP 1 Ispezione automatica + └─► STEP 2 Conversione in Markdown grezzo + └─► STEP 3 Rilevamento struttura + └─► STEP 4 Revisione manuale ← step più importante + └─► STEP 5 Chunking adattivo + └─► STEP 6 Verifica chunk + └─► STEP 8 Vettorizzazione + └─► STEP 9 Pipeline RAG + └─► STEP 10 Test automatici + +STEP 0 Prerequisito iniziale (PDF adatto) +STEP 7 Prerequisito tecnico (ambiente locale) +``` + +### Dove si concentra il rischio + +| Step | Rischio | Motivo | +|---|---|---| +| Step 0 | 🔴 Alto | Un PDF inadatto invalida tutto il lavoro successivo | +| Step 1 | 🟢 Basso | Automatico, solo osservazione | +| Step 2 | 🟢 Basso | Automatico, tool maturo | +| Step 3 | 🟢 Basso | Automatico, solo analisi | +| Step 4 | 🔴 Alto | Manuale — la qualità del MD determina la qualità del RAG | +| Step 5 | 🟡 Medio | Logica adattiva, dipende dalla qualità del MD | +| Step 6 | 🟢 Basso | Automatico, solo verifica | +| Step 7 | 🟢 Basso | Installazione standard | +| Step 8 | 🟢 Basso | Meccanico, lento ma affidabile | +| Step 9 | 🟡 Medio | Qualità del prompt | +| Step 10 | 🟢 Basso | Test automatici | + +--- + +## Struttura del progetto + +``` +rag-from-scratch/ +│ +├── sources/ # PDF originali — non modificare mai +│ └── documento.pdf +│ +├── processed/ # Output di ogni step per ogni documento +│ └── documento/ +│ ├── raw.md # MD grezzo da marker (non toccare) +│ ├── clean.md # MD revisionato a mano +│ ├── structure_profile.json # Profilo struttura da step 3 +│ └── chunks.json # Chunk pronti per la vettorizzazione +│ +├── scripts/ +│ ├── inspect.py # Step 1 — ispezione PDF +│ ├── detect_structure.py # Step 3 — analisi struttura MD +│ ├── chunker.py # Step 5 — chunking adattivo +│ ├── verify_chunks.py # Step 6 — verifica chunk +│ ├── ingest.py # Step 8 — vettorizzazione +│ ├── rag.py # Step 9 — pipeline RAG +│ └── test_pipeline.py # Step 10 — test automatici +│ +├── chroma_db/ # Vector store — generato da ingest.py +├── notes/ +│ └── revision_log.md # Log delle modifiche manuali al MD +├── requirements.txt +├── .gitignore +└── README.md +``` + +--- + +## Gli step + +--- + +### Step 0 — Scegli il PDF + +**Tipo:** prerequisito manuale +**Input:** nessuno +**Output:** un PDF adatto al sistema + +Il PDF deve soddisfare requisiti minimi prima di qualsiasi elaborazione. +Un PDF inadatto rende tutto il lavoro successivo inutile. + +**Criteri obbligatori:** + +- Il testo è selezionabile nel PDF reader — se non riesci a copiare una parola, + pdfplumber non la leggerà +- Non è protetto da password +- È generato digitalmente, non scansionato — una foto di un libro non è un PDF di testo +- Il contenuto importante è nel testo, non nelle immagini + +**Criteri desiderabili:** + +- Ha una struttura logica riconoscibile: capitoli, sezioni, paragrafi +- Le sezioni hanno titoli espliciti +- Non ha layout a colonne multiple +- È in una lingua sola o prevalentemente una + +**Come verificarlo:** +Apri il PDF nel tuo reader, seleziona del testo da pagine diverse e copialo. +Se il testo copiato è leggibile e nell'ordine giusto, il PDF è adatto. +Se ottieni caratteri strani o testo nell'ordine sbagliato, il PDF ha problemi. + +--- + +### Step 1 — Ispezione automatica + +**Tipo:** automatico +**Input:** `sources/documento.pdf` +**Output:** report testuale con score e lista problemi +**Script:** `scripts/inspect.py` + +```bash +python scripts/inspect.py sources/documento.pdf --save +``` + +Lo script analizza il PDF pagina per pagina e produce un report. +Serve per capire la qualità del documento e mappare i problemi +prima di affrontare la revisione manuale. + +**Cosa rileva:** + +- Testo non estraibile (pagine con sole immagini) +- Sillabazioni a fine riga +- Layout a colonne (righe molto corte e numerose) +- Intestazioni e piè di pagina ripetitivi +- Caratteri Unicode anomali +- Pagine vuote + +**Output del report:** + +``` +Score: 87/100 +Pagine totali: 243 +Pagine con problemi: 12 + + Pagina 14: sillabazione rilevata (3 occorrenze) + Pagina 67: possibile layout a colonne + Pagina 201: caratteri Unicode anomali + +PROSSIMI PASSI: + → conversione con marker funzionerà bene + → attenzione alle pagine 14 e 67 nella revisione manuale +``` + +**Decisione:** + +| Score | Azione | +|---|---| +| ≥ 70 | Procedi allo step 2 | +| 40–70 | Procedi con cautela, revisione estesa necessaria | +| < 40 | Valuta una fonte PDF migliore | + +--- + +### Step 2 — Conversione in Markdown grezzo + +**Tipo:** automatico +**Input:** `sources/documento.pdf` +**Output:** `processed/documento/raw.md` +**Tool:** marker-pdf + +```bash +marker_single sources/documento.pdf processed/documento/raw.md +``` + +Converte il PDF in Markdown. Il risultato non è perfetto — è la base +su cui lavorerai nello step 4. + +**Regola fondamentale:** `raw.md` non va mai modificato. +È il punto di partenza di riferimento. Se qualcosa va storto +nella revisione, puoi sempre ripartire da qui. + +```bash +# Crea subito la copia su cui lavorare +cp processed/documento/raw.md processed/documento/clean.md +``` + +**Cosa produce marker:** + +- Titoli riconosciuti e convertiti in `#` `##` `###` +- Paragrafi separati da righe vuote +- Sillabazione parzialmente risolta + +**Cosa non produce marker:** + +- Rimozione intestazioni e piè di pagina +- Correzione completa del layout a colonne +- Descrizione del contenuto delle immagini + +--- + +### Step 3 — Rilevamento struttura + +**Tipo:** automatico +**Input:** `processed/documento/raw.md` +**Output:** `processed/documento/structure_profile.json` +**Script:** `scripts/detect_structure.py` + +```bash +python scripts/detect_structure.py processed/documento/raw.md +``` + +Analizza la struttura del Markdown grezzo senza modificarlo. +Il profilo prodotto guida sia la revisione manuale che il chunker. + +**I quattro livelli strutturali:** + +``` +Livello 3 — struttura ricca + Il documento ha ### con regolarità. + Ogni ### è un'unità semantica chiara. + Esempi: opere filosofiche, manuali tecnici, leggi. + Strategia chunking: boundary su ### + +Livello 2 — struttura parziale + Il documento ha ## ma pochi o nessun ###. + Le sezioni sono i capitoli, non le sottosezioni. + Esempi: articoli scientifici, report, saggi. + Strategia chunking: boundary su ##, split interno su paragrafi + +Livello 1 — solo paragrafi + Il documento non ha titoli significativi. + La struttura è data dalle righe vuote. + Esempi: testi narrativi, lettere, trascrizioni. + Strategia chunking: boundary su paragrafo + +Livello 0 — testo piatto + Un blocco continuo senza struttura riconoscibile. + Esempi: PDF mal convertiti, testi antichi. + Strategia chunking: sliding window su frasi +``` + +**Profilo prodotto:** + +```json +{ + "livello_struttura": 3, + "n_h1": 1, + "n_h2": 9, + "n_h3": 296, + "n_paragrafi": 312, + "boundary_primario": "h3", + "lingua_rilevata": "it", + "lunghezza_media_sezione": 420, + "strategia_chunking": "h3_aware", + "avvertenze": [ + "14 sezioni sotto i 200 caratteri — verranno accorpate", + "8 sezioni sopra i 800 caratteri — verranno divise" + ] +} +``` + +--- + +### Step 4 — Revisione manuale + +**Tipo:** manuale +**Input:** `processed/documento/raw.md` + profilo struttura +**Output:** `processed/documento/clean.md` +**Tool:** il tuo editor di testo + +> Questo è lo step più importante dell'intera pipeline. +> La qualità del RAG dipende da questo step più di qualsiasi +> parametro tecnico o scelta di modello. + +Apri `clean.md` nel tuo editor e lavora sezione per sezione, +guidato dal report dello step 1 e dal profilo dello step 3. + +**Struttura target del MD pulito:** + +```markdown +# Titolo del documento + +## Sezione principale + +### Sottosezione o unità atomica + +Testo fluente, frasi complete, nessun artefatto. +Ogni paragrafo è semanticamente autonomo. +Una riga vuota separa le sezioni. + +### Sottosezione successiva + +Testo della sottosezione successiva... +``` + +**Cosa rimuovi:** + +- Numeri di pagina isolati (una riga con solo `42`) +- Intestazioni e piè di pagina ripetitivi +- Righe vuote multiple — massimo una tra sezioni + +**Cosa correggi:** + +- Sillabazioni residue: `estra-\ntto` → `estratto` +- Righe spezzate artificialmente: unisci le righe + che appartengono alla stessa frase +- Parole unite per errore: `ilbene` → `il bene` + +**Cosa sistemi:** + +- Ogni sezione ha il suo titolo al livello corretto +- Nessuna sezione manca di `###` se ne aveva uno nell'originale +- Nessun titolo è duplicato o malformato + +**Il criterio di qualità:** +Leggi ogni sezione ad alta voce. Se suona naturale e fluente è corretta. +Se si interrompe c'è una riga spezzata. Se suona ripetitiva c'è un artefatto. + +**Traccia il lavoro:** + +```bash +# Aggiorna notes/revision_log.md con ogni correzione rilevante +# Commita spesso per poter tornare indietro + +git add processed/documento/clean.md notes/revision_log.md +git commit -m "revisione: sezioni 1-50 corrette" +``` + +--- + +### Step 5 — Chunking adattivo + +**Tipo:** automatico +**Input:** `processed/documento/clean.md` + `structure_profile.json` +**Output:** `processed/documento/chunks.json` +**Script:** `scripts/chunker.py` + +```bash +python scripts/chunker.py processed/documento/clean.md --save +``` + +Divide il Markdown pulito in chunk. Usa il profilo strutturale +per scegliere la strategia giusta. Non sa nulla del contenuto — +si basa solo sulla struttura. + +**Regole invarianti per qualsiasi documento:** + +- Un chunk non attraversa mai il confine tra due sezioni diverse +- Un chunk non spezza mai una frase a metà +- Ogni chunk porta il suo contesto nel prefisso +- L'overlap tra chunk avviene solo su frasi intere, + mai tra sezioni diverse + +**Parametri:** + +| Parametro | Default | Significato | +|---|---|---| +| `MIN_CHARS` | 200 | Sotto questa soglia, accorpa al chunk successivo | +| `MAX_CHARS` | 800 | Sopra questa soglia, spezza su frasi | +| `OVERLAP_S` | 2 | Frasi di overlap tra sotto-chunk dello stesso boundary | + +**Struttura di ogni chunk:** + +```json +{ + "chunk_id": "sezione_principale__sottosezione_3__s0", + "text": "[Sezione principale > Sottosezione 3]\nTesto del chunk...", + "sezione": "Sezione principale", + "titolo": "Sottosezione 3", + "sub_index": 0, + "n_chars": 412 +} +``` + +Il prefisso `[Sezione > Titolo]` è fondamentale: permette all'embedding +di catturare il contesto topico del chunk anche quando il testo +da solo sarebbe ambiguo. + +--- + +### Step 6 — Verifica chunk + +**Tipo:** automatico +**Input:** `processed/documento/chunks.json` +**Output:** report problemi + statistiche +**Script:** `scripts/verify_chunks.py` + +```bash +python scripts/verify_chunks.py processed/documento/chunks.json +``` + +Analizza ogni chunk e segnala i problemi. Non corregge nulla. +Se ci sono problemi torni allo step 4 o aggiusti i parametri +dello step 5. Non si va allo step 8 finché questo step è pulito. + +**Cosa verifica:** + +- Ogni chunk ha il prefisso di contesto +- Nessun chunk è vuoto +- Nessun chunk è sotto `MIN_CHARS` +- Nessun chunk è sopra `MAX_CHARS` * 1.5 +- Ogni chunk finisce con punteggiatura (frase completa) + +**Tabella diagnosi:** + +| Problema | Causa probabile | Soluzione | +|---|---|---| +| Molti chunk troppo corti | `MIN_CHARS` troppo alto | Abbassa `MIN_CHARS` | +| Molti chunk troppo lunghi | `MAX_CHARS` troppo basso | Alza `MAX_CHARS` | +| Chunk senza prefisso | Bug nel parsing | Controlla `###` nel MD | +| Chunk che finiscono a metà frase | Riga spezzata nel MD | Correggi nello step 4 | + +**Output se tutto ok:** + +``` +Totale chunk: 301 +✅ OK: 301 + +Distribuzione lunghezze: + Min: 187 char + Max: 923 char + Media: 401 char + +✅ Nessun problema — procedi con la vettorizzazione. +``` + +--- + +### Step 7 — Installazione ambiente + +**Tipo:** manuale (una volta sola) +**Input:** nessuno +**Output:** ambiente locale funzionante + +```bash +# Installa Ollama +curl -fsSL https://ollama.com/install.sh | sh + +# Scarica i modelli +ollama pull qwen3:8b # LLM — ~5 GB +ollama pull nomic-embed-text # Embedding — ~270 MB + +# Installa dipendenze Python +pip install -r requirements.txt + +# Verifica +ollama list +# deve mostrare entrambi i modelli +``` + +**Modelli usati:** + +| Modello | Ruolo | Dimensione | RAM occupata | +|---|---|---|---| +| `nomic-embed-text` | Converte testo in vettori | 270 MB | ~500 MB | +| `qwen3:8b` | Genera le risposte | 5 GB | ~6-7 GB | + +Questo step si esegue una volta sola. Da questo momento +Ollama è sempre disponibile sul sistema. + +--- + +### Step 8 — Vettorizzazione + +**Tipo:** automatico (lento) +**Input:** `processed/documento/chunks.json` +**Output:** `chroma_db/` popolato +**Script:** `scripts/ingest.py` + +```bash +python scripts/ingest.py processed/documento/chunks.json +``` + +Trasforma ogni chunk in un vettore numerico e lo salva in ChromaDB. +È il processo più lento — su CPU circa 1-3 secondi per chunk. +Per 300 chunk aspetta 5-15 minuti. + +**Cosa succede per ogni chunk:** + +``` +testo del chunk + │ + ▼ Ollama (nomic-embed-text) +vettore di 768 numeri +[0.23, -0.41, 0.87, 0.12, ...] + │ + ▼ ChromaDB +salva: testo + vettore + metadati +``` + +**Perché 768 numeri:** +Ogni numero rappresenta una dimensione semantica. +Testi con significato simile producono vettori simili — +i loro numeri sono vicini nello spazio a 768 dimensioni. +Questo è ciò che permette il retrieval semantico. + +**Output durante l'esecuzione:** + +``` +✅ Ollama OK — nomic-embed-text disponibile +📦 301 chunk da ingestire + + [ 1/301] ✓ sezione_1__sotto_1__s0 ETA: 290s + [ 2/301] ✓ sezione_1__sotto_2__s0 ETA: 287s + ... + [301/301] ✓ sezione_9__sotto_42__s0 ETA: 0s + +✅ Ingestione completata in 312s — 301/301 chunk salvati +``` + +`chroma_db/` contiene ora tutti i vettori su disco. +Non è necessario ripetere questo step a meno che il documento cambi. + +--- + +### Step 9 — Pipeline RAG + +**Tipo:** interattivo +**Input:** `chroma_db/` + domanda dell'utente +**Output:** risposta basata sul documento +**Script:** `scripts/rag.py` + +```bash +python scripts/rag.py +``` + +Mette insieme retrieval e generation in un loop interattivo. + +**Flusso per ogni domanda:** + +``` +La tua domanda in testo + │ + ▼ embedding della domanda (nomic-embed-text) +vettore 768 dimensioni + │ + ▼ ricerca per similarità coseno in ChromaDB +top 3 chunk semanticamente più vicini + │ + ▼ costruzione del prompt +"Rispondi SOLO dal contesto: + [chunk 1] + [chunk 2] + [chunk 3] + Domanda: ..." + │ + ▼ Ollama (Qwen3 8B) +risposta generata +``` + +**Come si usa:** + +``` +Domanda: La tua domanda qui +Domanda: La tua domanda qui -v ← aggiunge i chunk recuperati +Domanda: exit +``` + +**La similarità coseno:** +Misura l'angolo tra due vettori — non la distanza ma l'orientamento. +Due testi semanticamente simili puntano nella stessa direzione +nello spazio a 768 dimensioni, indipendentemente dalla loro lunghezza. + +**Regola del prompt:** +Il LLM risponde SOLO dal contesto fornito. +Se la risposta non è nel documento lo dice esplicitamente. +Non inventa, non integra con conoscenza esterna. + +--- + +### Step 10 — Test automatici + +**Tipo:** automatico +**Input:** sistema completo +**Output:** tutti i test verdi +**Script:** `scripts/test_pipeline.py` + +```bash +python scripts/test_pipeline.py +``` + +Verifica ogni componente in isolamento e poi nel sistema completo. +I test non dipendono dal contenuto del documento — usano dati +fittizi creati e distrutti in memoria. + +**Struttura dei test:** + +``` +Test unitari — ogni componente isolato + ✓ split_sentences non spezza le frasi + ✓ parse_markdown rileva la struttura corretta + ✓ chunk_sezione rispetta i boundary + ✓ il prefisso è sempre presente in ogni chunk + +Test integrazione — i componenti parlano tra loro + ✓ Ollama è raggiungibile + ✓ i modelli sono disponibili + ✓ l'embedding produce 768 dimensioni + ✓ testi diversi producono vettori diversi + ✓ ChromaDB scrive e legge correttamente + +Test qualità — il sistema si comporta bene + ✓ il retrieval trova il chunk pertinente + ✓ il retrieval non trova il chunk non pertinente + ✓ il LLM usa il contesto fornito + ✓ il LLM ammette quando la risposta non è nel contesto +``` + +--- + +## Principi di progettazione + +**Atomico** +Ogni step fa una cosa sola. Il chunker non sa niente di Ollama. +L'ingestione non sa niente del MD originale. +Se un pezzo si rompe, sai esattamente dove. + +**Verificabile** +Ogni step ha un criterio di completamento oggettivo. +Non si passa allo step successivo finché il precedente non è verificato. + +**Reversibile** +Puoi tornare indietro senza perdere il lavoro degli altri step. +Cambi il MD? Riesegui solo step 5 e 8. +Cambi i parametri del chunker? Riesegui solo step 5 e 8. +Non si riparte mai da zero. + +**Senza assunzioni** +Il sistema non assume nulla sulla struttura del documento. +Rileva il livello strutturale e si adatta. +Funziona su libri, manuali, articoli, contratti, dispense. + +**Tutto locale** +Nessuna chiamata a API esterne. +Nessun dato trasmesso fuori dalla macchina. +Nessun costo di utilizzo.