# 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 · nomic-embed-text · Qwen3.5 · ChromaDB **Compatibile con:** Linux · macOS · Windows (WSL2) · CPU Only · ~8 GB RAM libera --- ## Indice - [Panoramica](#panoramica) - [Struttura del progetto](#struttura-del-progetto) - [Gli step](#gli-step) - [Step 0 — Scegli il PDF](#step-0--scegli-il-pdf) - [Step 1 — Ispezione automatica](#step-1--ispezione-automatica) - [Step 2 — Conversione in Markdown grezzo](#step-2--conversione-in-markdown-grezzo) - [Step 3 — Rilevamento struttura](#step-3--rilevamento-struttura) - [Step 4 — Revisione manuale](#step-4--revisione-manuale) - [Step 5 — Chunking adattivo](#step-5--chunking-adattivo) - [Step 6 — Verifica chunk](#step-6--verifica-chunk) - [Step 7 — Installazione ambiente](#step-7--installazione-ambiente) - [Step 8 — Vettorizzazione](#step-8--vettorizzazione) - [Step 9 — Pipeline RAG](#step-9--pipeline-rag) - [Step 10 — Test automatici](#step-10--test-automatici) - [Principi di progettazione](#principi-di-progettazione) --- ## Panoramica ``` PDF └─► STEP 1 Ispezione automatica └─► STEP 2 Conversione in Markdown grezzo └─► STEP 3 Rilevamento struttura └─► STEP 4 Revisione manuale ← step più importante └─► STEP 5 Chunking adattivo └─► STEP 6 Verifica chunk └─► STEP 8 Vettorizzazione └─► STEP 9 Pipeline RAG └─► STEP 10 Test automatici STEP 0 Prerequisito iniziale (PDF adatto) STEP 7 Prerequisito tecnico (ambiente locale) ``` ### Dove si concentra il rischio | Step | Rischio | Motivo | |---|---|---| | Step 0 | 🔴 Alto | Un PDF inadatto invalida tutto il lavoro successivo | | Step 1 | 🟢 Basso | Automatico, solo osservazione | | Step 2 | 🟢 Basso | Automatico, tool maturo | | Step 3 | 🟢 Basso | Automatico, solo analisi | | Step 4 | 🔴 Alto | Manuale — la qualità del MD determina la qualità del RAG | | Step 5 | 🟡 Medio | Logica adattiva, dipende dalla qualità del MD | | Step 6 | 🟢 Basso | Automatico, solo verifica | | Step 7 | 🟢 Basso | Installazione standard | | Step 8 | 🟢 Basso | Meccanico, lento ma affidabile | | Step 9 | 🟡 Medio | Qualità del prompt | | Step 10 | 🟢 Basso | Test automatici | --- ## Struttura del progetto ``` rag-from-scratch/ │ ├── sources/ # PDF originali — non modificare mai │ └── documento.pdf │ ├── step-0/ │ └── check_pdf.py # Verifica requisiti del PDF │ ├── step-1/ │ └── inspect_pdf.py # Ispezione automatica del PDF │ ├── step-2/ │ ├── convert_pdf.py # Conversione PDF → Markdown grezzo │ └── / │ └── raw.md # MD grezzo (non toccare) │ ├── step-3/ │ ├── detect_structure.py # Rilevamento struttura MD │ └── / │ └── structure_profile.json # Profilo struttura │ ├── step-4/ │ ├── revise.py # Pre-processing automatico MD │ ├── revision_log.md # Log modifiche manuali │ └── / │ ├── clean.md # MD revisionato │ └── structure_profile.json # Profilo aggiornato │ ├── step-5/ │ ├── chunker.py # Chunking adattivo │ └── / │ └── chunks.json # Chunk pronti per la vettorizzazione │ ├── step-6/ │ ├── verify_chunks.py # Verifica chunk │ ├── fix_chunks.py # Fix chunk problematici │ └── / │ └── chunks.json # Chunk verificati │ ├── step-7/ │ ├── check_env.py # Verifica ambiente locale │ └── README.md # Guida installazione Ollama e dipendenze │ ├── step-8/ │ └── ingest.py # Vettorizzazione → ChromaDB │ ├── step-9/ │ ├── config.py # Configurazione pipeline RAG ← modifica qui │ ├── rag.py # Pipeline RAG interattiva │ ├── test_ollama.py # Test chat Ollama senza RAG │ └── README.md │ ├── chroma_db/ # Vector store — generato da step-8 ├── requirements.txt ├── .gitignore └── README.md ``` --- ## Gli step --- ### Step 0 — Scegli il PDF **Tipo:** prerequisito manuale **Input:** nessuno **Output:** un PDF adatto al sistema Il PDF deve soddisfare requisiti minimi prima di qualsiasi elaborazione. Un PDF inadatto rende tutto il lavoro successivo inutile. **Criteri obbligatori:** - Il testo è selezionabile nel PDF reader — se non riesci a copiare una parola, pdfplumber non la leggerà - Non è protetto da password - È generato digitalmente, non scansionato — una foto di un libro non è un PDF di testo - Il contenuto importante è nel testo, non nelle immagini **Criteri desiderabili:** - Ha una struttura logica riconoscibile: capitoli, sezioni, paragrafi - Le sezioni hanno titoli espliciti - Non ha layout a colonne multiple - È in una lingua sola o prevalentemente una **Come verificarlo:** Apri il PDF nel tuo reader, seleziona del testo da pagine diverse e copialo. Se il testo copiato è leggibile e nell'ordine giusto, il PDF è adatto. Se ottieni caratteri strani o testo nell'ordine sbagliato, il PDF ha problemi. --- ### Step 1 — Ispezione automatica **Tipo:** automatico **Input:** tutti i PDF in `sources/` **Output:** `step-1/_step1_report.txt` **Script:** `step-1/inspect_pdf.py` ```bash python step-1/inspect_pdf.py ``` Lo script scansiona automaticamente tutti i PDF in `sources/`, analizza ogni documento pagina per pagina e produce un report. Serve per capire la qualità del documento e mappare i problemi prima di affrontare la revisione manuale. **Cosa rileva:** - Testo non estraibile (pagine con sole immagini) - Sillabazioni a fine riga - Layout a colonne (righe molto corte e numerose) - Intestazioni e piè di pagina ripetitivi - Caratteri Unicode anomali - Pagine vuote **Output del report:** ``` Score: 87/100 Pagine totali: 243 Pagine con problemi: 12 Pagina 14: sillabazione rilevata (3 occorrenze) Pagina 67: possibile layout a colonne Pagina 201: caratteri Unicode anomali PROSSIMI PASSI: → conversione con marker funzionerà bene → attenzione alle pagine 14 e 67 nella revisione manuale ``` **Decisione:** | Score | Azione | |---|---| | ≥ 70 | Procedi allo step 2 | | 40–70 | Procedi con cautela, revisione estesa necessaria | | < 40 | Valuta una fonte PDF migliore | --- ### Step 2 — Conversione in Markdown grezzo **Tipo:** automatico **Input:** tutti i PDF in `sources/` (o uno solo con `--pdf`) **Output:** `step-2//raw.md` + `step-2//clean.md` **Script:** `step-2/convert_pdf.py` ```bash python step-2/convert_pdf.py # tutti i PDF in sources/ python step-2/convert_pdf.py --pdf sources/doc.pdf # un solo PDF ``` Converte il PDF in Markdown usando `pymupdf4llm`. Il risultato non è perfetto — è la base su cui lavorerai nello step 4. Lo script crea due file: - `raw.md` — conversione grezza, **non modificare mai**. È il punto di partenza di riferimento. - `clean.md` — copia di lavoro che verrà modificata negli step successivi. **Cosa produce la conversione:** - Titoli riconosciuti e convertiti in `#` `##` `###` - Paragrafi separati da righe vuote - Sillabazione parzialmente risolta **Cosa non produce:** - Rimozione intestazioni e piè di pagina - Correzione completa del layout a colonne - Descrizione del contenuto delle immagini --- ### Step 3 — Rilevamento struttura **Tipo:** automatico **Input:** `step-2//` **Output:** `step-3//structure_profile.json` **Script:** `step-3/detect_structure.py` ```bash python step-3/detect_structure.py # tutti i documenti in step-2/ python step-3/detect_structure.py --stem # un solo documento python step-3/detect_structure.py --force # riesegui anche se già presente ``` Copia `raw.md` e `clean.md` da `step-2//` e analizza la struttura del Markdown senza modificarla. Il profilo prodotto guida sia la revisione manuale che il chunker. **I quattro livelli strutturali:** ``` Livello 3 — struttura ricca Il documento ha ### con regolarità. Ogni ### è un'unità semantica chiara. Esempi: opere filosofiche, manuali tecnici, leggi. Strategia chunking: boundary su ### Livello 2 — struttura parziale Il documento ha ## ma pochi o nessun ###. Le sezioni sono i capitoli, non le sottosezioni. Esempi: articoli scientifici, report, saggi. Strategia chunking: boundary su ##, split interno su paragrafi Livello 1 — solo paragrafi Il documento non ha titoli significativi. La struttura è data dalle righe vuote. Esempi: testi narrativi, lettere, trascrizioni. Strategia chunking: boundary su paragrafo Livello 0 — testo piatto Un blocco continuo senza struttura riconoscibile. Esempi: PDF mal convertiti, testi antichi. Strategia chunking: sliding window su frasi ``` **Profilo prodotto:** ```json { "livello_struttura": 3, "n_h1": 1, "n_h2": 9, "n_h3": 296, "n_paragrafi": 312, "boundary_primario": "h3", "lingua_rilevata": "it", "lunghezza_media_sezione": 420, "strategia_chunking": "h3_aware", "avvertenze": [ "14 sezioni sotto i 200 caratteri — verranno accorpate", "8 sezioni sopra i 800 caratteri — verranno divise" ] } ``` --- ### Step 4 — Revisione manuale **Tipo:** manuale (con pre-processing automatico) **Input:** `step-3//clean.md` + `step-3//structure_profile.json` **Output:** `step-4//clean.md` — MD revisionato **Script:** `step-4/revise.py` > Questo è lo step più importante dell'intera pipeline. > La qualità del RAG dipende da questo step più di qualsiasi > parametro tecnico o scelta di modello. #### Pre-processing automatico Prima di qualsiasi revisione manuale, esegui lo script di revisione automatica: ```bash python step-4/revise.py --stem documento ``` Lo script applica le seguenti trasformazioni euristiche, valide per qualsiasi documento: | Trasformazione | Descrizione | |---|---| | Rimozione TOC | Righe che iniziano con `INDICE`, `INDEX`, `CONTENTS`, ecc. | | ALL-CAPS → `##` | Righe standalone in maiuscolo convertite in header section-case | | `N. testo` → `### N.` | Sezioni numerate (con 1+ spazio dopo il punto) convertite in h3 | | Unione paragrafi | Blocchi spezzati da salti pagina PDF uniti automaticamente | | Whitespace | Spazi multipli normalizzati, righe vuote ridotte | Il profilo strutturale aggiornato viene salvato in `step-4//structure_profile.json`. #### Revisione assistita da Claude Code Dopo il pre-processing, usa la skill integrata per una revisione qualitativa: ``` /step4-review documento ``` La skill analizza `step-4//clean.md` e produce un report strutturato: ``` 🔴 BLOCCANTI — problemi che compromettono il chunking 🟡 MINORI — artefatti visibili ma non bloccanti 🟢 OK — categorie senza problemi ``` Poi propone le correzioni e le applica solo su tua approvazione. #### Revisione manuale (senza Claude Code) Se non usi Claude Code, esegui questi 6 check dal terminale. In tutti i comandi sostituisci `` con il nome reale del documento. **Check 1 — Sillabazione residua** Parole spezzate a fine riga con trattino (artefatto da PDF non risolto): ```bash grep -n "\-$" step-4//clean.md | head -20 ``` Se trovi risultati: unisci la riga con la successiva eliminando il trattino e il ritorno a capo. **Check 2 — Righe orfane** Righe brevi (<60 char) isolate che sembrano numeri di pagina, autori, intestazioni: ```bash grep -n "^[^#\-\*\|].\{1,59\}$" step-4//clean.md | grep -v "^\s*$" | head -30 ``` Per ogni riga: valuta se è testo legittimo (frase breve) o artefatto (numero di pagina, nome autore ripetuto, intestazione PDF). Gli artefatti vanno eliminati. **Check 3 — Frasi spezzate** Paragrafi che terminano senza punteggiatura di fine frase: ```bash grep -n "[^.!?»)\]\'\"]$" step-4//clean.md \ | grep -v "^[0-9]*:#" \ | grep -v "^[0-9]*:\s*$" \ | grep -v "^\s*[-\*]" \ | head -20 ``` Segnala le righe brevi che finiscono a metà concetto. Uniscile alla riga successiva. **Check 4 — Header sospetti** ```bash grep -n "^##\? " step-4//clean.md | head -40 ``` Verifica: - Header con testo >80 caratteri → probabilmente è testo normale, non un header - Header in MAIUSCOLO non convertito → cambia in formato sentence-case - Header duplicati (stesso testo due volte) → valuta se unire o rinominare - `###` senza un `##` padre → salto di gerarchia anomalo **Check 5 — Sezioni quasi vuote** ```bash python3 -c " import re text = open('step-4//clean.md').read() sections = re.split(r'^(#{1,3} .+)$', text, flags=re.MULTILINE) for i in range(1, len(sections)-1, 2): header = sections[i].strip() body = sections[i+1].strip() if i+1 < len(sections) else '' if not body: print(f'VUOTA: {header!r}') elif len(body) < 80: print(f'CORTA ({len(body)} char): {header!r} → {body[:60]!r}') " ``` Le sezioni vuote generano chunk inutili. Eliminale o accorpale alla sezione precedente. **Check 6 — Gerarchia strutturale** ```bash grep -n "^#\{1,3\} " step-4//clean.md | head -50 ``` Deve esserci un solo `# h1` all'inizio. Poi `## h2` e opzionalmente `### h3`. Segnala `###` prima del primo `##`, o più di un `#`. --- **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. --- ### Step 5 — Chunking adattivo **Tipo:** automatico **Input:** `step-4//clean.md` + `step-4//structure_profile.json` **Output:** `step-5//chunks.json` **Script:** `step-5/chunker.py` ```bash python step-5/chunker.py --stem documento ``` Divide il Markdown pulito in chunk. Usa il profilo strutturale 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:** | 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. --- ### Step 6 — Verifica e fix 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 step si articola in un ciclo: verifica → fix automatico → ri-verifica. Non si va allo step 8 finché non ci sono 🔴. **Workflow completo:** ``` 1. Verifica python step-6/verify_chunks.py --stem documento 2a. Se ✅ OK o solo 🟡 → vai allo step 8 2b. Se ci sono 🔴 → prova il fix automatico: python step-6/fix_chunks.py --stem documento --dry-run # anteprima python step-6/fix_chunks.py --stem documento # applica 3. Ri-verifica dopo il fix: python step-6/verify_chunks.py --stem documento 4. Se rimangono 🔴 → torna allo step 4 e correggi clean.md, poi riesegui dall'inizio: python step-5/chunker.py --stem documento --force python step-6/verify_chunks.py --stem documento ``` > **Shortcut con Claude:** usa `/step6-fix ` — esegue dry-run, spiega le operazioni, chiede conferma e ri-verifica automaticamente. #### Senza Claude Code — come leggere l'output e decidere **1. Leggi l'output di `verify_chunks.py`** L'output termina con una delle tre condizioni: | Condizione | Significato | Cosa fare | |---|---|---| | `✅ N/N documenti senza problemi` | Nessun problema | Vai allo step 8 | | `🟡 Solo avvisi minori` | Chunk corti o lunghi, non bloccanti | Puoi andare allo step 8 oppure ottimizzare con `fix_chunks.py` | | `⚠️ 0/N documenti senza problemi` + 🔴 | Frasi spezzate o chunk vuoti | Esegui `fix_chunks.py`, poi ri-verifica | **2. Prima di applicare il fix: leggi il dry-run** ```bash python step-6/fix_chunks.py --stem --dry-run ``` L'output elenca le operazioni pianificate. Significato: | Operazione | Cosa fa | Sicurezza | |---|---|---| | `fondi N chunk incompleti` | Unisce il chunk troncato col successivo | Sempre sicura | | `fondi N chunk troppo corti` | Unisce chunk <200 char col successivo | Sicura se il risultato non supera MAX×1.5 | | `spezza N chunk troppo lunghi` | Divide chunk >1200 char su frasi | Sicura solo se esistono frasi naturali dove spezzare | | `rimuovi N chunk vuoti` | Elimina chunk senza testo | Sempre sicura | **3. Se i 🔴 persistono dopo il fix** `fix_chunks.py` non riesce ad autocorreggersi quando il problema è nella struttura del testo sorgente. 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 | Salto di pagina PDF con numero di pagina nel mezzo | Trova e rimuovi il numero di pagina, unisci le righe | | Chunk con testo artefatto (URL, watermark) | Artefatto non rimosso allo step 4 | Elimina la sezione in `clean.md` | | Chunk con frase enorme non spezzabile | Singolo paragrafo >MAX_CHARS senza frasi intermedie | Spezza manualmente il paragrafo su frasi logiche | Dopo ogni correzione in `clean.md` riesegui dall'inizio dello step 5: ```bash python step-5/chunker.py --stem --force rm -f step-6//chunks.json # forza la rilettura da step-5 python step-6/verify_chunks.py --stem ``` **Cosa verifica `verify_chunks.py`:** - 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` | **Tabella diagnosi — problemi non risolvibili con fix_chunks:** | Sintomo | Causa probabile | Soluzione | |---|---|---| | Molti chunk corti dopo il fix | `MIN_CHARS` troppo alto o testo frammentato nel MD | Abbassa `MIN_CHARS` o correggi step 4 | | Chunk spezzato creato dal fix stesso | Frase singola > `MAX_CHARS` non spezzabile | Spezza manualmente il paragrafo in step 4 | | Chunk che finisce a metà frase non risolvibile | Salto di pagina PDF non sanato nel MD | Correggi la riga spezzata in `clean.md` | **Output se tutto ok:** ``` Totale chunk: 301 ✅ OK: 301 Distribuzione lunghezze: Min: 187 char Max: 923 char Media: 401 char ✅ 1/1 documenti senza problemi ``` --- ### Step 7 — Installazione ambiente **Tipo:** manuale (una volta sola) **Input:** nessuno **Output:** ambiente locale funzionante **Script:** `step-7/check_env.py` Installa Ollama, scarica i modelli e verifica l'ambiente. Si esegue una volta sola. Vedi [`step-7/README.md`](step-7/README.md) per istruzioni dettagliate e scelta dei modelli. ```bash python step-7/check_env.py ``` --- ### Step 8 — 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à. Senza `--force`, se la collection è presente lo step viene saltato | **Quando usare `--force`:** Se hai modificato i chunk (es. hai rieseguito step-6 dopo correzioni), la collection in ChromaDB contiene ancora i vecchi vettori. `--force` la cancella e la ricrea da zero con i chunk aggiornati. **Cosa succede per ogni chunk:** ``` testo del chunk │ ▼ Ollama (nomic-embed-text) vettore di 768 numeri [0.23, -0.41, 0.87, 0.12, ...] │ ▼ ChromaDB salva: testo + vettore + metadati (sezione, titolo, sub_index) ``` **Perché 768 numeri:** Ogni numero rappresenta una dimensione semantica. Testi con significato simile producono vettori simili — i loro numeri sono vicini nello spazio a 768 dimensioni. Questo è ciò che permette il retrieval semantico. **Output durante l'esecuzione:** ``` ✅ Ollama OK — nomic-embed-text disponibile 📦 872 chunk da ingestire [ 1/872] ✓ sezione_1__sotto_1__s0 ETA: 870s [ 2/872] ✓ sezione_1__sotto_2__s0 ETA: 867s ... [872/872] ✓ sezione_9__sotto_42__s0 ETA: 0s ✅ Ingestione completata in 718s — 872/872 chunk salvati Collection 'nietzsche' in chroma_db/ ``` `chroma_db/` contiene ora tutti i vettori su disco. Non è necessario ripetere questo step a meno che il documento cambi. --- ### Step 9 — Pipeline RAG **Tipo:** interattivo **Input:** `chroma_db/` + domanda dell'utente **Output:** risposta basata sul documento **Script:** `step-9/rag.py` ```bash source .venv/bin/activate python step-9/rag.py --stem ``` Loop interattivo che risponde a domande sul documento. Configura i parametri in `step-9/config.py` prima di avviare. Vedi [`step-9/README.md`](step-9/README.md) per la configurazione completa. --- ### Step 10 — Test automatici **Tipo:** automatico **Input:** sistema completo **Output:** tutti i test verdi **Script:** `step-10/test_pipeline.py` *(da implementare)* ```bash python step-10/test_pipeline.py --stem ``` Verifica ogni componente in isolamento e poi nel sistema completo. I test non dipendono dal contenuto del documento — usano dati fittizi creati e distrutti in memoria. **Struttura dei test:** ``` Test unitari — ogni componente isolato ✓ split_sentences non spezza le frasi ✓ parse_markdown rileva la struttura corretta ✓ chunk_sezione rispetta i boundary ✓ il prefisso è sempre presente in ogni chunk Test integrazione — i componenti parlano tra loro ✓ Ollama è raggiungibile ✓ i modelli sono disponibili ✓ l'embedding produce 768 dimensioni ✓ testi diversi producono vettori diversi ✓ ChromaDB scrive e legge correttamente Test qualità — il sistema si comporta bene ✓ il retrieval trova il chunk pertinente ✓ il retrieval non trova il chunk non pertinente ✓ il LLM usa il contesto fornito ✓ il LLM ammette quando la risposta non è nel contesto ``` --- ## Principi di progettazione **Atomico** Ogni step 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 step ha un criterio di completamento oggettivo. Non si passa allo step successivo finché il precedente non è verificato. **Reversibile** Puoi tornare indietro senza perdere il lavoro degli altri step. Cambi il MD? Riesegui solo step 5 e 8. Cambi i parametri del chunker? Riesegui solo step 5 e 8. 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.