docs: riscrive README con abstract esteso, guida parametri e licenza MIT

Espande l'abstract con motivazione personale e contesto d'uso (manuali
tecnici di ingegneria). Aggiunge guida pratica alla scelta dei parametri
per chunking, embedding e RAG con tabelle di modelli Ollama e link al
catalogo. Aggiunge file LICENSE MIT e relativo link nel README.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 10:18:46 +02:00
parent b4e64ccedb
commit 9261e480da
2 changed files with 99 additions and 80 deletions
+78 -80
View File
@@ -1,10 +1,15 @@
# RAG from scratch
Esercizio pratico per costruire una pipeline RAG (Retrieval-Augmented Generation) completa a partire da documenti PDF, interamente in locale senza dipendenze cloud. L'obiettivo è avere un assistente che risponde a domande sui tuoi documenti usando solo il contenuto di quei documenti, senza allucinazioni.
Sono appassionato di intelligenza artificiale e questo progetto nasce dalla curiosita' di capire come funziona davvero un sistema RAG costruendolo da zero, senza usare framework che nascondono i dettagli. Nessuna API esterna, nessun dato che lascia la macchina: tutto gira in locale.
Il risultato e' una pipeline completa che trasforma PDF in un assistente interrogabile in linguaggio naturale. L'ho sviluppato e testato principalmente su manuali tecnici di ingegneria — documenti densi, con formule, tabelle e gerarchie di sezioni profonde — ma funziona su qualsiasi PDF ben strutturato: paper accademici, documentazione tecnica, archivi personali.
L'idea e' semplice: MinerU estrae il testo dal PDF producendo un Markdown strutturato, il chunker lo divide in blocchi semantici coerenti, gli embedding li trasformano in vettori numerici, ChromaDB li indicizza, e Ollama genera le risposte partendo dai chunk piu' rilevanti per la domanda. Ogni componente e' sostituibile e configurabile.
**Stack:** Python · MinerU · ChromaDB · Ollama
**Chunking:** AST-based, deterministico, senza LLM
**Embedding + Generazione:** Ollama (locale, nessun dato inviato a server esterni)
**Embedding + Generazione:** Ollama (locale, nessun dato inviato a server esterni)
**Licenza:** [MIT](LICENSE)
```
PDF
@@ -22,8 +27,8 @@ PDF
|---------|--------|------|
| Python | 3.10 3.13 | testato su 3.11 |
| RAM | 8 GB | 16 GB+ per modelli LLM grandi |
| Disco | 5 GB liberi | + spazio modelli Ollama |
| GPU | non necessaria | accelera MinerU e modelli LLM |
| Disco | 5 GB liberi | + spazio modelli Ollama (~4 GB per bge-m3 + qwen3.5:4b) |
| GPU | non necessaria | accelera MinerU e i modelli LLM in modo significativo |
| OS | Linux, macOS, Windows (WSL2) | |
**Dipendenze Python** — installate via `requirements.txt`:
@@ -35,18 +40,15 @@ PDF
**Dipendenze esterne:**
- [Ollama](https://ollama.com) — server locale per embedding e generazione LLM
- [MinerU](https://github.com/opendatalab/MinerU) — conversione PDF → Markdown (opzionale se hai già i file `.md`)
- [MinerU](https://github.com/opendatalab/MinerU) — conversione PDF → Markdown (opzionale se hai gia' i file `.md`)
---
## Setup ambiente
```bash
# Crea e attiva il virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Installa le dipendenze
pip install -r requirements.txt
```
@@ -54,11 +56,11 @@ pip install -r requirements.txt
## Passo 1 — Converti i PDF con MinerU
MinerU estrae il testo dai PDF producendo un Markdown strutturato con tabelle, formule ed immagini. Ci sono due modalità:
MinerU estrae il testo dai PDF producendo Markdown strutturato con tabelle, formule e immagini. Gestisce bene anche PDF scansionati tramite OCR.
### Opzione A — Google Colab (consigliata, usa la GPU gratuita di Google)
Il notebook `mineru.ipynb` esegue MinerU su Colab sfruttando la GPU messa a disposizione gratuitamente. Utile se non hai una GPU locale o se i PDF sono pesanti.
Il notebook `mineru.ipynb` esegue MinerU su Colab sfruttando la GPU gratuita. E' la scelta piu' comoda se non hai una GPU locale o se i PDF sono pesanti.
1. Apri `mineru.ipynb` su Google Colab
2. Carica i PDF nella cartella `input/` indicata nelle celle
@@ -69,17 +71,17 @@ Il notebook `mineru.ipynb` esegue MinerU su Colab sfruttando la GPU messa a disp
sources/
└── <stem>_output/
└── auto/
├── <stem>.md ← questo è l'input della pipeline
├── <stem>.md ← input della pipeline
└── images/
```
### Opzione B — Installazione locale
```bash
pip install "mineru[all]" # installa MinerU (scarica ~15 GB di modelli al primo avvio)
pip install "mineru[all]" # scarica ~15 GB di modelli al primo avvio
mineru -p documento.pdf -o sources/<stem>_output/ -b pipeline
# Con GPU:
# Con GPU CUDA:
mineru -p documento.pdf -o sources/<stem>_output/ -b hybrid-auto-engine
```
@@ -87,35 +89,35 @@ mineru -p documento.pdf -o sources/<stem>_output/ -b hybrid-auto-engine
## Passo 2 — Chunking
Il chunker analizza il Markdown con un parser AST, divide il testo in blocchi semantici e li impacchetta in chunk ottimizzati per il retrieval.
Il chunker analizza il Markdown con un parser AST, identifica blocchi semantici (paragrafi, tabelle, formule, codice) e li impacchetta in chunk ottimizzati per il retrieval.
```bash
# Singolo documento
python chunks/chunker.py --stem <stem>
# Tutti i documenti presenti in sources/ in una volta
python chunks/chunker.py
# Rigenera anche se chunks.json esiste già
python chunks/chunker.py --stem <stem> --force
python chunks/chunker.py --stem <stem> # singolo documento
python chunks/chunker.py # tutti i documenti in sources/
python chunks/chunker.py --stem <stem> --force # rigenera anche se esiste gia'
```
Output in `chunks/<stem>/`: `chunks.json`, `meta.json`, `report.json`.
### Configurazione — `chunks/config.py`
Modifica i parametri nella classe `ChunkerConfig` se necessario:
| Parametro | Default | Descrizione |
|-----------|---------|-------------|
| `max_chars` | `1200` | Dimensione massima di un chunk (caratteri) |
| `min_chars` | `80` | Soglia minima; chunk piu' piccoli vengono accorpati |
| `target_chars` | `800` | Dimensione target per il packing greedy |
| `min_chars` | `80` | Soglia minima; chunk piu' piccoli vengono accorpati al successivo |
| `target_chars` | `800` | Dimensione target per il packing dei blocchi |
| `context_depth` | `3` | Livelli di heading inclusi come prefisso nel testo per l'embedding |
| `skip_headings` | vedi file | Sezioni saltate completamente (indice, sommario, ecc.) |
| `skip_pre_heading` | `True` | Salta il contenuto prima del primo heading (frontespizi, ecc.) |
| `skip_pre_heading` | `True` | Salta il contenuto prima del primo heading (copertine, ecc.) |
| `atomic_types` | `table, code, list, math, html` | Tipi di blocco che non vengono mai spezzati |
**Come scegliere i parametri di chunking:**
- **`max_chars` e `target_chars`**: dipendono dalla lunghezza media dei paragrafi del tuo documento e dalla finestra di contesto del modello di embedding. Con `bge-m3` (8192 token) i valori di default vanno bene per la maggior parte dei manuali tecnici. Se i tuoi paragrafi sono molto corti (documentazione stile reference), abbassa `target_chars` a 400500 per evitare chunk troppo eterogenei. Se sono molto lunghi (testi narrativi), puoi salire fino a 15002000.
- **`context_depth`**: determina quanti livelli di heading vengono preposti al testo nell'embedding (es. "Capitolo 3 > Sezione 3.2 > contenuto"). Valore 23 e' ottimale per documenti con gerarchia profonda come manuali tecnici. Con documenti piatti (un solo livello di heading) metti 1.
- **`skip_headings`**: aggiungi i titoli delle sezioni che non vuoi indicizzare (indici, bibliografie, ringraziamenti). Il confronto e' case-insensitive e per prefisso, quindi `"appendice"` cattura anche "Appendice A", "Appendice B", ecc.
- **`min_chars`**: lascia il default a meno che il documento non abbia molti paragrafi di una riga (caption, label) che vuoi escludere — in quel caso alzalo a 150200.
---
## Passo 3 — Vettorizzazione (ingest)
@@ -123,30 +125,39 @@ Modifica i parametri nella classe `ChunkerConfig` se necessario:
Genera gli embedding dei chunk e li indicizza in ChromaDB. Richiede Ollama attivo.
```bash
# Avvia Ollama (se non e' gia' in esecuzione)
ollama serve
ollama serve # avvia Ollama se non e' gia' in esecuzione
ollama pull bge-m3 # modello embedding (una volta sola)
# Installa il modello di embedding (una volta sola)
ollama pull bge-m3
# Indicizza un singolo documento
python ingestion/ingest.py --stem <stem>
# Indicizza piu' documenti in una collection condivisa (RAG cross-documento)
python ingestion/ingest.py --collection <nome> --stems doc1 doc2 doc3
# Rigenera dopo aver aggiornato i chunk o cambiato modello embedding
python ingestion/ingest.py --stem <stem> --force
python ingestion/ingest.py --stem <stem> # singolo documento
python ingestion/ingest.py --collection <nome> --stems doc1 doc2 doc3 # multi-documento
python ingestion/ingest.py --stem <stem> --force # rigenera dopo modifiche
```
Con `--collection` tutti i documenti vengono indicizzati insieme e sono interrogabili con un'unica query — utile quando i temi si sovrappongono o vuoi fare confronti tra documenti.
### Configurazione — `ingestion/config.py`
| Parametro | Default | Descrizione |
|-----------|---------|-------------|
| `EMBED_MODEL` | `bge-m3` | Modello Ollama per gli embedding. Se lo cambi, riesegui l'ingest con `--force` |
| `EMBED_MAX_CHARS` | `6000` | Caratteri massimi inviati al modello per ogni chunk |
| `EMBED_MODEL` | `bge-m3` | Modello Ollama per gli embedding |
| `EMBED_MAX_CHARS` | `6000` | Caratteri massimi inviati al modello per chunk |
| `OLLAMA_URL` | `http://localhost:11434` | URL del server Ollama |
**Come scegliere il modello di embedding:**
Il catalogo completo e' disponibile su [ollama.com/search?c=embedding](https://ollama.com/search?c=embedding). Alcuni punti di riferimento:
| Modello | Dim. | Note |
|---------|------|------|
| `bge-m3` | ~1 GB | Consigliato: multilingua, robusto su testi tecnici, finestra 8192 token |
| `nomic-embed-text` | ~274 MB | Leggero, buono per prove rapide; solo inglese |
| `mxbai-embed-large` | ~670 MB | Qualita' alta su benchmark inglesi |
| `bge-large` | ~670 MB | Variante grande di bge, multilingua |
| `snowflake-arctic-embed` | ~670 MB | Ottimizzato per retrieval su testi lunghi |
| `all-minilm` | ~46 MB | Molto leggero, qualita' ridotta |
**Attenzione:** se cambi `EMBED_MODEL` devi rieseguire l'ingest con `--force` su tutti i documenti — i vettori generati da modelli diversi non sono compatibili.
---
## Passo 4 — Interrogazione
@@ -154,28 +165,25 @@ python ingestion/ingest.py --stem <stem> --force
### Da terminale
```bash
# Installa il modello LLM (una volta sola)
ollama pull qwen3.5:4b
ollama pull qwen3.5:4b # modello LLM (una volta sola)
# RAG interattivo — retrieval + risposta generata dal modello
python rag/rag.py --collection <nome>
python rag/rag.py --stem <stem> # scorciatoia per collection a documento singolo
python rag/rag.py --collection <nome> # RAG interattivo
python rag/rag.py --stem <stem> # scorciatoia per collection a documento singolo
# Retrieval puro senza LLM (utile per debug)
python rag/retrieve.py --collection <nome>
python rag/retrieve.py --collection <nome> # retrieval puro senza LLM (debug)
python rag/retrieve.py --stem <stem> --top-k 10
```
Nel loop interattivo: digita la domanda e premi Invio. Aggiungi `-v` alla fine per vedere i chunk recuperati con i relativi score di similarita'.
Nel loop: digita la domanda e premi Invio. Aggiungi `-v` alla fine per vedere i chunk recuperati con i relativi score di similarita'.
### Con la GUI desktop
```bash
python gui/main.py
python gui/main.py --collection <nome> # apre direttamente la collection
python gui/main.py --collection <nome>
```
La GUI mostra le risposte con Markdown e formule matematiche renderizzate. Il tema chiaro/scuro e' selezionabile dal pulsante in alto a destra.
La GUI mostra le risposte con Markdown e formule matematiche renderizzate (KaTeX). Il tema chiaro/scuro e' selezionabile dal pulsante in alto a destra.
### Configurazione — `rag/config.py`
@@ -188,33 +196,23 @@ La GUI mostra le risposte con Markdown e formule matematiche renderizzate. Il te
| `OLLAMA_URL` | `http://localhost:11434` | URL del server Ollama |
| `SYSTEM_PROMPT` | vedi file | Istruzioni di sistema inviate al modello prima del contesto |
---
**Come scegliere i parametri RAG:**
## Struttura del repository
- **`OLLAMA_MODEL`**: il catalogo completo e' su [ollama.com/library](https://ollama.com/library). Scegli in base alla RAM disponibile:
```
rag/
├── sources/ ← output MinerU (una cartella per documento)
├── chunks/
│ ├── chunker.py ← orchestrazione pipeline AST
│ ├── config.py ← parametri di chunking
│ ├── parser.py ← markdown-it-py + dollarmath
│ ├── segmenter.py ← token stream -> Block[]
│ ├── packer.py ← Block[] -> Chunk[]
│ └── validator.py ← invarianti e metriche
├── ingestion/
│ ├── ingest.py ← embedding -> ChromaDB
│ └── config.py ← parametri embedding
├── rag/
│ ├── rag.py ← loop RAG interattivo
│ ├── retrieve.py ← retrieval puro (debug)
│ └── config.py ← parametri RAG e system prompt
├── gui/
│ ├── main.py ← entry point GUI
│ ├── chat_window.py ← finestra principale PySide6
│ ├── worker.py ← thread per il pipeline RAG
│ └── chat.html ← template HTML (Markdown + KaTeX)
├── chroma_db/ ← database vettoriale (non tracciato da git)
├── mineru.ipynb ← notebook Colab per conversione PDF
└── requirements.txt
```
| Modello | RAM indicativa | Note |
|---------|---------------|------|
| `qwen3:1.7b` | ~1.5 GB | Minimo; buono solo per domande semplici |
| `phi4-mini` | ~2.5 GB | Compatto ma sorprendentemente capace |
| `qwen3.5:4b` | ~2.5 GB | Default consigliato: buon equilibrio qualita'/velocita' |
| `gemma3:4b` | ~3 GB | Alternativa Google, buono in italiano |
| `llama3.2:3b` | ~2 GB | Veloce, buono per inglese |
| `mistral:7b` | ~4.5 GB | Solido su testi tecnici in italiano e inglese |
| `qwen3:8b` | ~5 GB | Qualita' alta, richiede 16+ GB RAM totali |
| `llama3.1:8b` | ~5 GB | Meta, ottimo ragionamento, inglese principalmente |
| `deepseek-r1:8b` | ~5 GB | Reasoning avanzato; piu' lento ma preciso su domande complesse |
- **`TOP_K`**: quanti chunk vengono recuperati e inclusi nel contesto. Valori bassi (34) danno risposte piu' precise ma rischiano di perdere informazioni distribuite su piu' sezioni. Valori alti (810) danno piu' contesto ma aumentano il tempo di generazione e possono introdurre rumore. 6 e' un buon default per manuali tecnici.
- **`TEMPERATURE`**: per domande fattuali su documenti tecnici tienila bassa (0.10.2). Valori piu' alti (0.50.7) sono utili solo se vuoi risposte piu' discorsive o comparative.
- **`NO_THINK`**: `True` rende le risposte piu' rapide disabilitando il ragionamento interno dei modelli Qwen3. Mettilo a `False` per domande complesse che richiedono ragionamento multi-step.
- **`SYSTEM_PROMPT`**: personalizzalo in base al tipo di documento. Il default e' calibrato per documenti tecnici in italiano. Se usi documenti in inglese o vuoi risposte piu' strutturate, modificalo direttamente nel file.