# 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 │ └── / │ ├── raw.md # MD grezzo (non toccare) │ ├── clean.md # MD pulito — copia di lavoro │ └── report.json # Metriche qualità conversione │ ├── step-5/ # Chunking adattivo │ ├── chunker.py │ └── / │ └── chunks.json │ ├── step-6/ # Verifica e fix chunk │ ├── verify_chunks.py │ ├── fix_chunks.py │ └── / │ └── 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/.pdf` **Output:** `conversione//clean.md` + `report.json` **Script:** `conversione/pipeline.py` ```bash # Singolo documento python conversione/pipeline.py --stem # Tutti i PDF in sources/ python conversione/pipeline.py # Forza riesecuzione (sovrascrive output esistente) python conversione/pipeline.py --stem --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//`: | 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//clean.md` **Output:** `conversione//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//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//clean.md` **Output:** `step-5//chunks.json` **Script:** `step-5/chunker.py` ```bash python step-5/chunker.py --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//chunks.json` **Output:** `step-6//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 2a. Se ✅ OK o solo 🟡 → vai alla vettorizzazione 2b. Se ci sono 🔴 → prova il fix automatico: python step-6/fix_chunks.py --stem --dry-run # anteprima python step-6/fix_chunks.py --stem # applica 3. Ri-verifica dopo il fix: python step-6/verify_chunks.py --stem 4. Se rimangono 🔴 → torna alla revisione Markdown e correggi clean.md, poi riesegui dall'inizio: python step-5/chunker.py --stem --force python step-6/verify_chunks.py --stem ``` > **Shortcut con Claude:** usa `/step6-fix ` — 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//chunks.json` **Output:** `chroma_db/` popolato **Script:** `step-8/ingest.py` ```bash source .venv/bin/activate python step-8/ingest.py --stem ``` 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 ` | 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/` + 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 ``` | Sintassi | Comportamento | |---|---| | `` | Risposta basata sul documento | | ` -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 ``` 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 | |---|---| | `` | Chunk più simili con score (testo troncato a 200 car.) | | ` -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.