On garde malgrès tout les promps à coté des parseurs car ils évoluent généralement ensemble.
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""Reranking LLM des passages RAG (chat des ateliers).
|
||
|
||
Le cosinus classe par similarité de SURFACE ; sur les questions ambiguës, des
|
||
passages proches lexicalement mais inutiles passent devant l'extrait qui répond
|
||
vraiment. Le reranking récupère un POOL élargi (ex. 3× top_k), fait noter la
|
||
pertinence de chaque extrait par le LLM en UN appel, et garde les top_k mieux
|
||
notés. Coût : ~1 appel LLM avant le premier token — opt-in via RAG_RERANK.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from app.application.llm_json import load_json_object
|
||
from app.application.prompts import rerank as prompts
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Taille du pool élargi : multiple du top_k demandé, plafonné (le prompt de
|
||
# notation doit rester raisonnable même avec rag_top_k élevé).
|
||
POOL_FACTOR = 3
|
||
POOL_MAX = 24
|
||
|
||
# Un extrait long n'a pas besoin d'être noté en entier : tronquer borne le
|
||
# prompt sans changer le jugement de pertinence.
|
||
_EXCERPT_CHARS = 600
|
||
|
||
|
||
def pool_size(top_k: int) -> int:
|
||
"""Taille du pool à récupérer avant reranking."""
|
||
return min(max(top_k * POOL_FACTOR, top_k), POOL_MAX)
|
||
|
||
|
||
async def rerank(llm, question: str, passages: list[dict], top_k: int) -> list[dict]:
|
||
"""Renvoie les `top_k` passages les mieux notés par le LLM (tri stable :
|
||
à note égale, l'ordre cosinus d'origine est préservé).
|
||
|
||
BEST-EFFORT : échec LLM, JSON invalide ou nombre de notes incohérent →
|
||
on renvoie simplement les `top_k` premiers du classement cosinus.
|
||
"""
|
||
if len(passages) <= top_k:
|
||
return passages
|
||
numbered = "\n\n".join(
|
||
f"--- EXTRAIT {i + 1} ---\n{(p.get('text') or '')[:_EXCERPT_CHARS]}"
|
||
for i, p in enumerate(passages)
|
||
)
|
||
prompt = prompts.RERANK_PROMPT.format(
|
||
question=question, passages=numbered, count=len(passages))
|
||
try:
|
||
raw = await llm.generate(prompt, temperature=0.0)
|
||
except Exception as exc: # noqa: BLE001 — un chat dégradé vaut mieux que pas de chat
|
||
logger.warning("Reranking ignoré (échec LLM) : %s", exc)
|
||
return passages[:top_k]
|
||
parsed, _ = load_json_object(raw)
|
||
scores = parsed.get("scores") if isinstance(parsed, dict) else None
|
||
if not isinstance(scores, list) or len(scores) != len(passages):
|
||
logger.warning("Reranking ignoré (notes inexploitables).")
|
||
return passages[:top_k]
|
||
try:
|
||
scored = [(float(s), i) for i, s in enumerate(scores)]
|
||
except (TypeError, ValueError):
|
||
logger.warning("Reranking ignoré (notes non numériques).")
|
||
return passages[:top_k]
|
||
order = sorted(range(len(passages)), key=lambda i: (-scored[i][0], i))
|
||
return [passages[i] for i in order[:top_k]]
|