# 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.5 · 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 │ ├── step-0/ │ └── check_pdf.py # Verifica requisiti del PDF │ ├── step-1/ │ └── inspect_pdf.py # Ispezione automatica del PDF │ ├── step-2/ │ ├── convert_pdf.py # Conversione PDF → Markdown grezzo │ └── / │ └── raw.md # MD grezzo (non toccare) │ ├── step-3/ │ ├── detect_structure.py # Rilevamento struttura MD │ └── / │ └── structure_profile.json # Profilo struttura │ ├── step-4/ │ ├── revise.py # Pre-processing automatico MD │ ├── revision_log.md # Log modifiche manuali │ └── / │ ├── clean.md # MD revisionato │ └── structure_profile.json # Profilo aggiornato │ ├── step-5/ │ ├── chunker.py # Chunking adattivo │ └── / │ └── chunks.json # Chunk pronti per la vettorizzazione │ ├── step-6/ │ ├── verify_chunks.py # Verifica chunk │ ├── fix_chunks.py # Fix chunk problematici │ └── / │ └── chunks.json # Chunk verificati │ ├── step-7/ │ ├── check_env.py # Verifica ambiente locale │ └── README.md # Guida installazione Ollama e dipendenze │ ├── step-8/ │ └── ingest.py # Vettorizzazione → ChromaDB │ ├── step-9/ │ ├── config.py # Configurazione pipeline RAG ← modifica qui │ ├── rag.py # Pipeline RAG interattiva │ ├── test_ollama.py # Test chat Ollama senza RAG │ └── README.md │ ├── chroma_db/ # Vector store — generato da step-8 ├── 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:** tutti i PDF in `sources/` **Output:** `step-1/_step1_report.txt` **Script:** `step-1/inspect_pdf.py` ```bash python step-1/inspect_pdf.py ``` Lo script scansiona automaticamente tutti i PDF in `sources/`, analizza ogni documento 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:** tutti i PDF in `sources/` (o uno solo con `--pdf`) **Output:** `step-2//raw.md` + `step-2//clean.md` **Script:** `step-2/convert_pdf.py` ```bash python step-2/convert_pdf.py # tutti i PDF in sources/ python step-2/convert_pdf.py --pdf sources/doc.pdf # un solo PDF ``` Converte il PDF in Markdown usando `pymupdf4llm`. Il risultato non è perfetto — è la base su cui lavorerai nello step 4. Lo script crea due file: - `raw.md` — conversione grezza, **non modificare mai**. È il punto di partenza di riferimento. - `clean.md` — copia di lavoro che verrà modificata negli step successivi. **Cosa produce la conversione:** - Titoli riconosciuti e convertiti in `#` `##` `###` - Paragrafi separati da righe vuote - Sillabazione parzialmente risolta **Cosa non produce:** - Rimozione intestazioni e piè di pagina - Correzione completa del layout a colonne - Descrizione del contenuto delle immagini --- ### Step 3 — Rilevamento struttura **Tipo:** automatico **Input:** `step-2//` **Output:** `step-3//structure_profile.json` **Script:** `step-3/detect_structure.py` ```bash python step-3/detect_structure.py # tutti i documenti in step-2/ python step-3/detect_structure.py --stem # un solo documento python step-3/detect_structure.py --force # riesegui anche se già presente ``` Copia `raw.md` e `clean.md` da `step-2//` e analizza la struttura del Markdown senza modificarla. 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 (con pre-processing automatico) **Input:** `step-3//clean.md` + `step-3//structure_profile.json` **Output:** `step-4//clean.md` — MD revisionato **Script:** `step-4/revise.py` > 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. #### Pre-processing automatico Prima di qualsiasi revisione manuale, esegui lo script di revisione automatica: ```bash python step-4/revise.py --stem documento ``` Lo script applica le seguenti trasformazioni euristiche, valide per qualsiasi documento: | Trasformazione | Descrizione | |---|---| | Rimozione TOC | Righe che iniziano con `INDICE`, `INDEX`, `CONTENTS`, ecc. | | ALL-CAPS → `##` | Righe standalone in maiuscolo convertite in header section-case | | `N. testo` → `### N.` | Sezioni numerate (con 1+ spazio dopo il punto) convertite in h3 | | Unione paragrafi | Blocchi spezzati da salti pagina PDF uniti automaticamente | | Whitespace | Spazi multipli normalizzati, righe vuote ridotte | Il profilo strutturale aggiornato viene salvato in `step-4//structure_profile.json`. #### Revisione assistita da Claude Code Dopo il pre-processing, usa la skill integrata per una revisione qualitativa: ``` /step4-review documento ``` La skill analizza `step-4//clean.md` e produce un report strutturato: ``` 🔴 BLOCCANTI — problemi che compromettono il chunking 🟡 MINORI — artefatti visibili ma non bloccanti 🟢 OK — categorie senza problemi ``` Poi propone le correzioni e le applica solo su tua approvazione. #### Revisione manuale residua Apri `step-4//clean.md` nel tuo editor e verifica quanto segnalato dalla skill. Il criterio di qualità rimane lo stesso: **Struttura target:** ```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. ``` **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 # step-4/revision_log.md viene aggiornato automaticamente da revise.py # Commita dopo la revisione manuale git add step-4/revision_log.md git commit -m "step-4: revisione nietzsche completata" ``` --- ### Step 5 — Chunking adattivo **Tipo:** automatico **Input:** `step-4//clean.md` + `step-4//structure_profile.json` **Output:** `step-5//chunks.json` **Script:** `step-5/chunker.py` ```bash python step-5/chunker.py --stem documento ``` 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:** `step-5//chunks.json` **Output:** report problemi + statistiche **Script:** `step-6/verify_chunks.py` ```bash python step-6/verify_chunks.py --stem documento ``` 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 **Script:** `step-7/check_env.py` Installa Ollama, scarica i modelli e verifica l'ambiente. Si esegue una volta sola. Vedi [`step-7/README.md`](step-7/README.md) per istruzioni dettagliate e scelta dei modelli. ```bash python step-7/check_env.py ``` --- ### Step 8 — Vettorizzazione **Tipo:** automatico (lento) **Input:** `step-6//chunks.json` **Output:** `chroma_db/` popolato **Script:** `step-8/ingest.py` ```bash source .venv/bin/activate python step-8/ingest.py --stem ``` Trasforma ogni chunk in un vettore numerico e lo salva in ChromaDB. È il processo più lento — su CPU circa 1 secondo per chunk. Per 900 chunk aspetta circa 15 minuti. **Argomenti:** | Argomento | Descrizione | |---|---| | `--stem ` | Processa un singolo documento. Senza questo argomento processa tutti gli stem trovati in `step-6/` | | `--force` | Cancella e ricrea la collection se esiste già. Senza `--force`, se la collection è presente lo step viene saltato | **Quando usare `--force`:** Se hai modificato i chunk (es. hai rieseguito step-6 dopo correzioni), la collection in ChromaDB contiene ancora i vecchi vettori. `--force` la cancella e la ricrea da zero con i chunk aggiornati. **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 (sezione, titolo, sub_index) ``` **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 📦 872 chunk da ingestire [ 1/872] ✓ sezione_1__sotto_1__s0 ETA: 870s [ 2/872] ✓ sezione_1__sotto_2__s0 ETA: 867s ... [872/872] ✓ sezione_9__sotto_42__s0 ETA: 0s ✅ Ingestione completata in 718s — 872/872 chunk salvati Collection 'nietzsche' in chroma_db/ ``` `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:** `step-9/rag.py` ```bash source .venv/bin/activate python step-9/rag.py --stem ``` Loop interattivo che risponde a domande sul documento. Configura i parametri in `step-9/config.py` prima di avviare. Vedi [`step-9/README.md`](step-9/README.md) per la configurazione completa. --- ### Step 10 — Test automatici **Tipo:** automatico **Input:** sistema completo **Output:** tutti i test verdi **Script:** `step-10/test_pipeline.py` *(da implementare)* ```bash python step-10/test_pipeline.py --stem ``` 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.