Files
rag-from-scratch/README.md
T
davide af9ffc0559 docs(README): riscrittura per struttura reale del progetto
Sostituisce la struttura step-0…step-10 con la pipeline
effettiva: conversione/, revisione /prepare-md, chunking,
verifica, ollama/, vettorizzazione, interrogazione
2026-04-17 18:51:15 +02:00

469 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](#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)
- [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
```
### 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`
```bash
# 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:**
```bash
python conversione/validate.py
```
Mostra una tabella di stato per tutti gli stem convertiti. Vedi [`conversione/README.md`](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:**
```markdown
# 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`
```bash
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:**
```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.
---
### 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`](ollama/README.md) per istruzioni dettagliate e scelta dei modelli.
```bash
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`
```bash
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`](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
```bash
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)
```bash
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: `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 |
#### Test senza RAG
Per verificare che Ollama risponda correttamente prima di interrogare il documento:
```bash
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.