Reranking
Quando lavoriamo con un assistente AI che risponde a quesiti tecnici sopra una base documentale, il primo problema che incontriamo non è quasi mai “trovare qualcosa”. Il problema è che il sistema trova troppo, per questo si parla di Reranking.
Una ricerca semantica ben fatta restituisce quasi sempre venti, trenta frammenti che sembrano rilevanti. Quasi tutti contengono le parole giuste. Quasi tutti parlano dell’argomento giusto. Ma se li passiamo tutti al modello, accadono due cose: il contesto si riempie di rumore, e il modello finisce per dare lo stesso peso a una fonte decisiva e a una frase pescata per pura somiglianza lessicale.
È qui che entra in gioco un motore di reranking.
In questa guida spieghiamo cos’è, perché serve davvero e come costruirne uno che non si limiti a riordinare, ma che protegga il sistema da un difetto sottile e pericoloso: degradare in silenzio.
Il punto di partenza: perché il retrieval ha bisogno di un secondo passaggio
Un sistema RAG classico funziona così:
- l’utente fa una domanda;
- il sistema la trasforma in una query (spesso riformulata da un piccolo modello);
- un motore di ricerca vettoriale recupera i frammenti più simili;
- i frammenti finiscono nel prompt;
- il modello risponde.
Il passaggio 3, di solito, è già buono. I motori moderni supportano il retrieval ibrido: combinano una ricerca dense (basata su embedding, capisce il significato) con una ricerca sparse tipo BM25 (basata sulle parole, capisce la corrispondenza esatta di termini e codici). I due risultati vengono fusi con un algoritmo come RRF, Reciprocal Rank Fusion, che bilancia i due ranking.
In sintesi, dopo questa fase abbiamo una lista di candidati già discreta.
Il problema è che “discreta” non basta. Tra i candidati ci sono quasi sempre:
- una o due fonti davvero decisive;
- alcune fonti laterali, recuperate perché condividono parole chiave ma non rispondono al quesito;
- fonti OCR sporche o frammentarie, estratte da scansioni di bassa qualità;
- ripetizioni o duplicati semantici.
Senza un secondo filtro, il modello deve fare da solo tutto questo lavoro di selezione, dentro un contesto già carico. E lo fa male, perché le istruzioni “usa solo le fonti pertinenti” valgono quanto il prompt riesce a far rispettare.
Cos’è un motore di reranking
Un motore di reranking è un componente che sta tra il retrieval e la generazione. Prende la lista dei candidati, la rivaluta e la riordona in base a criteri più fini della semplice somiglianza.
La differenza con il retrieval è importante:
il retrieval risponde a “questo frammento è simile alla domanda?”; il reranking risponde a “questo frammento aiuta davvero a rispondere alla domanda?”.
La prima è una misura geometrica. La seconda è una misura di pertinenza, e richiede un ragionamento.
Un reranking ben fatto fa tre cose:
- punteggia ogni fonte con un voto (0–1, ad esempio);
- scarta le fonti non pertinenti (relevance filter);
- seleziona solo le migliori, in numero limitato.
Il risultato che arriva al modello è più corto, più pulito, più vero.
Come funziona in pratica: il reranking come LLM-as-judge
L’approccio più efficace, oggi, è usare un modello linguistico come giudice. Si costruisce un prompt che presenta la domanda, i criteri di valutazione e la lista dei candidati, e si chiede al modello di restituire un punteggio per ciascuno.
I criteri contano più del modello. Un buon set di criteri è pesato e dichiarato:
| Criterio | Peso | Cosa misura |
|---|---|---|
| Pertinenza semantica | 30% | Il frammento risponde davvero al quesito? |
| Coerenza alla procedura | 25% | È pertinente al tipo di atto o domanda? |
| Autorevolezza | 20% | La fonte è normativa, dottrinale, laterale? |
| Requisito del contesto | 15% | Copre un punto necessario al caso? |
| Qualità e recenza | 10% | È leggibile, completo, aggiornato? |
La somma pesata produce il punteggio finale. Poi si taglia: si tengono le prime N fonti e si scartano le altre.
Un piccolo esempio, in TypeScript, rende l’idea meglio delle parole:
type Candidate = {
index: number
text: string
source: string
section: string
score: number // score del retrieval
}
type Ranked = {
index: number
score: number // 0–1, nuovo punteggio di pertinenza
reason: string
}
async function rerankWithLLM(
candidates: Candidate[],
query: string,
criteria: string
): Promise<Ranked[]> {
const prompt = buildPrompt(candidates, query, criteria)
const text = await llm.generate(prompt, {
// fondamentale: temperatura bassa, serve stabilità
temperature: 0.1,
responseFormat: "json"
})
// Il modello restituisce un array: [{index, score, reason}, ...]
const parsed = safeJsonParse<Ranked[]>(text)
if (parsed === null) {
// non fidarsi: vedi sotto
return []
}
return parsed
.filter((r) => typeof r.score === "number")
.sort((a, b) => b.score - a.score)
.slice(0, 8) // teniamo solo le migliori 8
}
La parte importante non è il codice in sé. È che il reranking trasforma un problema geometrico (la somiglianza) in un problema di giudizio, e il giudizio può essere guidato da criteri dichiarati e modificabili.
Il punto critico: cosa succede quando il giudice sbaglia
Qui arriviamo alla parte più importante dell’articolo, e a un errore che si vede spesso nei sistemi reali.
Il reranking con un LLM dipende da una chiamata di rete. Il modello può:
- restituire JSON non valido;
- andare in timeout;
- usare un nome di modello sbagliato nella configurazione;
- rifiutare la richiesta per limiti di quota.
In tutti questi casi, la tentazione è implementare un fallback: se il LLM fallisce, si ordina in modo deterministico e si va avanti. In apparenza è ragionevole. In realtà è pericoloso.
Vediamo un caso concreto, tratto da un sistema reale che abbiamo analizzato. La pipeline di reranking richiamava un modello con un nome inesistente nella configurazione. Al primo JSON non valido, tutto il reranking cadeva su un “ordinamento deterministico” e il sistema continuava a rispondere. Nessun errore visibile. Nessun allarme.
Il risultato, silenzioso, era questo:
- le fonti normative selezionate scendevano da una scelta ragionata a un ordinamento per score grezzo;
- la qualità delle risposte peggiorava, ma non in modo catastrofico: abbastanza da non far scattare controlli, non abbastanza da andare bene;
- nessuno sapeva che il sistema stava lavorando in regime degradato.
Il bug non era nel modello. Era nel fatto che il fallback mentiva sulla qualità della selezione.
La regola, quindi, è semplice:
un fallback deterministico è accettabile. Un fallback silenzioso non lo è mai.
Costruire un fallback che non mente
Il rimedio è far sì che il sistema dichiari sempre in che regime sta lavorando. Il risultato del reranking non è solo una lista: è una lista più uno stato.
type RerankStatus =
| "ok" // LLM usato correttamente
| "llm_invalid_json" // JSON non valido, fallback deterministico
| "llm_error" // errore di rete, fallback deterministico
| "no_candidates" // niente da rerankare
type RerankResult = {
sources: Ranked[]
usedLLM: boolean
status: RerankStatus
message?: string // opzionale, per spiegare cosa è successo
}
Quando lo stato è ok, il sistema sa di poter fidarsi della selezione. Quando lo stato è llm_invalid_json o llm_error, il sistema sa che la selezione è degradata e può:
- segnalare il limite nella risposta all’utente;
- abbassare il punteggio di affidabilità complessivo;
- loggare l’evento per intervento umano.
In pratica, il fallback deterministico diventa qualcosa del genere:
function deterministicRerank(
candidates: Candidate[],
query: string
): Ranked[] {
const terms = new Set(
query.toLowerCase()
.split(/\W+/)
.filter((t) => t.length > 3)
)
return candidates
.map((c) => {
const text = c.text.toLowerCase()
const hits = [...terms].filter((t) => text.includes(t)).length
const keywordScore = terms.size === 0 ? 0 : hits / terms.size
// combina score del retrieval e match delle keyword
const score = c.score * 0.6 + keywordScore * 0.4
return {
index: candidates.indexOf(c),
score,
reason: `fallback deterministico (retrieval=${c.score.toFixed(3)}, keyword=${keywordScore.toFixed(3)})`
}
})
.sort((a, b) => b.score - a.score)
.slice(0, 8)
}
Notate il campo reason: dice esplicitamente come è stato assegnato il punteggio. Un fallback che spiega sé stesso è un fallback onesto.
Reranking come controllo qualità: relevance filter e OCR caution
Un buon motore di reranking non si ferma al punteggio. Fa due lavori che spesso vengono sottovalutati.
Relevance filter
Non tutto ciò che viene recuperato è utile. Il reranking dovrebbe scartare attivamente le fonti che contengono solo parole chiave comuni ma non trattano il punto richiesto. Se una fonte parla di un argomento vicino ma diverso, non va nella risposta principale: al massimo va segnalata come accessorio.
In sintesi: meglio poche fonti buone, con il limite dichiarato, che molte fonti rumorose.
OCR caution
Quando una fonte proviene da una scansione, spesso il testo è sporco, incompleto o frammentario. Il reranking dovrebbe riconoscere questa condizione e ridurre il grado di certezza associato a quella fonte. Non si tratta di buttarla via: si tratta di non trasformarla in prova piena.
Una buona pratica è marcare queste fonti nel prompt finale:
Fonte [3]: manuale tecnico, sezione sconosciuta, OCR frammentario.
Usa con cautela: la conclusione richiede verifica sul documento originale.
È un piccolo gesto, ma cambia la qualità della risposta. Il modello smette di trattare un testo rovinato come una verità e inizia a usare formule caute.
Un esempio end-to-end
Proviamo a mettere insieme i pezzi con un esempio concreto.
L’utente di un assistente tecnico fa una domanda complessa su un caso specifico. La query riformulata viene mandata al retrieval ibrido, che restituisce 24 frammenti candidati.
Senza reranking, i 24 frammenti finiscono tutti nel prompt. Tra questi:
- 3 sono realmente decisivi;
- 6 sono pertinenti ma accessori;
- 10 sono laterali (condividono termini ma non rispondono);
- 5 sono frammenti OCR di bassa qualità.
Il modello risponde, ma mescola tutto. Qualche fonte laterale viene citata come fosse centrale. Un frammento OCR sporco diventa, inavvertitamente, base di una conclusione.
Con un reranking corretto:
- il giudice LLM punteggia i 24 candidati;
- le fonti laterali vengono scartate (relevance filter);
- le fonti OCR vengono marcate come tali (OCR caution);
- si selezionano le 4 fonti migliori;
- lo stato del reranking è
ok, quindi la selezione è affidabile.
La risposta ora si basa su 4 fonti buone, con le fonti frammentarie segnalate e le laterali escluse. Più breve, più onesta, più utile.
Se invece il giudice LLM fallisce, il sistema cade sul fallback deterministico, lo stato diventa llm_invalid_json, e la risposta include un avviso: “la selezione delle fonti è meno precisa del solito, verifica la pertinenza”. Nessuno viene ingannato.
Conclusione
Un motore di reranking si pensa spesso come un “ottimizzatore”: qualcosa che rende le risposte un po’ migliori. In realtà il suo ruolo è più profondo.
Tra il retrieval e la generazione, il reranking è il punto in cui il sistema decide cosa è vero. Decide quali fonti sono decisive, quali sono rumore, quali sono fragili. E se questo passaggio viene saltato o fatto male — in silenzio — tutto il sistema continua a funzionare, ma risponde peggio, senza che nessuno se ne accorga.
La parte più importante non è il punteggio in sé. È il cambio di prospettiva: un reranking serio non si limita a riordinare. Dichiara il proprio stato, riconosce i propri limiti e non mente quando degrada.
Ed è proprio qui che un motore di reranking mostra il suo valore reale: non nella ricerca della risposta perfetta, ma nella capacità di dire, onestamente, quanto ci si può fidare di quella risposta.
Fonti
- Reciprocal Rank Fusion ( originale, Cormack et al., 2009): https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf
- Qdrant — Hybrid search e RRF: https://qdrant.tech/articles/hybrid-search/
- BM25, la funzione di ranking sparse: https://en.wikipedia.org/wiki/Okapi_BM25
- LLM-as-a-Judge (Zheng et al., 2023): https://arxiv.org/abs/2306.05685
AI Disclaimer
Questo articolo è stato sviluppato a partire da appunti personali usati per lo sviluppo di un sistema rag reale, e con l’ausilio dell’intelligenza artificiale per riorganizzare i contenuti, rendere più chiara la struttura e trasformare le note iniziali in una guida più leggibile e coerente per tutti.