davide 4f28358ec1 feat: pipeline RAG consolidata — conversione unificata, refactor struttura, CLAUDE.md minimale
Branch ollama introduce:
- Pipeline conversione PDF → Markdown unificata (conversione/pipeline.py) con 30+ transform
  che sostituisce i vecchi step-0..step-4
- Ambiente Ollama (ollama/) con check_env.py e test_ollama.py
- Rimozione cartelle step-0..step-4, step-7, step-9 obsolete
- Consolidamento script alla root
- CLAUDE.md riscritto: path reali, istruzioni minime, agnostico alla struttura step-X
- validate.py con scoring orientato a chunking/vettorizzazione
- README riallineato alla struttura reale del repo
2026-04-20 11:06:18 +02:00

RAG from Scratch — Singolo PDF Generico

Sistema RAG (Retrieval-Augmented Generation) costruito da zero, senza framework di alto livello. Funziona su qualsiasi PDF digitale. Gira interamente in locale, senza GPU, senza cloud.

Stack: Python · Ollama · ChromaDB · Qwen3-embedding · Qwen3.5
Compatibile con: Linux · macOS · Windows (WSL2) · CPU Only · ~8 GB RAM libera


Indice


Panoramica

PDF (sources/)
    │
    ▼  conversione/pipeline.py
clean.md  ←  revisiona con /prepare-md
    │
    ▼  step-5/chunker.py
chunks.json
    │
    ▼  step-6/verify_chunks.py + fix_chunks.py
chunks.json verificato
    │
    ▼  step-8/ingest.py
ChromaDB
    │
    ▼  rag.py
risposta

Dove si concentra il rischio

Fase Rischio Motivo
Conversione 🟡 Medio Automatica, ma il PDF deve essere digitale e non protetto
Revisione Markdown 🔴 Alto Manuale — la qualità del MD determina la qualità del RAG
Chunking 🟡 Medio Logica adattiva, dipende dalla qualità del MD
Verifica chunk 🟢 Basso Automatica, solo verifica
Ambiente Ollama 🟢 Basso Installazione standard
Vettorizzazione 🟢 Basso Meccanica, lenta ma affidabile
Interrogazione 🟡 Medio Qualità del prompt e dei parametri in config.py

Struttura del progetto

rag-from-scratch/
│
├── sources/                        # PDF originali — non modificare mai
│   └── documento.pdf
│
├── conversione/                    # PDF → Markdown strutturato
│   ├── pipeline.py                 # Conversione PDF → clean.md
│   ├── validate.py                 # Validazione batch di tutti gli stem
│   └── <stem>/
│       ├── raw.md                  # MD grezzo (non toccare)
│       ├── clean.md                # MD pulito — copia di lavoro
│       └── report.json             # Metriche qualità conversione
│
├── step-5/                         # Chunking adattivo
│   ├── chunker.py
│   └── <stem>/
│       └── chunks.json
│
├── step-6/                         # Verifica e fix chunk
│   ├── verify_chunks.py
│   ├── fix_chunks.py
│   └── <stem>/
│       └── chunks.json             # Chunk verificati
│
├── step-8/                         # Vettorizzazione → ChromaDB
│   ├── ingest.py
│   └── README.md
│
├── ollama/                         # Ambiente Ollama
│   ├── check_env.py                # Verifica prerequisiti
│   ├── test_ollama.py              # Test chat senza RAG
│   └── README.md
│
├── chroma_db/                      # Vector store — generato da ingest.py
├── config.py                       # Configurazione pipeline RAG ← modifica qui
├── rag.py                          # Pipeline RAG interattiva
├── retrieve.py                     # Retrieval puro (senza LLM)
├── requirements.txt
├── .gitignore
└── README.md

Pipeline


Conversione

Tipo: automatico
Input: sources/<stem>.pdf
Output: conversione/<stem>/clean.md + report.json
Script: conversione/pipeline.py

# Singolo documento
python conversione/pipeline.py --stem <nome>

# Tutti i PDF in sources/
python conversione/pipeline.py

