Dal modello architetturale al prototipo: costruire un Hybrid Retriever legale in Python
Capita di continuo: si cerca un’informazione in un archivio aziendale digitando “penale per ritardo”, ma il motore restituisce zero risultati. Il motivo? Il contratto utilizzava l’espressione “sanzione per inadempimento temporale”.
Quando applichiamo i Large Language Models (LLM) a settori dove la precisione delle parole è tutto — come quello legale — questo problema non fa che amplificarsi. Un LLM, se lasciato a sé stesso, non conosce i nostri contratti. Può scrivere una risposta grammaticalmente ineccepibile, ma basata sul nulla, generando quelle che chiamiamo allucinazioni.
L’architettura Retrieval-Augmented Generation (RAG) nasce per questo. Separa il recupero delle informazioni (retrieval) dalla loro elaborazione (generation). Il modello linguistico riceve il documento pertinente e lo usa come base fattuale per rispondere. Ma l’efficacia dell’intero sistema si regge su un unico, delicato pilastro: il Retriever. Come facciamo a essere sicuri di pescare il documento giusto?
Il problema del Retriever: lessico o semantica?
I sistemi tradizionali si basano sulla corrispondenza esatta delle parole. Un motore statistico come BM25 è chirurgico nel trovare termini specifici, numeri di protocollo o riferimenti normativi. Se cerco l’articolo “1326 c.c.”, BM25 lo trova all’istante. Ma è cieco di fronte ai sinonimi.
Al contrario, gli embedding semantici trasformano i testi in vettori numerici. In questo spazio matematico, frasi con parole diverse ma significato simile collassano nella stessa area. Se cerco “obblighi sulla continuità operativa”, il sistema a embedding mi restituirà “Il fornitore garantisce il rispetto dei livelli di servizio”, capendo l’intento. Tuttavia, i modelli semantici faticano terribilmente a gestire la ricerca di codici fiscali, acronimi o termini tecnici isolati.
Entrambi gli approcci, presi singolarmente, hanno dei limiti evidenti.
L’architettura Hybrid Search
La ricerca ibrida risolve il dilemma non scegliendo, ma fondendo i due segnali:
[math]\text{Retrieval finale} = \text{segnale lessicale} + \text{segnale semantico}[/math]
Un sistema Hybrid Search combina la precisione di BM25 con la fluidità linguistica degli embedding. Se la clausola contiene la parola esatta, BM25 fa il lavoro pesante; se il concetto è espresso con una perifrasi, intervengono i vettori semantici.
Dal concetto al codice: costruire un Hybrid Retriever in Python
Costruiamo un prototipo funzionante utilizzando un corpus sintetico di 20 clausole contrattuali. L’obiettivo è isolare e comprendere il principio ingegneristico alla base della ricerca ibrida, calcolando la similarità tramite prodotto scalare tra vettori normalizzati e unendo i risultati con l’algoritmo Reciprocal Rank Fusion (RRF).
1. Installazione librerie
!pip install rank_bm25 sentence-transformers numpy pandas
2. Creazione del corpus sintetico
documents = [
"Il contratto può essere risolto anticipatamente in caso di grave inadempimento della controparte.",
"Il pagamento delle fatture deve essere effettuato entro trenta giorni dalla data di emissione.",
"La parte fornitrice garantisce la continuità del servizio secondo i livelli SLA concordati.",
"Il cliente può richiedere la cancellazione dei dati personali secondo quanto previsto dal GDPR.",
"Il trattamento dei dati personali deve avvenire esclusivamente per le finalità indicate nel presente accordo.",
"La proprietà intellettuale del software sviluppato rimane di titolarità del fornitore.",
"Il fornitore deve mantenere riservate tutte le informazioni ricevute durante il rapporto contrattuale.",
"In caso di controversia le parti tenteranno una procedura di mediazione prima del ricorso giudiziario.",
"Il contratto ha durata annuale con rinnovo automatico salvo disdetta scritta.",
"La società cliente può effettuare audit periodici sulle procedure di sicurezza del fornitore.",
"Il fornitore garantisce backup periodici dei dati archiviati sulla piattaforma.",
"Eventuali modifiche contrattuali devono essere approvate mediante accordo scritto tra le parti.",
"La responsabilità del fornitore è limitata ai danni direttamente imputabili al servizio.",
"Il cliente deve utilizzare il software nel rispetto dei termini della licenza concessa.",
"Il fornitore può sospendere il servizio in caso di mancato pagamento.",
"Le informazioni confidenziali non possono essere divulgate a soggetti terzi.",
"Il trasferimento dei dati verso paesi extra UE deve rispettare le normative applicabili.",
"La sicurezza informatica del sistema deve essere verificata attraverso controlli periodici.",
"Il contratto disciplina obblighi e responsabilità delle parti coinvolte.",
"Il recesso anticipato richiede un preavviso minimo di sessanta giorni."
]
3. Retriever lessicale (BM25)
from rank_bm25 import BM25Okapi
tokenized_docs = [doc.lower().split() for doc in documents]
bm25 = BM25Okapi(tokenized_docs)
def bm25_search(query, k=5):
tokens = query.lower().split()
scores = bm25.get_scores(tokens)
ranking = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
return ranking[:k]
Nota per la messa in produzione (Text Preprocessing)
L’utilizzo di .split() è un’approssimazione utile solo a scopo didattico. In un ambiente di produzione legale, l’indicizzazione lessicale richiede una pipeline NLP strutturata.
Questo include:
-
Tokenizer linguistici avanzati: per gestire correttamente apostrofi, punteggiatura e caratteri speciali.
-
Rimozione delle stop-word: eliminazione di articoli e congiunzioni che inquinerebbero le frequenze di BM25.
-
Stemming/Lemmatizzazione: ricondurre verbi e plurali alla radice (es. “risolto”, “risoluzione”, “risolvere”).
-
Normalizzazione di acronimi ed entità giuridiche: nel dominio legale, concetti come “GDPR”, “Regolamento UE 2016/679”, “protezione dati” e “privacy” devono spesso essere ricondotti a un token canonico comune prima di passare l’indice a BM25, o gestiti tramite tecniche di query expansion.
4. Retriever semantico con embedding
Nota tecnica:
Una caratteristica importante della famiglia E5 è l’utilizzo di due prefissi differenti:
passage:per i documenti indicizzatiquery:per le interrogazioniQuesta distinzione permette al modello di apprendere la relazione corretta tra domanda e documento.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer(
"intfloat/multilingual-e5-small"
)
# Embedding dei documenti
formatted_docs = [
"passage: " + doc
for doc in documents
]
embeddings = model.encode(
formatted_docs,
batch_size=32,
normalize_embeddings=True,
show_progress_bar=True
)
def semantic_search(query, k=5):
query_embedding = model.encode(
"query: " + query,
normalize_embeddings=True
)
# Per vettori normalizzati:
# cosine similarity = prodotto scalare
scores = np.dot(
embeddings,
query_embedding
)
ranking = np.argsort(scores)[::-1]
return [
(idx, scores[idx])
for idx in ranking[:k]
]
Nei sistemi reali con milioni di token, il batching permette di controllare il consumo di memoria e ottimizzare il throughput dell’elaborazione, evitando che l’intero corpus venga caricato simultaneamente in memoria
Perché calcoliamo la Cosine Similarity con un semplice prodotto scalare?
Se osserviamo il codice, per calcolare la similarità non abbiamo importato funzioni complesse, ma abbiamo usato un semplice prodotto scalare (np.dot). Perché?
La formula geometrica classica della Cosine Similarity è:
[math]\displaystyle \cos(\theta) = \frac{\mathbf{x} \cdot \mathbf{y}}{\|\mathbf{x}\| \|\mathbf{y}\|}[/math]
Tuttavia, in fase di generazione degli embedding, abbiamo passato il parametro normalize_embeddings=True. Questo comando applica una normalizzazione L2, forzando la norma (ovvero la lunghezza) di tutti i vettori a [math]1[/math].
Di conseguenza, avendo [math]\|\mathbf{x}\| = \|\mathbf{y}\| = 1[/math], il denominatore della frazione scompare e l’equazione si riduce a:
[math]\cos(\theta) = \mathbf{x} \cdot \mathbf{y}[/math]
Questa non è solo un’eleganza matematica, ma una drastica ottimizzazione computazionale. Eseguire un prodotto scalare nudo e crudo richiede molte meno operazioni CPU/GPU rispetto al calcolo delle norme e alla successiva divisione, un dettaglio che diventa vitale quando il nostro Vector Database deve confrontare milioni di documenti in pochi millisecondi.
5. La fusione dei risultati: Reciprocal Rank Fusion (RRF) e pesatura dei segnali
Abbiamo ora due sistemi di ricerca indipendenti:
- BM25, che valuta la corrispondenza lessicale tra query e documento;
- Embedding Retriever, che valuta la vicinanza semantica nello spazio vettoriale.
Il problema è che i due algoritmi producono punteggi non direttamente confrontabili. BM25 restituisce uno score basato sulla frequenza e distribuzione dei termini (che non ha un limite superiore fisso), mentre gli embedding producono una similarità coseno compresa generalmente tra [math]-1[/math] e [math]1[/math]. Sommare direttamente questi valori porterebbe a un disastro matematico, schiacciando il segnale semantico sotto i numeri molto più grandi di BM25.
Per questo motivo utilizziamo una tecnica basata sul ranking posizionale e non sul valore assoluto dello score: la Reciprocal Rank Fusion (RRF).
L’idea è semplice ed elegante: un documento riceve un punteggio maggiore se compare nelle prime posizioni di più classifiche indipendenti. La formula originale è:
[math]\displaystyle \text{RRF}(d) = \sum_i \frac{1}{k + r_i(d)}[/math]
dove:
- [math]d[/math] è il documento candidato;
- [math]r_i(d)[/math] è la posizione occupata dal documento nella classifica del retriever [math]i[/math];
- [math]k[/math] è un parametro di smoothing, generalmente impostato a [math]60[/math].
Il parametro [math]k[/math] funge da ammortizzatore: riduce l’effetto dei primissimi risultati, evitando che chi domina una singola classifica oscuri completamente gli altri segnali.
RRF Pesata: adattare la ricerca al dominio legale
La RRF standard tratta tutti i retriever come equivalenti. Questa ipotesi è ragionevole in scenari generalisti, ma diventa un limite in domini specialistici. In un contratto, una parola esatta ha spesso un’importanza giurisprudenziale superiore rispetto a una vaga similarità concettuale.
Se cerchiamo “articolo 1326 c.c.”, “Regolamento UE 2016/679” o “clausola 14.2”, la corrispondenza lessicale rappresenta un segnale inequivocabile.
Per questo motivo nei sistemi reali si utilizza una versione pesata della RRF:
[math]\displaystyle \text{RRF}(d) = \sum_i \frac{w_i}{k + r_i(d)}[/math]
dove [math]w_i[/math] rappresenta l’importanza (peso) assegnata allo specifico motore. Nel dominio legale, è prassi comune impostare inizialmente:
[math]w_{\text{BM25}} > w_{\text{semantic}}[/math]
Questo garantisce che termini tecnici, riferimenti normativi e identificativi di pratica abbiano la priorità.
Implementazione in Python
def hybrid_search(query, k=5, w_bm25=1.2, w_semantic=1.0):
bm25_results = bm25_search(query)
semantic_results = semantic_search(query)
scores = {}
# Fusione del ranking BM25 con peso maggiore
for rank, (idx, _) in enumerate(bm25_results):
scores[idx] = scores.get(idx, 0) + w_bm25 / (60 + rank)
# Fusione del ranking semantico
for rank, (idx, _) in enumerate(semantic_results):
scores[idx] = scores.get(idx, 0) + w_semantic / (60 + rank)
# Ordinamento finale basato sullo score RRF combinato
final_ranking = sorted(
scores.items(),
key=lambda x: x[1],
reverse=True
)
return final_ranking[:k]
In questo modo il sistema non si obbliga a scegliere tra ricerca lessicale e ricerca semantica, ma modula dinamicamente il contributo di entrambe.
Perché “embedding = sempre migliore” è un falso mito
La ricerca semantica rappresenta una rivoluzione perché permette di superare la rigidità delle parole chiave. Un modello di embedding capisce istantaneamente che “sanzione per inadempimento temporale” e “penale per ritardo” esprimono un concetto pressoché identico.
Tuttavia, nel settore legale la comprensione del significato non sostituisce la precisione terminologica. Un modello semantico da solo si disorienta facilmente quando deve distinguere:
- Due articoli di legge diversi ma contestualmente simili;
- Due versioni successive e revisionate dello stesso contratto;
- Clausole con numerazione specifica o codici di riferimento.
Per questo motivo, un Retriever legale robusto sa che abbandonare il vecchio per il nuovo è un errore: il segreto è combinare l’astrazione dell’intelligenza artificiale con l’infallibile precisione di un database tradizionale.
6. Test del sistema
query = "Quali sono gli obblighi relativi alla protezione dei dati personali?"
results = hybrid_search(query)
for idx, score in results:
print(f"\nScore RRF: {round(score, 4)}")
print(documents[idx])
Confronto architetturale
| Metodo | Punto forte | Limite |
|---|---|---|
| BM25 (Lessicale) | Infallibile su termini tecnici, nomi e normative | Non comprende sinonimi e variazioni sintattiche |
| Embedding (Semantico) | Comprende il significato e l’intento della query | Può ignorare dettagli numerici o codici specifici |
| Hybrid Search | Massimizza la precisione fondendo i due approcci | Richiede tuning del peso (RRF) o di algoritmi alpha |
Come misurare la qualità del Retriever: metriche e validazione
Costruire un’architettura ibrida è solo il primo passo. Un sistema di retrieval, per quanto sofisticato, è un’ipotesi algoritmica che deve essere validata. Senza un Gold Standard (un dataset di test con domande e risposte corrette già note), non possiamo sapere se le modifiche al codice stanno effettivamente migliorando le performance o solo introducendo rumore.
Le metriche fondamentali
Per quantificare l’efficacia del Retriever, utilizziamo quattro indicatori chiave che analizzano la precisione e la posizione dei risultati:
| Metrica | Significato | Focus |
|---|---|---|
| [math]\text{Recall@}k[/math] | Il documento corretto è presente tra i primi [math]k[/math] risultati? | Copertura: stiamo trovando l’informazione? |
| [math]\text{Precision@}k[/math] | Quanti dei primi [math]k[/math] risultati sono realmente pertinenti? | Rumore: stiamo inquinando il contesto? |
| MRR (Mean Reciprocal Rank) |
Media del reciproco della posizione del primo risultato corretto. | Ranking: quanto in alto è il “primo” risultato utile? |
| nDCG (Normalized Discounted Cumulative Gain) |
Misura la qualità del ranking pesando i risultati in base alla loro rilevanza. | Qualità: i risultati più rilevanti sono in cima? |
Analisi comparativa: BM25 vs. Embedding vs. Hybrid
Immaginiamo di testare il sistema con una query ambigua e complessa, come: “penale per ritardo pagamento”. Il documento target che il sistema deve recuperare è la Clausola 14: “sanzione per inadempimento temporale”.
Si noti come, in questo scenario, BM25 fatichi a trovare una corrispondenza esatta (assenza delle keyword “penale” o “pagamento” nel testo di destinazione), mentre la ricerca semantica fatichi a dare priorità al documento corretto rispetto a clausole generiche sull’inadempimento.
Ecco i risultati ottenuti su un set di test di 100 query legali:
| Metodo | [math]\text{Recall@}5[/math] | [math]\text{Precision@}5[/math] | MRR |
|---|---|---|---|
| BM25 | 40% | 0.20 | 0.25 |
| Embedding | 75% | 0.45 | 0.50 |
| Hybrid Search | 95% | 0.75 | 0.82 |
Valori esemplificativi utilizzati a fini didattici.
Perché questo esercizio è critico?
L’analisi mostra che l’unione dei due segnali (Hybrid Search) non è una somma aritmetica, ma un incremento non lineare della performance.
- L’Hybrid Search “salva” la Recall: Laddove il modello di embedding fallisce (magari perché il documento contiene un acronimo tecnico molto specifico), BM25 interviene recuperando il record corretto.
- L’Hybrid Search “pulisce” la Precision: Laddove BM25 restituirebbe decine di documenti contenenti le parole chiave ma irrilevanti per il contesto, l’embedding filtra i risultati mantenendo solo quelli semanticamente affini.
In produzione, questa validazione deve essere automatizzata. Framework come RAGAS o TruLens permettono di eseguire questi test su ogni nuovo commit, assicurando che l’introduzione di un nuovo modello di embedding o la modifica dei pesi di RRF non degradino la qualità del sistema. Se non misuri il Retriever, non stai costruendo un prodotto, stai solo facendo esperimenti.
Dal prototipo all’architettura Enterprise
Il codice che abbiamo scritto rappresenta il “motore matematico” nudo e crudo di un Retriever moderno. In un ambiente di produzione reale, tuttavia, il processo non cambia nella logica, ma scala strutturalmente per gestire milioni di documenti e latenze nell’ordine dei millisecondi.
Per trasformare questo prototipo in un sistema Enterprise, l’architettura deve essere disaccoppiata e stratificata come segue:
[ Documenti Legali Grezzi ]
│
▼
[ Chunking Semantico ]
(Suddivisione in paragrafi)
│
┌───────────────────────┴───────────────────────┐
▼ ▼
[ BM25 Index ] [ Embedding Index ]
(Elasticsearch / OpenSearch) (Vector DB: Qdrant, Milvus, pgvector)
│ │
└───────────────────────┬───────────────────────┘
▼
[ HYBRID RETRIEVER ]
(Reciprocal Rank Fusion - Top 100)
│
▼
[ CROSS-ENCODER ]
(Reranking ad alta precisione - Top 5)
│
▼
[ Generazione ]
(LLM con prompt di grounding)
│
▼
[ Risposta con Citazioni ]
I componenti della pipeline reale
Analizziamo i tre colli di bottiglia fondamentali che un’infrastruttura di questo tipo deve risolvere rispetto al nostro prototipo:
1. Il Chunking Semantico
In produzione non passiamo mai un intero contratto di 50 pagine al modello di embedding. Il documento viene diviso in chunk (blocchi) di dimensioni fisse, applicando un overlap (sovrapposizione) per non spezzare concetti a metà. Nel dominio legale, il chunking deve rispettare la struttura gerarchica del testo: dividere per “Articolo” o “Clausola” è immensamente superiore a una divisione cieca per numero di caratteri.
2. I Database di Livello Storage
Le liste e gli array in memoria RAM esploderebbero in pochi secondi.
- L’indice BM25 viene affidato a motori di ricerca full-text consolidati (Elasticsearch, OpenSearch).
- L’indice Semantico viene delegato a un Vector Database dedicato (come Qdrant, Pinecone, o estensioni SQL come pgvector), ottimizzato per eseguire calcoli di similarità spaziale (HNSW) su milioni di vettori in pochi millisecondi.
3. Il Reranking con Cross-Encoder
Questa è la vera differenza tra un sistema mediocre e uno eccellente.
Il nostro retriever ibrido è velocissimo, ma fa un calcolo asimmetrico: confronta due vettori pre-calcolati. Un Cross-Encoder, invece, non produce vettori separati. Prende la query dell’utente, prende il documento, li unisce letteralmente in un’unica stringa e li fa passare insieme attraverso le reti neurali del modello (Self-Attention).
Il Cross-Encoder capisce le sfumature logiche con una precisione chirurgica, ma è computazionalmente pesantissimo. Ecco perché si usa la pipeline a imbuto mostrata nello schema: l’Hybrid Retriever estrae i migliori 100 documenti in una frazione di secondo, e il Cross-Encoder riordina solo quelli, estrapolando i 5 perfetti da passare all’LLM.
Solo a questo punto, con un contesto filtrato, validato e pesato matematicamente, chiediamo al Large Language Model di generare la risposta finale citando le fonti.
Il Retriever non è un semplice componente di supporto, ma il vero motore epistemico di un sistema RAG. L’accuratezza del modello linguistico dipende dalla qualità delle informazioni recuperate: il compito dell’LLM non è conoscere tutto, ma ragionare sui documenti corretti. In questa prospettiva, progettare un buon sistema di retrieval significa trasformare un modello linguistico generico in un assistente realmente affidabile per domini ad alta specializzazione come quello giuridico.
AI affidabile, RAG, LLM e le nuove professioni dell’Intelligenza Artificiale
Se vuoi comprendere come progettare sistemi di Intelligenza Artificiale affidabili, ridurre le allucinazioni degli LLM, costruire pipeline RAG professionali e scoprire le competenze richieste dalle nuove professioni dell’AI, ecco una selezione di articoli di approfondimento.
👉Legal Engineer: la professione che nasce dall’incontro tra diritto e algoritmi
👉Come creare un sistema RAG in Python: Hybrid Search, Reranking e valutazione quantitativa
👉Perché gli LLM allucinano? Plausibilità, verità e precisione assistita
👉Guida alla temperatura negli LLM: come controllare la creatività dell’IA
👉Cos’è la Burstiness e perché può rivelare un testo generato dall’IA
👉Paradosso dell’automazione e Legge di Jevons: perché l’IA crea più lavoro umano
👉Chi insegna ai chatbot? Il lavoro invisibile dietro l’Intelligenza Artificiale





