Aller au contenu principal

Reranking : améliorer la pertinence

Objectifs

  • Comprendre pourquoi le reranking améliore les résultats
  • Implémenter un reranker avec un cross-encoder
  • Intégrer le reranking dans un pipeline RAG

Le problème du bi-encoder

La recherche par embeddings utilise un bi-encoder : la requête et les documents sont encodés séparément. C’est rapide mais imprécis car le modèle ne voit jamais la requête et le document ensemble.

Un cross-encoder (reranker) prend la paire (requête, document) en entrée et produit un score de pertinence plus fin. Il est trop lent pour chercher dans tout le corpus, mais parfait pour re-classer un petit ensemble de candidats.

Pipeline en deux étapes

Requête → Bi-encoder → Top 50 candidats → Cross-encoder → Top 5 résultats

Reranking avec Cohere

Cohere propose un reranker performant via API :

import cohere
from openai import OpenAI

co = cohere.Client(api_key="votre-cle-cohere")
openai_client = OpenAI()

def rechercher_et_reranker(
    question: str,
    documents: list[str],
    embeddings_corpus,
    k_retrieval: int = 20,
    k_final: int = 5
) -> list[dict]:
    """Recherche sémantique + reranking."""
    # Étape 1 : Retrieval rapide (bi-encoder)
    query_emb = openai_client.embeddings.create(
        input=question,
        model="text-embedding-3-large"
    ).data[0].embedding

    import numpy as np
    scores = embeddings_corpus @ np.array(query_emb)
    top_indices = np.argsort(scores)[-k_retrieval:][::-1]
    candidats = [documents[i] for i in top_indices]

    # Étape 2 : Reranking (cross-encoder)
    rerank_response = co.rerank(
        query=question,
        documents=candidats,
        top_n=k_final,
        model="rerank-v3.5"
    )

    return [
        {
            "texte": candidats[r.index],
            "score_rerank": r.relevance_score,
            "index_original": int(top_indices[r.index])
        }
        for r in rerank_response.results
    ]

Reranking avec un LLM

Vous pouvez utiliser un LLM pour le reranking, surtout si vous voulez éviter une dépendance externe :

from openai import OpenAI

client = OpenAI()

def reranker_llm(
    question: str,
    candidats: list[str],
    k: int = 5
) -> list[dict]:
    """Utilise un LLM pour scorer la pertinence."""
    scores = []

    for i, doc in enumerate(candidats):
        response = client.chat.completions.create(
            model="gpt-5.3",
            messages=[{
                "role": "user",
                "content": (
                    f"Sur une échelle de 0 à 10, à quel point ce document "
                    f"est-il pertinent pour répondre à la question ?\n\n"
                    f"Question : {question}\n\n"
                    f"Document : {doc}\n\n"
                    f"Réponds uniquement avec un nombre entre 0 et 10."
                )
            }],
            temperature=0,
            max_tokens=5
        )
        try:
            score = float(response.choices[0].message.content.strip())
        except ValueError:
            score = 0.0
        scores.append({"index": i, "texte": doc, "score": score})

    scores.sort(key=lambda x: x["score"], reverse=True)
    return scores[:k]

Reranking par lots avec le LLM

Pour réduire le nombre d’appels, demandez au LLM de classer plusieurs documents en une seule requête :

def reranker_llm_batch(
    question: str,
    candidats: list[str],
    k: int = 5
) -> list[int]:
    """Reranking en un seul appel LLM."""
    docs_formates = "\n".join(
        f"[{i}] {doc[:200]}" for i, doc in enumerate(candidats)
    )

    response = client.chat.completions.create(
        model="gpt-5.3",
        messages=[{
            "role": "user",
            "content": (
                f"Classe ces documents par pertinence pour la question. "
                f"Retourne les indices des {k} documents les plus "
                f"pertinents, séparés par des virgules, du plus au "
                f"moins pertinent.\n\n"
                f"Question : {question}\n\n"
                f"Documents :\n{docs_formates}\n\n"
                f"Indices (ex: 3,1,7,0,5) :"
            )
        }],
        temperature=0,
        max_tokens=50
    )

    indices_str = response.choices[0].message.content.strip()
    return [int(i.strip()) for i in indices_str.split(",")][:k]

Intégration dans le pipeline RAG

class PipelineRAGAvecReranking:
    def __init__(self):
        self.client = OpenAI()
        self.co = cohere.Client()

    def repondre(self, question: str, chunks, embeddings_corpus):
        # 1. Retrieval large
        candidats = self.rechercher(question, embeddings_corpus, k=20)

        # 2. Reranking
        docs_candidats = [chunks[i]["texte"] for i in candidats]
        reranked = self.co.rerank(
            query=question,
            documents=docs_candidats,
            top_n=5,
            model="rerank-v3.5"
        )

        # 3. Sélectionner les meilleurs
        contextes = [
            chunks[candidats[r.index]]
            for r in reranked.results
            if r.relevance_score > 0.3  # Seuil de pertinence
        ]

        # 4. Générer la réponse
        return self.generer(question, contextes)

Impact du reranking

MétriqueSans rerankingAvec rerankingGain
Recall@50.720.72=
Precision@50.580.78+34 %
MRR0.650.82+26 %

Le reranking n’améliore pas le recall (les candidats sont les mêmes) mais améliore significativement la précision et le classement.

Résumé

  • Le reranking re-classe les candidats avec un modèle plus précis
  • Pipeline typique : bi-encoder (top 20-50) → cross-encoder (top 5)
  • Cohere rerank-v3.5 est une solution clé en main
  • Un LLM peut servir de reranker si vous préférez éviter une dépendance
  • Le gain en précision est significatif (20-35 % typiquement)