Files
rag-from-scratch/README.md
T
davide 9261e480da 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>
2026-06-09 10:18:46 +02:00

11 KiB
Raw Blame History

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

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 — 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.

  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

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_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)

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 (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.