Files
rag-from-scratch/README.md
T
2026-04-12 23:53:13 +02:00

659 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
| 4070 | 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.