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
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Davide Grilli
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+77 -79
View File
@@ -1,10 +1,15 @@
# RAG from scratch # 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 **Stack:** Python · MinerU · ChromaDB · Ollama
**Chunking:** AST-based, deterministico, senza LLM **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 PDF
@@ -22,8 +27,8 @@ PDF
|---------|--------|------| |---------|--------|------|
| Python | 3.10 3.13 | testato su 3.11 | | Python | 3.10 3.13 | testato su 3.11 |
| RAM | 8 GB | 16 GB+ per modelli LLM grandi | | RAM | 8 GB | 16 GB+ per modelli LLM grandi |
| Disco | 5 GB liberi | + spazio modelli Ollama | | Disco | 5 GB liberi | + spazio modelli Ollama (~4 GB per bge-m3 + qwen3.5:4b) |
| GPU | non necessaria | accelera MinerU e modelli LLM | | GPU | non necessaria | accelera MinerU e i modelli LLM in modo significativo |
| OS | Linux, macOS, Windows (WSL2) | | | OS | Linux, macOS, Windows (WSL2) | |
**Dipendenze Python** — installate via `requirements.txt`: **Dipendenze Python** — installate via `requirements.txt`:
@@ -35,18 +40,15 @@ PDF
**Dipendenze esterne:** **Dipendenze esterne:**
- [Ollama](https://ollama.com) — server locale per embedding e generazione LLM - [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 ## Setup ambiente
```bash ```bash
# Crea e attiva il virtual environment
python -m venv .venv python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate source .venv/bin/activate # Windows: .venv\Scripts\activate
# Installa le dipendenze
pip install -r requirements.txt pip install -r requirements.txt
``` ```
@@ -54,11 +56,11 @@ pip install -r requirements.txt
## Passo 1 — Converti i PDF con MinerU ## 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) ### 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 1. Apri `mineru.ipynb` su Google Colab
2. Carica i PDF nella cartella `input/` indicata nelle celle 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/ sources/
└── <stem>_output/ └── <stem>_output/
└── auto/ └── auto/
├── <stem>.md ← questo è l'input della pipeline ├── <stem>.md ← input della pipeline
└── images/ └── images/
``` ```
### Opzione B — Installazione locale ### Opzione B — Installazione locale
```bash ```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 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 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 ## 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 ```bash
# Singolo documento python chunks/chunker.py --stem <stem> # singolo documento
python chunks/chunker.py --stem <stem> python chunks/chunker.py # tutti i documenti in sources/
python chunks/chunker.py --stem <stem> --force # rigenera anche se esiste gia'
# 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
``` ```
Output in `chunks/<stem>/`: `chunks.json`, `meta.json`, `report.json`. Output in `chunks/<stem>/`: `chunks.json`, `meta.json`, `report.json`.
### Configurazione — `chunks/config.py` ### Configurazione — `chunks/config.py`
Modifica i parametri nella classe `ChunkerConfig` se necessario:
| Parametro | Default | Descrizione | | Parametro | Default | Descrizione |
|-----------|---------|-------------| |-----------|---------|-------------|
| `max_chars` | `1200` | Dimensione massima di un chunk (caratteri) | | `max_chars` | `1200` | Dimensione massima di un chunk (caratteri) |
| `min_chars` | `80` | Soglia minima; chunk piu' piccoli vengono accorpati | | `min_chars` | `80` | Soglia minima; chunk piu' piccoli vengono accorpati al successivo |
| `target_chars` | `800` | Dimensione target per il packing greedy | | `target_chars` | `800` | Dimensione target per il packing dei blocchi |
| `context_depth` | `3` | Livelli di heading inclusi come prefisso nel testo per l'embedding | | `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_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 | | `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) ## 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. Genera gli embedding dei chunk e li indicizza in ChromaDB. Richiede Ollama attivo.
```bash ```bash
# Avvia Ollama (se non e' gia' in esecuzione) ollama serve # avvia Ollama se non e' gia' in esecuzione
ollama serve ollama pull bge-m3 # modello embedding (una volta sola)
# Installa il modello di embedding (una volta sola) python ingestion/ingest.py --stem <stem> # singolo documento
ollama pull bge-m3 python ingestion/ingest.py --collection <nome> --stems doc1 doc2 doc3 # multi-documento
python ingestion/ingest.py --stem <stem> --force # rigenera dopo modifiche
# 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
``` ```
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` ### Configurazione — `ingestion/config.py`
| Parametro | Default | Descrizione | | Parametro | Default | Descrizione |
|-----------|---------|-------------| |-----------|---------|-------------|
| `EMBED_MODEL` | `bge-m3` | Modello Ollama per gli embedding. Se lo cambi, riesegui l'ingest con `--force` | | `EMBED_MODEL` | `bge-m3` | Modello Ollama per gli embedding |
| `EMBED_MAX_CHARS` | `6000` | Caratteri massimi inviati al modello per ogni chunk | | `EMBED_MAX_CHARS` | `6000` | Caratteri massimi inviati al modello per chunk |
| `OLLAMA_URL` | `http://localhost:11434` | URL del server Ollama | | `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 ## Passo 4 — Interrogazione
@@ -154,28 +165,25 @@ python ingestion/ingest.py --stem <stem> --force
### Da terminale ### Da terminale
```bash ```bash
# Installa il modello LLM (una volta sola) ollama pull qwen3.5:4b # modello LLM (una volta sola)
ollama pull qwen3.5:4b
# RAG interattivo — retrieval + risposta generata dal modello python rag/rag.py --collection <nome> # RAG interattivo
python rag/rag.py --collection <nome> python rag/rag.py --stem <stem> # scorciatoia per collection a documento singolo
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> # retrieval puro senza LLM (debug)
python rag/retrieve.py --collection <nome>
python rag/retrieve.py --stem <stem> --top-k 10 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 ### Con la GUI desktop
```bash ```bash
python gui/main.py 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` ### 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 | | `OLLAMA_URL` | `http://localhost:11434` | URL del server Ollama |
| `SYSTEM_PROMPT` | vedi file | Istruzioni di sistema inviate al modello prima del contesto | | `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:
``` | Modello | RAM indicativa | Note |
rag/ |---------|---------------|------|
├── sources/ ← output MinerU (una cartella per documento) | `qwen3:1.7b` | ~1.5 GB | Minimo; buono solo per domande semplici |
├── chunks/ | `phi4-mini` | ~2.5 GB | Compatto ma sorprendentemente capace |
│ ├── chunker.py ← orchestrazione pipeline AST | `qwen3.5:4b` | ~2.5 GB | Default consigliato: buon equilibrio qualita'/velocita' |
│ ├── config.py ← parametri di chunking | `gemma3:4b` | ~3 GB | Alternativa Google, buono in italiano |
│ ├── parser.py ← markdown-it-py + dollarmath | `llama3.2:3b` | ~2 GB | Veloce, buono per inglese |
│ ├── segmenter.py ← token stream -> Block[] | `mistral:7b` | ~4.5 GB | Solido su testi tecnici in italiano e inglese |
│ ├── packer.py ← Block[] -> Chunk[] | `qwen3:8b` | ~5 GB | Qualita' alta, richiede 16+ GB RAM totali |
│ └── validator.py ← invarianti e metriche | `llama3.1:8b` | ~5 GB | Meta, ottimo ragionamento, inglese principalmente |
├── ingestion/ | `deepseek-r1:8b` | ~5 GB | Reasoning avanzato; piu' lento ma preciso su domande complesse |
│ ├── ingest.py ← embedding -> ChromaDB
│ └── config.py ← parametri embedding - **`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.
├── rag/ - **`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.
│ ├── rag.py ← loop RAG interattivo - **`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.
│ ├── retrieve.py ← retrieval puro (debug) - **`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.
│ └── 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
```