# Forza riesecuzione (sovrascrive output esistente)
python conversione/pipeline.py --stem <nome> --force

Converte il PDF in Markdown strutturato in quattro fasi automatiche: validazione, estrazione testo (algoritmo XY-Cut++ per layout multi-colonna), pulizia strutturale e analisi della struttura del documento.

Produce tre file in conversione/<stem>/:

File Descrizione
raw.md Markdown grezzo estratto dal PDF — non modificare mai
clean.md Markdown pulito e strutturato — input per il chunker
report.json Metriche qualità, anomalie, strategia di chunking suggerita

Requisiti aggiuntivi: Java 11+ nel PATH (opendataloader-pdf lo richiede).

Validazione batch:

python conversione/validate.py

Mostra una tabella di stato per tutti gli stem convertiti. Vedi conversione/README.md per dettagli completi.

PDF supportati: digitali con testo selezionabile. Non supportati: scansionati (solo immagini) e protetti da password.


Revisione Markdown

Tipo: semi-automatico
Input: conversione/<stem>/clean.md
Output: conversione/<stem>/clean.md corretto in-place

Questo è il passaggio più importante dell'intera pipeline. La qualità del RAG dipende da questo passaggio più di qualsiasi parametro tecnico o scelta di modello.

/prepare-md conversione/<stem>/clean.md

La skill analizza il clean.md e corregge automaticamente i problemi che compromettono il chunking: sillabazione, artefatti, header malformati, paragrafi spezzati, gerarchia incoerente, sezioni vuote.

Struttura target dopo la revisione:

# Titolo del documento

## Sezione principale

### Sottosezione o unità atomica

Testo fluente, frasi complete, nessun artefatto.
Ogni paragrafo è semanticamente autonomo.
Una riga vuota separa le sezioni.

Criterio di qualità: leggi ogni sezione ad alta voce. Se suona naturale è corretta. Se si interrompe c'è una riga spezzata. Se suona ripetitiva c'è un artefatto.


Chunking

Tipo: automatico
Input: conversione/<stem>/clean.md
Output: step-5/<stem>/chunks.json
Script: step-5/chunker.py

python step-5/chunker.py --stem <stem>

Divide il Markdown pulito in chunk. Usa il profilo strutturale da report.json per scegliere la strategia giusta. Non sa nulla del contenuto — si basa solo sulla struttura.

Regole invarianti per qualsiasi documento:

  • Un chunk non attraversa mai il confine tra due sezioni diverse
  • Un chunk non spezza mai una frase a metà
  • Ogni chunk porta il suo contesto nel prefisso
  • L'overlap tra chunk avviene solo su frasi intere, mai tra sezioni diverse

Parametri (in step-5/chunker.py):

Parametro Default Significato
MIN_CHARS 200 Sotto questa soglia, accorpa al chunk successivo
MAX_CHARS 800 Sopra questa soglia, spezza su frasi
OVERLAP_S 2 Frasi di overlap tra sotto-chunk dello stesso boundary

Struttura di ogni chunk:

{
  "chunk_id": "sezione_principale__sottosezione_3__s0",
  "text": "[Sezione principale > Sottosezione 3]\nTesto del chunk...",
  "sezione": "Sezione principale",
  "titolo": "Sottosezione 3",
  "sub_index": 0,
  "n_chars": 412
}

Il prefisso [Sezione > Titolo] è fondamentale: permette all'embedding di catturare il contesto topico del chunk anche quando il testo da solo sarebbe ambiguo.


Verifica chunk

Tipo: automatico
Input: step-5/<stem>/chunks.json
Output: step-6/<stem>/chunks.json verificato + report.json
Script: step-6/verify_chunks.py, step-6/fix_chunks.py

Questo passaggio si articola in un ciclo: verifica → fix automatico → ri-verifica. Non si procede alla vettorizzazione finché non ci sono 🔴.

Workflow:

1. Verifica
   python step-6/verify_chunks.py --stem <stem>

2a. Se ✅ OK o solo 🟡 → vai alla vettorizzazione

