Files
rag-from-scratch/README.md
T

469 lines
15 KiB
Markdown
Raw Normal View History

2026-04-12 23:53:13 +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
2026-04-12 23:53:13 +02:00
**Compatibile con:** Linux · macOS · Windows (WSL2) · CPU Only · ~8 GB RAM libera
---
## Indice
- [Panoramica](#panoramica)
- [Struttura del progetto](#struttura-del-progetto)
- [Pipeline](#pipeline)
- [Conversione](#conversione)
- [Revisione Markdown](#revisione-markdown)
- [Chunking](#chunking)
- [Verifica chunk](#verifica-chunk)
- [Ambiente Ollama](#ambiente-ollama)
- [Vettorizzazione](#vettorizzazione)
- [Interrogazione](#interrogazione)
2026-04-12 23:53:13 +02:00
- [Principi di progettazione](#principi-di-progettazione)
---
## 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
2026-04-12 23:53:13 +02:00
```
### Dove si concentra il rischio
| Fase | Rischio | Motivo |
2026-04-12 23:53:13 +02:00
|---|---|---|
| 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` |
2026-04-12 23:53:13 +02:00
---
## 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
2026-04-12 23:53:13 +02:00
├── 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)
2026-04-12 23:53:13 +02:00
├── requirements.txt
├── .gitignore
└── README.md
```
---
## Pipeline
2026-04-12 23:53:13 +02:00
---
### Conversione
2026-04-12 23:53:13 +02:00
**Tipo:** automatico
**Input:** `sources/<stem>.pdf`
**Output:** `conversione/<stem>/clean.md` + `report.json`
**Script:** `conversione/pipeline.py`
2026-04-12 23:53:13 +02:00
```bash
# Singolo documento
python conversione/pipeline.py --stem <nome>
2026-04-12 23:53:13 +02:00
# Tutti i PDF in sources/
python conversione/pipeline.py
2026-04-12 23:53:13 +02:00
# Forza riesecuzione (sovrascrive output esistente)
python conversione/pipeline.py --stem <nome> --force
2026-04-12 23:53:13 +02:00
```
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.
2026-04-12 23:53:13 +02:00
Produce tre file in `conversione/<stem>/`:
2026-04-12 23:53:13 +02:00
| File | Descrizione |
2026-04-12 23:53:13 +02:00
|---|---|
| `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 |
2026-04-12 23:53:13 +02:00
**Requisiti aggiuntivi:** Java 11+ nel PATH (`opendataloader-pdf` lo richiede).
2026-04-12 23:53:13 +02:00
**Validazione batch:**
2026-04-12 23:53:13 +02:00
```bash
python conversione/validate.py
2026-04-12 23:53:13 +02:00
```
Mostra una tabella di stato per tutti gli stem convertiti. Vedi [`conversione/README.md`](conversione/README.md) per dettagli completi.
2026-04-12 23:53:13 +02:00
**PDF supportati:** digitali con testo selezionabile. Non supportati: scansionati (solo immagini) e protetti da password.
2026-04-12 23:53:13 +02:00
---
### Revisione Markdown
2026-04-12 23:53:13 +02:00
**Tipo:** semi-automatico
**Input:** `conversione/<stem>/clean.md`
**Output:** `conversione/<stem>/clean.md` corretto in-place
2026-04-12 23:53:13 +02:00
> Questo è il passaggio più importante dell'intera pipeline.
> La qualità del RAG dipende da questo passaggio più di qualsiasi
2026-04-12 23:53:13 +02:00
> parametro tecnico o scelta di modello.
```
/prepare-md conversione/<stem>/clean.md
2026-04-12 23:53:13 +02:00
```
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:**
```markdown
# Titolo del documento
## Sezione principale
2026-04-12 23:53:13 +02:00
### Sottosezione o unità atomica
Testo fluente, frasi complete, nessun artefatto.
Ogni paragrafo è semanticamente autonomo.
Una riga vuota separa le sezioni.
```
2026-04-12 23:53:13 +02:00
**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.
2026-04-12 23:53:13 +02:00
---
### Chunking
2026-04-12 23:53:13 +02:00
**Tipo:** automatico
**Input:** `conversione/<stem>/clean.md`
2026-04-13 13:36:53 +02:00
**Output:** `step-5/<stem>/chunks.json`
**Script:** `step-5/chunker.py`
2026-04-12 23:53:13 +02:00
```bash
python step-5/chunker.py --stem <stem>
2026-04-12 23:53:13 +02:00
```
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.
2026-04-12 23:53:13 +02:00
**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
2026-04-12 23:53:13 +02:00
**Parametri (in `step-5/chunker.py`):**
2026-04-12 23:53:13 +02:00
| 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:**
```json
{
"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.
2026-04-12 23:53:13 +02:00
---
### Verifica chunk
2026-04-12 23:53:13 +02:00
**Tipo:** automatico
2026-04-13 13:36:53 +02:00
**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` |
2026-04-12 23:53:13 +02:00
**Se i 🔴 persistono dopo il fix** — i casi tipici e la soluzione in `clean.md`:
2026-04-12 23:53:13 +02:00
| Sintomo nel report | Causa in `clean.md` | Correzione |
2026-04-12 23:53:13 +02:00
|---|---|---|
| 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 |
2026-04-12 23:53:13 +02:00
---
### Ambiente Ollama
2026-04-12 23:53:13 +02:00
**Tipo:** manuale (una volta sola)
**Input:** nessuno
**Output:** ambiente locale funzionante
**Script:** `ollama/check_env.py`
2026-04-12 23:53:13 +02:00
Installa Ollama, scarica i modelli e verifica l'ambiente. Si esegue una volta sola prima della vettorizzazione.
2026-04-12 23:53:13 +02:00
Vedi [`ollama/README.md`](ollama/README.md) per istruzioni dettagliate e scelta dei modelli.
2026-04-12 23:53:13 +02:00
```bash
python ollama/check_env.py
2026-04-12 23:53:13 +02:00
```
---
### Vettorizzazione
2026-04-12 23:53:13 +02:00
**Tipo:** automatico (lento)
2026-04-14 10:59:40 +02:00
**Input:** `step-6/<stem>/chunks.json`
**Output:** `chroma_db/<stem>` popolato
2026-04-14 10:59:40 +02:00
**Script:** `step-8/ingest.py`
2026-04-12 23:53:13 +02:00
```bash
2026-04-14 10:59:40 +02:00
source .venv/bin/activate
python step-8/ingest.py --stem <nome>
2026-04-12 23:53:13 +02:00
```
Trasforma ogni chunk in un vettore numerico e lo salva in ChromaDB.
2026-04-14 10:59:40 +02:00
È 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à |
2026-04-14 10:59:40 +02:00
**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.
2026-04-12 23:53:13 +02:00
**Cosa succede per ogni chunk:**
```
testo del chunk
▼ Ollama (EMBED_MODEL)
vettore N-dim
2026-04-12 23:53:13 +02:00
▼ ChromaDB
2026-04-14 10:59:40 +02:00
salva: testo + vettore + metadati (sezione, titolo, sub_index)
2026-04-12 23:53:13 +02:00
```
Vedi [`step-8/README.md`](step-8/README.md) per la scelta del modello di embedding e le regole di coerenza con la fase di interrogazione.
2026-04-12 23:53:13 +02:00
---
2026-04-12 23:53:13 +02:00
### Interrogazione
2026-04-12 23:53:13 +02:00
**Tipo:** interattivo
**Input:** `chroma_db/<stem>` + domanda dell'utente
**Output:** risposta basata sul documento
2026-04-14 10:59:40 +02:00
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 |
2026-04-12 23:53:13 +02:00
#### rag.py — Risposta in linguaggio naturale
```bash
source .venv/bin/activate
python rag.py --stem <nome>
2026-04-12 23:53:13 +02:00
```
| Sintassi | Comportamento |
|---|---|
| `<testo>` | Risposta basata sul documento |
| `<testo> -v` | Risposta + chunk recuperati con score di similarità |
| `exit` | Esce dal programma |
2026-04-12 23:53:13 +02:00
Flusso interno:
2026-04-12 23:53:13 +02:00
```
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
```
2026-04-12 23:53:13 +02:00
#### retrieve.py — Retrieval puro (senza LLM)
2026-04-12 23:53:13 +02:00
```bash
source .venv/bin/activate
python retrieve.py --stem <nome>
2026-04-12 23:53:13 +02:00
```
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.
2026-04-12 23:53:13 +02:00
| 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 |
2026-04-12 23:53:13 +02:00
Accetta `--top-k N` per sovrascrivere il valore di `config.py` per quella sessione.
2026-04-12 23:53:13 +02:00
#### Configurazione (`config.py`)
2026-04-12 23:53:13 +02:00
| Parametro | Default | Descrizione |
|---|---|---|
| `TOP_K` | `6` | Chunk recuperati per ogni domanda. Valori consigliati: `3``10` |
| `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`](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 |
2026-04-12 23:53:13 +02:00
#### Test senza RAG
2026-04-12 23:53:13 +02:00
Per verificare che Ollama risponda correttamente prima di interrogare il documento:
2026-04-12 23:53:13 +02:00
```bash
python ollama/test_ollama.py
2026-04-12 23:53:13 +02:00
```
Chat diretta con il modello, senza ChromaDB. Usa gli stessi parametri di `config.py`.
2026-04-12 23:53:13 +02:00
---
## Principi di progettazione
**Atomico**
Ogni fase fa una cosa sola. Il chunker non sa niente di Ollama.
2026-04-12 23:53:13 +02:00
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.
2026-04-12 23:53:13 +02:00
**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.
2026-04-12 23:53:13 +02:00
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.