Legger · Documentazione
Metodologia e validazione
Il processo, le misure e i loro limiti
Abstract
Legger è stato costruito per fasi, ognuna chiusa da una verifica misurabile prima di procedere. Questa pagina documenta il processo con i numeri reali: l’analisi del corpus (287.912 file in 23 collezioni, con i difetti trovati e gestiti), il set di 30 domande di valutazione, il benchmark dei modelli di embedding — il migliore raggiunge il 96,7% di recall@10 — la decisione, misurata e non supposta, di escludere il reranking, la validazione end-to-end su 14 conversazioni (14/14 superate, incluse le domande-trappola) e i costi effettivi. Chiude con i limiti della validazione stessa: 30 domande sono un campione, non una dimostrazione. Per la descrizione del sistema, vedi Come funziona Legger.
§ 1
Approccio
Lo sviluppo è proceduto per fasi, ciascuna con un cancello go/no-go: prima di costruire una parte del prodotto si fissa un criterio quantitativo di accettazione — per la qualità della ricerca, un recall@10 di almeno l’85% (la metrica è spiegata nella sezione 4) — e lo si misura su un prototipo. Se la misura è sotto soglia non si procede: si itera sul punto debole o si cambia strada. È il contrario del costruire tutto e sperare che funzioni.
Per il codice deterministico — il parser dei file, il chunker, il riconoscimento degli estremi normativi, il guardrail delle citazioni — si è lavorato in modalità test-driven: prima si scrive un test automatico che definisce il comportamento atteso (e che inizialmente fallisce), poi il codice che lo soddisfa. I casi patologici scoperti nell’analisi del corpus sono diventati test permanenti: ogni modifica futura li riesegue.
§ 2
Analisi del corpus
Prima riga di qualunque pipeline: conoscere i propri dati. Il corpus conta 287.912 file di testo in 23 collezioni, per 73,3 GB nominali su disco. L’analisi sistematica (campioni di 500 file per collezione, più la collezione Codici al completo) ha prodotto una serie di scoperte che hanno plasmato il parser.
Due formati coesistono. La quasi totalità dei file è Markdown con intestazioni setext (titoli «sottolineati» con righe di = e -, anziché preceduti da #), ma alcuni file — tra cui, ironicamente, il Codice civile — sono in realtà HTML nel formato Akoma Ntoso codificato in base64 (una codifica che traveste dati arbitrari da testo semplice): è servito un decodificatore dedicato.
La dimensione su disco mente. Circa 71 dei 73,3 GB sono byte nulli (NUL) accodati ai file della collezione Regi decreti — un difetto del generatore del corpus: atti brevi gonfiati fino a ~1 MiB di vuoto. Il parser tronca al primo byte nullo.
Tre stili di marcatura degli articoli. Gli atti recenti marcano gli articoli come intestazioni ### Art. N, quelli intermedi come titoli sottolineati, e gli atti storici multivigenti (codice penale, codice di procedura civile) usano una riga piatta del tipo «Codice Penale-art. 3 bis» che non è un’intestazione affatto. Il parser li supporta tutti e tre.
Il nome del file non identifica l’atto. 95.492 nomi di file compaiono in due o più collezioni, ma sono in gran parte atti diversiche condividono il titolo; i duplicati plausibili (stesso nome e stessa dimensione) sono circa 6.303. Conseguenza: l’identità di un atto si ricava dal contenuto (la prima intestazione, con tipo, data e numero), mai dal nome del file. Un’appendice dell’analisi ha inoltre rilevato che su un filesystem che ignora maiuscole e minuscole (il default di macOS) 353 nomi collidono tra loro e ~176 atti risulterebbero «oscurati» (~0,1% del corpus): da qui il requisito di deployment che il corpus viva su un filesystem case-sensitive.
Il bootstrap dell’indice, eseguito su queste fondamenta, ha processato 181.870 atti (saltando 106.042 duplicati tra collezioni) e prodotto 966.822 chunk — zero errori di parsingsull’intero corpus.
§ 3
Il set di valutazione
La qualità della ricerca non si giudica a sensazione: si misura su un set di domande con risposta attesa nota. Il set conta 30 domande, ciascuna con il suo bersaglio — l’articolo che una risposta corretta deve recuperare — in quattro categorie:
- esplicite (10): estremi normativi precisi, come «art. 2051 c.c.» o «articolo 186 del codice della strada»;
- naturali(12): linguaggio giuridico senza estremi, come «entro quanto tempo si prescrive la richiesta di risarcimento per un fatto illecito?» (bersaglio: art. 2947 c.c.) o «il datore di lavoro risponde dei danni causati a terzi dal proprio dipendente?» — il cui bersaglio, l’art. 2049 c.c., parla di «padroni e committenti» con lessico ottocentesco;
- cittadino (5): linguaggio comune, come «il cane del vicino mi ha morso, chi mi paga i danni?» (bersaglio: art. 2052 c.c.);
- trappole(3): domande costruite per indurre l’errore. «Provvedimenti d’urgenza art. 770 c.p.c.»: l’art. 770 esiste ma riguarda altro — i provvedimenti d’urgenza sono all’art. 700, e il sistema deve correggere il numero, non assecondarlo. «Ingiuria art. 594 codice penale»: l’articolo è stato abrogato nel 2016 — la risposta corretta riferisce l’abrogazione, non il testo previgente.
§ 4
Il benchmark degli embedding
Due metriche, da capire prima della tabella. Recall@k è la percentuale di domande per cui il passaggio-bersaglio compare tra i primi k risultati della ricerca: recall@10 = 96,7% significa che in 29 domande su 30 il testo giusto era tra i primi dieci passaggi consegnati al modello. MRR (Mean Reciprocal Rank) è la media del reciproco della posizione del bersaglio: 1 se compare sempre primo, 0,5 se in media secondo, 0,1 se decimo — misura quindi quanto in alto arriva il risultato giusto, non solo se arriva.
Condizioni di prova: ricerca ibrida (semantica + lessicale, fusione RRF) con k = 10, sulla collezione Codici — 40 atti, 18.463 chunk, la più interrogata e la più ostica strutturalmente — con le 30 domande della sezione 3. Quattro modelli di embedding candidati; le ultime quattro colonne riportano il recall@10 per categoria di domanda.
| Modello | recall@5 | recall@10 | MRR | esplicite | naturali | cittadino | trappole |
|---|---|---|---|---|---|---|---|
| voyage-4-large | 93,3% | 96,7% | 0,717 | 90% | 100% | 100% | 100% |
| voyage-4 | 83,3% | 86,7% | 0,611 | 60% | 100% | 100% | 100% |
| voyage-law-2 | 70,0% | 76,7% | 0,485 | 50% | 91,7% | 80% | 100% |
| bge-m3 | n/d | n/d | n/d | — | — | — | — |
Quattro osservazioni. Primo: voyage-law-2, il modello specializzato in testi legalidella generazione precedente, perde nettamente dai modelli generalisti più recenti — la specializzazione dichiarata non sostituisce la misura. Secondo: bge-m3, il candidato auto-ospitato, non è stato valutato perché l’inferenza si è rivelata inaffidabile sulla macchina di sviluppo (un Mac Intel): escluso per impraticabilità, non per demerito. Terzo: l’intero divario tra i due voyage-4 è concentrato nelle domande con estremi espliciti — esattamente il caso che il fast path deterministico risolve senza ricerca semantica; sulle domande semantiche (naturali, cittadino, trappole) entrambi fanno il 100%. Quarto: la misura non è perfettamente deterministica — l’indice vettoriale usa HNSW, una struttura di ricerca approssimata, e una domanda oscilla al confine del top-10 tra un’esecuzione e l’altra: il 96,7% di voyage-4-large è in realtà un intervallo 96,7–100%.
Il cancello (recall@10 ≥ 85%) è stato superato da due modelli: go. In produzione l’indice usa voyage-4-large.
§ 5
La decisione sul reranking
Un reranker è un secondo modello che rilegge le coppie domanda–documento restituite dalla ricerca e le riordina: esamina i due testi insieme, quindi in teoria giudica la pertinenza meglio della sola vicinanza tra vettori — al prezzo di rileggere ogni coppia, una per una. Molte pipeline lo includono per default. Invece di presumere il beneficio, lo abbiamo misurato sulle stesse 30 domande, sopra la ricerca ibrida:
- recall@10: da 96,7% a 96,7% — +0,0 punti;
- recall@5: da 93,3% a 83,3% — una regressione;
- MRR: da 0,734 a 0,748 — un guadagno marginale;
- latenza: da 0,53 a 132,77 secondi per domanda su CPU — circa 250 volte più lento.
Nessun guadagno dove conta, una regressione su recall@5 e un costo di latenza inaccettabile: il reranking è disattivato di default (resta dietro un interruttore di configurazione, rivalutabile su altro hardware). È il secondo episodio della stessa lezione del benchmark: misurare batte supporre.
§ 6
Validazione end-to-end
Le metriche di ricerca non bastano: serve verificare che il sistema completo — recupero, generazione, citazioni — si comporti bene su conversazioni reali. La validazione manuale ha eseguito 14 turni di conversazione sulla pipeline reale (incluse due conversazioni a più turni e una domanda fuori corpus), trascritti integralmente. Per ogni turno si è verificato: che il retrieval contenesse il bersaglio, che la risposta fosse ancorata ai passaggi, che i marker di citazione fossero ben formati e tutti riconducibili ai chunk recuperati, e che i rifiuti fossero corretti.
Esito: 14/14 superati. Zero marker malformati, zero marker «orfani» (citazioni di testi mai recuperati), zero estremi inventati. Le trappole sono state gestite: sull’art. 594 c.p. il sistema ha riferito l’abrogazione (d.lgs. 7/2016) senza ricostruire il testo previgente; su «art. 770 c.p.c.» ha corretto esplicitamente il numero in 700, segnalando l’equivoco. Sulla domanda fuori corpus ha rifiutato di rispondere, citando solo il rinvio realmente presente nei passaggi e suggerendo come riformulare.
La validazione ha anche documentato un limite, poi corretto: i follow-up conversazionali («e il comma successivo?») facevano ricerca sul testo letterale del messaggio, recuperando rumore — il sistema degradava con onestà, dichiarando il contesto insufficiente invece di inventare. La riscrittura della domanda con il contesto della conversazione, oggi parte della pipeline, è la risposta a quel limite. In produzione, il controllo che in validazione era manuale è automatico e permanente: il guardrail verifica ogni marker di ogni risposta contro i passaggi recuperati, in tempo reale.
§ 7
Costi e infrastruttura
Ordine di grandezza, in trasparenza. L’indicizzazione iniziale dell’intero corpus — l’embedding di ~524 milioni di token, di cui 200 milioni in fascia gratuita — è costata circa 39 dollari, una tantum, con voyage-4-large (le alternative stimate: ~19,5 $ con voyage-4, ~6,5 $ con voyage-4-lite). L’esercizio gira su un singolo VPS europeo (ordine dei 30–50 € al mese) più i costi variabili delle API di generazione; il budget complessivo della fase beta è sotto i 150 € al mese. Non è un dettaglio contabile: costi bassi permettono di tenere il prodotto accessibile senza compromettere la qualità della pipeline.
§ 8
Limiti della validazione
Le misure vanno lette per quello che sono. 30 domande sono un campione: con questa numerosità un punto percentuale vale un terzo di domanda, e piccole differenze tra modelli non sono statisticamente significative — il set è destinato a crescere. Il benchmark copre la sola collezione Codici(40 atti, 18.463 chunk), non l’intero corpus indicizzato: è la collezione più interrogata e la più difficile strutturalmente, ma resta una parte del tutto. Il giudizio di ancoraggio è stato manuale sulle 14 conversazioni: una valutazione automatica e continua della qualità delle risposte (LLM-as-judge: un secondo modello che giudica le risposte del primo contro le fonti) è in roadmap, non in produzione. Infine, come già notato, l’indice approssimato introduce una piccola variabilità tra esecuzioni, dichiarata accanto a ogni numero che ne è toccato.
§ 9
Riproducibilità
Tutto ciò che questa pagina afferma è ricontrollabile. Il corpus è pubblico (il progetto italia-corpus, su GitHub), con l’intera storia git; l’analisi del corpus è rigenerabile con lo script che l’ha prodotta e riporta il commit esatto su cui è stata eseguita. I report integrali dei benchmark — con il dettaglio per singola domanda — sono file JSON conservati nel repository del progetto (backend/eval/results/), e il set di valutazione è rieseguibile con un comando a ogni modifica della pipeline: i numeri di domani si confronteranno con quelli di oggi, sulla stessa bilancia. La trascrizione completa della validazione end-to-end, con tutte le risposte e i controlli, è anch’essa nel repository.