2b. Se ci sono 🔴 → prova il fix automatico:
   python step-6/fix_chunks.py --stem <stem> --dry-run   # anteprima
   python step-6/fix_chunks.py --stem <stem>             # applica

3. Ri-verifica dopo il fix:
   python step-6/verify_chunks.py --stem <stem>

4. Se rimangono 🔴 → torna alla revisione Markdown e correggi clean.md,
   poi riesegui dall'inizio:
   python step-5/chunker.py --stem <stem> --force
   python step-6/verify_chunks.py --stem <stem>

Shortcut con Claude: usa /step6-fix <stem> — esegue dry-run, spiega le operazioni, chiede conferma e ri-verifica automaticamente.

Output di verify_chunks.py — tre condizioni finali:

Condizione Significato Cosa fare
✅ N/N documenti senza problemi Nessun problema Procedi
🟡 Solo avvisi minori Chunk corti o lunghi, non bloccanti Puoi procedere o ottimizzare con fix_chunks.py
⚠️ 0/N documenti senza problemi + 🔴 Frasi spezzate o chunk vuoti Esegui fix_chunks.py, poi ri-verifica

Cosa verifica:

  • Nessun chunk sotto MIN_CHARS 🟡
  • Nessun chunk sopra MAX_CHARS × 1.5 🟡
  • Ogni chunk finisce con punteggiatura di fine frase 🔴

Cosa corregge fix_chunks.py:

Operazione Quando
Rimuovi chunk vuoti Chunk privi di testo
Fondi chunk incompleto col successivo Chunk che finisce senza punteggiatura
Fondi chunk troppo corto col successivo Chunk sotto MIN_CHARS
Spezza chunk troppo lungo Chunk sopra MAX_CHARS × 1.5

Se i 🔴 persistono dopo il fix — i casi tipici e la soluzione in clean.md:

Sintomo nel report Causa in clean.md Correzione
Chunk finisce con : Intro di un elenco separata dall'elenco da una riga vuota Rimuovi la riga vuota tra l'intro e la lista
Chunk finisce a metà parola Numero di pagina nel mezzo del testo Trova e rimuovi il numero di pagina, unisci le righe
Chunk con testo artefatto Artefatto non rimosso nella revisione Elimina la sezione in clean.md
Chunk con frase enorme non spezzabile Paragrafo >MAX_CHARS senza frasi intermedie Spezza manualmente il paragrafo

Ambiente Ollama

Tipo: manuale (una volta sola)
Input: nessuno
Output: ambiente locale funzionante
Script: ollama/check_env.py

Installa Ollama, scarica i modelli e verifica l'ambiente. Si esegue una volta sola prima della vettorizzazione.

Vedi ollama/README.md per istruzioni dettagliate e scelta dei modelli.

python ollama/check_env.py

Vettorizzazione

Tipo: automatico (lento)
Input: step-6/<stem>/chunks.json
Output: chroma_db/<stem> popolato
Script: step-8/ingest.py

source .venv/bin/activate
python step-8/ingest.py --stem <nome>

Trasforma ogni chunk in un vettore numerico e lo salva in ChromaDB. È il processo più lento — su CPU circa 1 secondo per chunk. Per 900 chunk aspetta circa 15 minuti.

Argomenti:

Argomento Descrizione
--stem <nome> Processa un singolo documento. Senza questo argomento processa tutti gli stem trovati in step-6/
--force Cancella e ricrea la collection se esiste già

Quando usare --force: Se hai modificato i chunk o cambiato EMBED_MODEL in config.py, la collection in ChromaDB contiene i vecchi vettori. --force la cancella e ricrea da zero.

Cosa succede per ogni chunk:

testo del chunk
    │
    ▼  Ollama (EMBED_MODEL)
vettore N-dim
    │
    ▼  ChromaDB
salva: testo + vettore + metadati (sezione, titolo, sub_index)

Vedi step-8/README.md per la scelta del modello di embedding e le regole di coerenza con la fase di interrogazione.


Interrogazione

