RAG juridique à l'ARLC 2026 : de 0,034 à 0,791 en 17 itérations avec analyse d'échelle
Dans le cadre du défi juridique ARLC 2026, le pipeline RAG partait d'un score de fondation de 0,05 et d'un score global de 0,034. Les réponses déterministes (S_det) avaient déjà atteint 0,857, mais le facteur G annulait les résultats. Le problème provenait du format du doc_id : submission.json attendait le nom de fichier sans l'extension '.pdf', tandis que le code transmettait le nom complet. Corriger une seule ligne a fait passer G à 0,55 et le score global à 0,438 — une amélioration de 13 fois.
Leçon pour les développeurs RAG : valider le format de soumission doit précéder toute optimisation de récupération. Même un reranking parfait est inutile sans une correspondance correcte entre (doc_id, numéro_de_page).
Architecture du pipeline : ingestion au niveau page et recherche hybride
Le pipeline indexe des pages entières, sans découpage. Cela simplifie la fondation au niveau page, mais nécessite une distillation de contexte avant traitement par LLM.
Analyse PDF avec sauvegarde OCR
def extract_pages(pdf_path, min_chars=50, ocr_dpi=300):
pages = []
doc = pdfplumber.open(pdf_path)
for i, page in enumerate(doc.pages):
text = page.extract_text() or ""
if len(text.strip()) < min_chars:
text = ocr_page(pdf_path, i + 1, dpi=ocr_dpi)
pages.append({
"doc_id": pdf_path.stem, # sans .pdf !
"page_number": i + 1,
"text": text
})
return pages
Le code gère les PDF numérisés via OCR lorsque le texte extrait est trop court. Le doc_id utilise le nom de base sans extension ; le numéro de page est basé sur 1.
Recherche hybride avec fusion de rang inverse (RRF)
Combinaison de BM25 pour les correspondances exactes et des embeddings pour la sémantique, via la fusion de rang inverse (k=60) :
def hybrid_search(query, bm25_index, embedding_index, pages, top_k=20, rrf_k=60):
bm25_scores = bm25_index.get_scores(tokenize(query))
bm25_ranked = sorted(range(len(pages)), key=lambda i: -bm25_scores[i])
q_emb = embed_model.encode([query[:512]], normalize_embeddings=True)
sim_scores = (embedding_index @ q_emb.T).flatten()
emb_ranked = sorted(range(len(pages)), key=lambda i: -sim_scores[i])
combined = {}
for rank, idx in enumerate(bm25_ranked[:top_k * 3]):
combined[idx] = combined.get(idx, 0) + 1.0 / (rrf_k + rank)
for rank, idx in enumerate(emb_ranked[:top_k * 3]):
combined[idx] = combined.get(idx, 0) + 1.0 / (rrf_k + rank)
return sorted(combined, key=lambda i: -combined[i])[:top_k]
Modèle d'embeddings : all-MiniLM-L6-v2 (22M paramètres). La RRF ne nécessite pas de normalisation d'échelle — elle fonctionne directement sur les rangs.
Reranking et étiquetage de priorité
Un cross-encodeur (ms-marco-MiniLM-L-6-v2) reclasse les 30 meilleures pages. Une limite stricte de 30 empêche l'explosion du temps de première réponse (TTFT) en O(N²).
def rerank(pages, query, top_k=5):
HARD_CAP = 30
priority = [p for p in pages if p.get("_priority")]
non_priority = pages[:HARD_CAP - len(priority)]
all_pages = priority + non_priority
pairs = [(query, p["text"][:512]) for p in all_pages]
scores = cross_encoder.predict(pairs)
for i, p in enumerate(all_pages):
if p.get("_priority"):
scores[i] = 1000.0
ranked = sorted(zip(scores, all_pages), key=lambda x: -x[0])
return [p for _, p in ranked[:top_k]]
Les pages prioritaires (pages de titre, articles) obtiennent un score de 1000 — garantissant leur inclusion dans le contexte.
Routage documentaire pour les requêtes comparatives
Pour les questions comme « qui a été nommé en premier dans CFI 001/2020 vs CFI 002/2021 », un index de pages de titre est construit :
- Modèles : CFI 010/2024 → doc_id
- Requête double avec entrelacement round-robin des contextes
Chemins rapides déterministes pour les réponses typées
Types : nombre (±1 %), booléen, nom (exact), date (YYYY-MM-DD), noms (Jaccard), texte libre (jugement par LLM).
Exemples d'extraction par regex :
- Numéro de loi :
re.search(r"(?:DIFC\s+)?Law\s+No\.\s*(\d+)") - Date d'émission : analyser les premières pages avec
Date of Issue[:\s]+(\w+ \d+,?\s+\d{4}) - Montants : extraire les prétentions monétaires via case_ref
Les chemins rapides économisent 700 ms sur les appels Haiku, en retournant la réponse + la fondation.
Formule de notation et optimisation du TTFT
Total = (0,7 × S_det + 0,3 × S_asst) × G × T × F
- G (F-beta, β=2,5) : fondation au niveau page — le multiplicateur clé
- F : <1000 ms → +5 %, >3000 ms → -15 %
| TTFT (ms) | Multiplicateur |
|-----------|----------------|
| <1000 | 1,05 |
| <2000 | 1,02 |
| <3000 | 1,00 |
| >3000 | 0,85–0,99 |
Mur d'échelle : phase de préchauffage vs finale
Sur 30 documents (préchauffage), le score était de 0,791. Sur 300 (phase finale), il a chuté de 42 %. Causes :
- Plus grand corpus dilue la qualité de récupération
- Le TTFT croît linéairement avec la taille
- Le reranking sur 300+ pages nuit à F
Optimisations finales : mise en cache des embeddings, batchage du cross-encodeur, arrêt anticipé dans la recherche hybride.
Ce qui compte vraiment
- Indexation au niveau page : simplifie la fondation mais exige une distillation de contexte.
- RRF (k=60) : fusion optimale de BM25 + embeddings sans normalisation.
- Étiquetage de priorité : assure que les pages critiques sont toujours présentes.
- Chemins rapides : regex pour 80 % des requêtes typées réduit fortement le TTFT.
- Approche TTFT-first : la vitesse donne un bonus de +5 % gratuitement à qualité égale.
— Editorial Team
Aucun commentaire pour le moment.