Espande l'abstract, rimuove il diagramma ASCII in favore di testo discorsivo, aggiunge tabelle di modelli embedding e LLM con link al catalogo Ollama, guida alla scelta dei parametri per chunking/ingest/RAG. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
Il PDF viene convertito da MinerU in un file Markdown strutturato (sources/<stem>.md). Il chunker lo analizza e produce una lista di chunk semantici (chunks/<stem>/chunks.json). Il modulo di ingest genera gli embedding tramite Ollama e li indicizza in ChromaDB (chroma_db/). Infine rag.py (o la GUI) riceve la domanda, recupera i chunk piu' rilevanti e genera la risposta.
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 localemarkdown-it-py+mdit-py-plugins— parser Markdown AST-based per il chunkingPySide6— interfaccia grafica desktop (opzionale, solo per la GUI)
Dipendenze esterne:
- Ollama — server locale per embedding e generazione LLM
- MinerU — conversione PDF → Markdown (opzionale se hai gia' i file
.md)
Setup ambiente
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.
- Apri
mineru.ipynbsu Google Colab - Carica i PDF nella cartella
input/indicata nelle celle - Esegui tutte le celle — al termine viene scaricato uno ZIP per ogni documento
- Decomprimi gli ZIP nella cartella
sources/del progetto:
sources/
└── <stem>_output/
└── auto/
├── <stem>.md ← input della pipeline
└── images/
Opzione B — Installazione locale
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.
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_charsetarget_chars: dipendono dalla lunghezza media dei paragrafi del tuo documento e dalla finestra di contesto del modello di embedding. Conbge-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), abbassatarget_charsa 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.
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. 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
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
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. 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:Truerende le risposte piu' rapide disabilitando il ragionamento interno dei modelli Qwen3. Mettilo aFalseper 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.