Files
rag-from-scratch/README.md
T

663 lines
20 KiB
Markdown
Raw Normal View History

2026-04-12 23:53:13 +02:00
# 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
2026-04-12 23:53:13 +02:00
**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
│ └── <stem>/
│ └── raw.md # MD grezzo (non toccare)
├── step-3/
│ ├── detect_structure.py # Rilevamento struttura MD
│ └── <stem>/
│ └── structure_profile.json # Profilo struttura
├── step-4/
│ ├── revise.py # Pre-processing automatico MD
│ ├── revision_log.md # Log modifiche manuali
│ └── <stem>/
│ ├── clean.md # MD revisionato
│ └── structure_profile.json # Profilo aggiornato
├── step-5/
│ ├── chunker.py # Chunking adattivo
│ └── <stem>/
2026-04-12 23:53:13 +02:00
│ └── chunks.json # Chunk pronti per la vettorizzazione
├── step-6/
│ ├── verify_chunks.py # Verifica chunk
│ ├── fix_chunks.py # Fix chunk problematici
│ └── <stem>/
│ └── chunks.json # Chunk verificati
2026-04-12 23:53:13 +02:00
├── 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
2026-04-12 23:53:13 +02:00
├── 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/<stem>_step1_report.txt`
**Script:** `step-1/inspect_pdf.py`
2026-04-12 23:53:13 +02:00
```bash
python step-1/inspect_pdf.py
2026-04-12 23:53:13 +02:00
```
Lo script scansiona automaticamente tutti i PDF in `sources/`, analizza ogni documento pagina per pagina e produce un report.
2026-04-12 23:53:13 +02:00
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:** tutti i PDF in `sources/` (o uno solo con `--pdf`)
**Output:** `step-2/<stem>/raw.md` + `step-2/<stem>/clean.md`
**Script:** `step-2/convert_pdf.py`
2026-04-12 23:53:13 +02:00
```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
2026-04-12 23:53:13 +02:00
```
Converte il PDF in Markdown usando `pymupdf4llm`. Il risultato non è perfetto — è la base
2026-04-12 23:53:13 +02:00
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.
2026-04-12 23:53:13 +02:00
**Cosa produce la conversione:**
2026-04-12 23:53:13 +02:00
- Titoli riconosciuti e convertiti in `#` `##` `###`
- Paragrafi separati da righe vuote
- Sillabazione parzialmente risolta
**Cosa non produce:**
2026-04-12 23:53:13 +02:00
- 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/<stem>/`
**Output:** `step-3/<stem>/structure_profile.json`
**Script:** `step-3/detect_structure.py`
2026-04-12 23:53:13 +02:00
```bash
python step-3/detect_structure.py # tutti i documenti in step-2/
python step-3/detect_structure.py --stem <nome> # un solo documento
python step-3/detect_structure.py --force # riesegui anche se già presente
2026-04-12 23:53:13 +02:00
```
Copia `raw.md` e `clean.md` da `step-2/<stem>/` e analizza la struttura del Markdown senza modificarla.
2026-04-12 23:53:13 +02:00
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/<stem>/clean.md` + `step-3/<stem>/structure_profile.json`
**Output:** `step-4/<stem>/clean.md` — MD revisionato
**Script:** `step-4/revise.py`
2026-04-12 23:53:13 +02:00
> 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
2026-04-12 23:53:13 +02:00
Prima di qualsiasi revisione manuale, esegui lo script di revisione automatica:
2026-04-12 23:53:13 +02:00
```bash
python step-4/revise.py --stem documento
```
2026-04-12 23:53:13 +02:00
Lo script applica le seguenti trasformazioni euristiche, valide per qualsiasi documento:
2026-04-12 23:53:13 +02:00
| 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 |
2026-04-12 23:53:13 +02:00
Il profilo strutturale aggiornato viene salvato in `step-4/<stem>/structure_profile.json`.
#### Revisione assistita da Claude Code
2026-04-12 23:53:13 +02:00
Dopo il pre-processing, usa la skill integrata per una revisione qualitativa:
2026-04-12 23:53:13 +02:00
```
/step4-review documento
2026-04-12 23:53:13 +02:00
```
La skill analizza `step-4/<stem>/clean.md` e produce un report strutturato:
2026-04-12 23:53:13 +02:00
```
🔴 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.
2026-04-12 23:53:13 +02:00
#### Revisione manuale residua
2026-04-12 23:53:13 +02:00
Apri `step-4/<stem>/clean.md` nel tuo editor e verifica quanto segnalato
dalla skill. Il criterio di qualità rimane lo stesso:
2026-04-12 23:53:13 +02:00
**Struttura target:**
```markdown
# Titolo del documento
## Sezione principale
2026-04-12 23:53:13 +02:00
### Sottosezione o unità atomica
Testo fluente, frasi complete, nessun artefatto.
Ogni paragrafo è semanticamente autonomo.
Una riga vuota separa le sezioni.
```
2026-04-12 23:53:13 +02:00
**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
2026-04-12 23:53:13 +02:00
git add step-4/revision_log.md
git commit -m "step-4: revisione nietzsche completata"
2026-04-12 23:53:13 +02:00
```
---
### Step 5 — Chunking adattivo
**Tipo:** automatico
2026-04-13 13:36:53 +02:00
**Input:** `step-4/<stem>/clean.md` + `step-4/<stem>/structure_profile.json`
**Output:** `step-5/<stem>/chunks.json`
**Script:** `step-5/chunker.py`
2026-04-12 23:53:13 +02:00
```bash
2026-04-13 13:36:53 +02:00
python step-5/chunker.py --stem documento
2026-04-12 23:53:13 +02:00
```
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
2026-04-13 13:36:53 +02:00
**Input:** `step-5/<stem>/chunks.json`
2026-04-12 23:53:13 +02:00
**Output:** report problemi + statistiche
**Script:** `step-6/verify_chunks.py`
2026-04-12 23:53:13 +02:00
```bash
python step-6/verify_chunks.py --stem documento
2026-04-12 23:53:13 +02:00
```
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`
2026-04-12 23:53:13 +02:00
Installa Ollama, scarica i modelli e verifica l'ambiente. Si esegue una volta sola.
2026-04-12 23:53:13 +02:00
Vedi [`step-7/README.md`](step-7/README.md) per istruzioni dettagliate e scelta dei modelli.
2026-04-12 23:53:13 +02:00
```bash
python step-7/check_env.py
2026-04-12 23:53:13 +02:00
```
---
### Step 8 — Vettorizzazione
**Tipo:** automatico (lento)
2026-04-14 10:59:40 +02:00
**Input:** `step-6/<stem>/chunks.json`
2026-04-12 23:53:13 +02:00
**Output:** `chroma_db/` popolato
2026-04-14 10:59:40 +02:00
**Script:** `step-8/ingest.py`
2026-04-12 23:53:13 +02:00
```bash
2026-04-14 10:59:40 +02:00
source .venv/bin/activate
python step-8/ingest.py --stem <nome>
2026-04-12 23:53:13 +02:00
```
Trasforma ogni chunk in un vettore numerico e lo salva in ChromaDB.
2026-04-14 10:59:40 +02:00
È il processo più lento — su CPU circa 1 secondo per chunk.
Per 900 chunk aspetta circa 15 minuti.
**Argomenti:**
| Argomento | Descrizione |
|---|---|
| `--stem <nome>` | 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.
2026-04-12 23:53:13 +02:00
**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
2026-04-14 10:59:40 +02:00
salva: testo + vettore + metadati (sezione, titolo, sub_index)
2026-04-12 23:53:13 +02:00
```
**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
2026-04-14 10:59:40 +02:00
📦 872 chunk da ingestire
[ 1/872] ✓ sezione_1__sotto_1__s0 ETA: 870s
[ 2/872] ✓ sezione_1__sotto_2__s0 ETA: 867s
2026-04-12 23:53:13 +02:00
...
2026-04-14 10:59:40 +02:00
[872/872] ✓ sezione_9__sotto_42__s0 ETA: 0s
2026-04-12 23:53:13 +02:00
2026-04-14 10:59:40 +02:00
✅ Ingestione completata in 718s — 872/872 chunk salvati
Collection 'nietzsche' in chroma_db/
2026-04-12 23:53:13 +02:00
```
`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`
2026-04-12 23:53:13 +02:00
```bash
source .venv/bin/activate
python step-9/rag.py --stem <nome>
2026-04-12 23:53:13 +02:00
```
Loop interattivo che risponde a domande sul documento. Configura i parametri in `step-9/config.py` prima di avviare.
2026-04-12 23:53:13 +02:00
Vedi [`step-9/README.md`](step-9/README.md) per la configurazione completa.
2026-04-12 23:53:13 +02:00
---
### Step 10 — Test automatici
**Tipo:** automatico
**Input:** sistema completo
**Output:** tutti i test verdi
**Script:** `step-10/test_pipeline.py` *(da implementare)*
2026-04-12 23:53:13 +02:00
```bash
python step-10/test_pipeline.py --stem <nome>
2026-04-12 23:53:13 +02:00
```
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.