Tipo: interattivo
Input: chroma_db/<stem> + domanda dell'utente
Output: risposta basata sul documento

Due modalità:

Script Modalità Quando usarlo
rag.py Retrieval + generazione LLM Risposta in linguaggio naturale
retrieve.py Solo retrieval (no LLM) Debug, verifica chunk, ricerca semantica

rag.py — Risposta in linguaggio naturale

source .venv/bin/activate
python rag.py --stem <nome>
Sintassi Comportamento
<testo> Risposta basata sul documento
<testo> -v Risposta + chunk recuperati con score di similarità
exit Esce dal programma

Flusso interno:

domanda
    │
    ▼  embed (EMBED_MODEL, Ollama)
vettore N-dim
    │
    ▼  query ChromaDB — similarità coseno, top-K
chunk rilevanti
    │
    ▼  build_prompt (SYSTEM_PROMPT + contesti + domanda)
    │
    ▼  generate (OLLAMA_MODEL, Ollama)
risposta

retrieve.py — Retrieval puro (senza LLM)

source .venv/bin/activate
python retrieve.py --stem <nome>

Vettorizza la query e restituisce i chunk più simili con score di similarità — senza chiamare Ollama per la generation. Utile per verificare la qualità del retrieval e diagnosticare risposte sbagliate.

Sintassi Comportamento
<testo> Chunk più simili con score (testo troncato a 200 car.)
<testo> -f Chunk più simili con testo completo
exit Esce dal programma

Accetta --top-k N per sovrascrivere il valore di config.py per quella sessione.

Configurazione (config.py)

Parametro Default Descrizione
TOP_K 6 Chunk recuperati per ogni domanda. Valori consigliati: 310
TEMPERATURE 0.0 Deterministico a 0.0, creativo verso 1.0. Per RAG consigliato 0.0
NO_THINK True Disabilita il chain-of-thought interno dei modelli Qwen3/Qwen3.5. True = risposta diretta, più veloce
EMBED_MODEL "nomic-embed-text" Deve corrispondere al modello usato in ingest.py. Se cambiato, rieseguire con --force
OLLAMA_URL "http://localhost:11434" Modifica solo se Ollama gira su porta o host diversi
OLLAMA_MODEL "qwen3.5:0.8b" Modello LLM. Vedi ollama/README.md per la scelta
SYSTEM_PROMPT (vedi file) Istruzioni di comportamento inviate al LLM. Modifica per cambiare tono, lingua o condizione di fallback

Test senza RAG

Per verificare che Ollama risponda correttamente prima di interrogare il documento:

python ollama/test_ollama.py

Chat diretta con il modello, senza ChromaDB. Usa gli stessi parametri di config.py.


Principi di progettazione

Atomico Ogni fase fa una cosa sola. Il chunker non sa niente di Ollama. L'ingestione non sa niente del MD originale. Se un pezzo si rompe, sai esattamente dove.

Verificabile Ogni fase ha un criterio di completamento oggettivo. Non si passa alla fase successiva finché la precedente non è verificata.

Reversibile Puoi tornare indietro senza perdere il lavoro delle altre fasi. Cambi il MD? Riesegui solo chunking e vettorizzazione. Cambi i parametri del chunker? Riesegui solo chunking e vettorizzazione. Non si riparte mai da zero.

Senza assunzioni Il sistema non assume nulla sulla struttura del documento. Rileva il livello strutturale e si adatta. Funziona su libri, manuali, articoli, contratti, dispense.

Tutto locale Nessuna chiamata a API esterne. Nessun dato trasmesso fuori dalla macchina. Nessun costo di utilizzo.

S
Description
Sistema RAG costruito da zero su qualsiasi PDF digitale. Ogni fase della pipeline - estrazione, chunking adattivo, vettorizzazione, retrieval e generazione - è uno step separato e verificabile. Gira interamente in locale su CPU, senza GPU e senza cloud. Stack: Python - Ollama - ChromaDB.
Readme 789 KiB
Languages
Python 99.6%
Shell 0.4%