watch·ia
AccueilActusTutosGlossaireCette semaineTendancesSources
/
À chaud

Mettre un RAG en production : chunking, embeddings et reranking

mercredi 17 juin 202607:135 min de lecture
L'essentiel — 3 points
  • 01La qualité d'un RAG se joue d'abord sur le découpage et la qualité des données, pas sur le LLM.
  • 02Une recherche hybride (vecteurs + mots-clés) puis un reranker améliore nettement la pertinence.
  • 03Sans évaluation chiffrée du retrieval (Recall@k), tu optimises à l'aveugle : mesure d'abord la récupération.
AVANCÉ

À la fin de ce tuto, tu sauras faire passer un RAG du prototype qui impressionne en démo au système qui répond juste sur des milliers de documents réels. On couvre les quatre leviers qui comptent — chunking, recherche hybride, reranking, mesure — avec le code des étapes clés. Prérequis : avoir fait le tuto « Le RAG expliqué ».

Durée : 35 min.

Le constat de départ

Un RAG de démonstration se monte en une après-midi. Un RAG qui répond juste sur des milliers de documents réels, c'est un autre métier. Le modèle de langage est rarement le maillon faible : c'est la récupération (retrieval) qui fait ou défait la qualité. Si le bon passage n'est pas remonté, le meilleur LLM du monde ne peut pas répondre. Tous les leviers ci-dessous visent donc le retrieval.

Levier 1 — Le chunking, premier facteur de qualité

Un mauvais découpage condamne tout le reste. Quatre principes :

  • Découpe sur la structure, pas au caractère près : par section, paragraphe ou titre, pour ne pas couper une idée en deux.
  • Ajoute du chevauchement (overlap) entre chunks, pour ne pas perdre le contexte aux frontières.
  • Enrichis chaque chunk de métadonnées : source, date, titre de section. Elles servent au filtrage et à la citation.
  • Adapte la taille au contenu : un contrat juridique ne se découpe pas comme une FAQ.
def chunk_by_paragraph(text, max_chars=1200, overlap=150):
    paras, chunks, cur = text.split("\n\n"), [], ""
    for p in paras:
        if len(cur) + len(p) > max_chars and cur:
            chunks.append(cur.strip())
            cur = cur[-overlap:]  # chevauchement : on garde la fin du précédent
        cur += "\n\n" + p
    if cur.strip():
        chunks.append(cur.strip())
    return chunks

Levier 2 — Recherche hybride : vecteurs + mots-clés

La recherche purement vectorielle rate les correspondances exactes (références produit, codes, noms propres). La recherche par mots-clés (BM25) rate le sens. La recherche hybride combine les deux et fusionne les scores : c'est presque toujours supérieur à l'une ou l'autre seule.

# Pseudo-code de fusion : on récupère par les deux voies, on combine les rangs
def hybrid_search(query, k=20):
    vec_hits = vector_store.search(embed(query), k=k)      # sémantique
    kw_hits  = bm25_index.search(query, k=k)               # mots-clés exacts
    # Reciprocal Rank Fusion : chaque doc gagne 1/(rang+60) par liste
    scores = {}
    for rank, doc in enumerate(vec_hits): scores[doc.id] = scores.get(doc.id, 0) + 1/(rank+60)
    for rank, doc in enumerate(kw_hits):  scores[doc.id] = scores.get(doc.id, 0) + 1/(rank+60)
    return sorted(scores, key=scores.get, reverse=True)[:k]

La plupart des bases vectorielles sérieuses (Qdrant, Weaviate, pgvector + extension) proposent l'hybride en natif — tu n'as pas toujours à l'écrire à la main.

Levier 3 — Le reranking, le levier sous-estimé

La recherche initiale est rapide mais grossière : elle remonte 20-50 candidats approximatifs. Un reranker (un modèle dédié, type cross-encoder) repasse ensuite sur ces candidats pour les réordonner finement, et on ne garde que le top 3-5.

retrieval large (k=30, rapide)  →  rerank (réordonne finement)  →  top-k serré (k=4)

Ce second filtre améliore nettement la pertinence pour un coût modéré. C'est souvent le meilleur rapport gain/effort une fois le chunking correct.

def rerank(query, candidates, top_k=4):
    # un cross-encoder note chaque paire (query, passage) ; coûteux mais précis
    scored = [(reranker.score(query, c.text), c) for c in candidates]
    scored.sort(reverse=True, key=lambda x: x[0])
    return [c for _, c in scored[:top_k]]

Levier 4 — Mesurer, sinon tu optimises à l'aveugle

Sans métriques, chaque « amélioration » est une intuition. Construis un petit jeu de questions-réponses de référence (voir le tuto sur les évals) et mesure :

  • Recall@k : la bonne info est-elle dans les k morceaux récupérés ? C'est la métrique reine — si le retrieval rate, le LLM ne peut pas rattraper.
  • Pertinence des passages remontés.
  • Fidélité de la réponse aux sources (pas d'invention).
def recall_at_k(eval_set, k=4):
    hits = 0
    for case in eval_set:
        retrieved = hybrid_search(case["question"], k=k)
        if case["doc_id_attendu"] in retrieved:
            hits += 1
    return hits / len(eval_set)

Isole l'évaluation du retrieval de celle de la génération. Recall@k te dit si la récupération marche ; la fidélité te dit si le LLM exploite bien ce qu'on lui donne. Mesurer les deux séparément te dit quel maillon corriger.

Les détails de production qui font la différence

  • Citations : fais pointer chaque réponse vers ses sources (via les métadonnées des chunks), pour la confiance et l'audit.
  • Mise à jour : prévois la réindexation quand les documents changent.
  • Cas vide : si rien de pertinent n'est trouvé, réponds « je ne sais pas » plutôt que d'inventer (cf. le prompt cadré du tuto RAG de base).
  • Cache : mets en cache les embeddings et les requêtes fréquentes pour le coût et la latence.
  • Sécurité : un document indexé peut contenir une prompt injection (voir le tuto sécurité). Traite les passages récupérés comme des données non fiables.

À retenir

En production, le RAG se gagne sur le retrieval, pas sur le LLM. Soigne le chunking et la qualité des données, passe en recherche hybride, ajoute un reranker pour resserrer le top-k. Surtout, mesure : un jeu d'éval distinguant retrieval et génération t'évite d'optimiser à l'aveugle. Et gère les détails — citations, réindexation, cas vide, sécurité — qui transforment un prototype en système de confiance.

Réagir :
Partager —XLinkedIn