9261e480da
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>
219 lines
11 KiB
Markdown
219 lines
11 KiB
Markdown
# RAG from scratch
|
||
|
||
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)
|
||
**Licenza:** [MIT](LICENSE)
|
||
|
||
```
|
||
PDF
|
||
└─ MinerU ──► sources/<stem>.md
|
||
└─ chunker.py ──► chunks/<stem>/chunks.json
|
||
└─ ingest.py ──► chroma_db/
|
||
└─ rag.py / GUI
|
||
```
|
||
|
||
---
|
||
|
||
## Requisiti minimi
|
||
|
||
| Risorsa | Minimo | Note |
|
||
|---------|--------|------|
|
||
| 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 (~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`:
|
||
|
||
- `chromadb` — database vettoriale locale
|
||
- `markdown-it-py` + `mdit-py-plugins` — parser Markdown AST-based per il chunking
|
||
- `PySide6` — interfaccia grafica desktop (opzionale, solo per la GUI)
|
||
|
||
**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 gia' i file `.md`)
|
||
|
||
---
|
||
|
||
## Setup ambiente
|
||
|
||
```bash
|
||
python -m venv .venv
|
||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
---
|
||
|
||
## Passo 1 — Converti i PDF con MinerU
|
||
|
||
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 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
|
||
3. Esegui tutte le celle — al termine viene scaricato uno ZIP per ogni documento
|
||
4. Decomprimi gli ZIP nella cartella `sources/` del progetto:
|
||
|
||
```
|
||
sources/
|
||
└── <stem>_output/
|
||
└── auto/
|
||
├── <stem>.md ← input della pipeline
|
||
└── images/
|
||
```
|
||
|
||
### Opzione B — Installazione locale
|
||
|
||
```bash
|
||
pip install "mineru[all]" # scarica ~15 GB di modelli al primo avvio
|
||
|
||
mineru -p documento.pdf -o sources/<stem>_output/ -b pipeline
|
||
# Con GPU CUDA:
|
||
mineru -p documento.pdf -o sources/<stem>_output/ -b hybrid-auto-engine
|
||
```
|
||
|
||
---
|
||
|
||
## Passo 2 — Chunking
|
||
|
||
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
|
||
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`
|
||
|
||
| Parametro | Default | Descrizione |
|
||
|-----------|---------|-------------|
|
||
| `max_chars` | `1200` | Dimensione massima di un chunk (caratteri) |
|
||
| `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 (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 400–500 per evitare chunk troppo eterogenei. Se sono molto lunghi (testi narrativi), puoi salire fino a 1500–2000.
|
||
- **`context_depth`**: determina quanti livelli di heading vengono preposti al testo nell'embedding (es. "Capitolo 3 > Sezione 3.2 > contenuto"). Valore 2–3 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 150–200.
|
||
|
||
---
|
||
|
||
## Passo 3 — Vettorizzazione (ingest)
|
||
|
||
Genera gli embedding dei chunk e li indicizza in ChromaDB. Richiede Ollama attivo.
|
||
|
||
```bash
|
||
ollama serve # avvia Ollama se non e' gia' in esecuzione
|
||
ollama pull bge-m3 # modello embedding (una volta sola)
|
||
|
||
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 |
|
||
| `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
|
||
|
||
### Da terminale
|
||
|
||
```bash
|
||
ollama pull qwen3.5:4b # modello LLM (una volta sola)
|
||
|
||
python rag/rag.py --collection <nome> # RAG interattivo
|
||
python rag/rag.py --stem <stem> # scorciatoia per collection a documento singolo
|
||
|
||
python rag/retrieve.py --collection <nome> # retrieval puro senza LLM (debug)
|
||
python rag/retrieve.py --stem <stem> --top-k 10
|
||
```
|
||
|
||
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>
|
||
```
|
||
|
||
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`
|
||
|
||
| Parametro | Default | Descrizione |
|
||
|-----------|---------|-------------|
|
||
| `OLLAMA_MODEL` | `qwen3.5:4b` | Modello LLM per la generazione |
|
||
| `TOP_K` | `6` | Numero di chunk recuperati per ogni domanda |
|
||
| `TEMPERATURE` | `0.2` | 0 = deterministico, valori alti = piu' creativo |
|
||
| `NO_THINK` | `True` | Disabilita il reasoning interno (piu' veloce; solo modelli Qwen3) |
|
||
| `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:**
|
||
|
||
- **`OLLAMA_MODEL`**: il catalogo completo e' su [ollama.com/library](https://ollama.com/library). Scegli in base alla RAM disponibile:
|
||
|
||
| 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 (3–4) danno risposte piu' precise ma rischiano di perdere informazioni distribuite su piu' sezioni. Valori alti (8–10) 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.1–0.2). Valori piu' alti (0.5–0.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.
|