af9ffc0559
Sostituisce la struttura step-0…step-10 con la pipeline effettiva: conversione/, revisione /prepare-md, chunking, verifica, ollama/, vettorizzazione, interrogazione
469 lines
15 KiB
Markdown
469 lines
15 KiB
Markdown
# 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.
|