Aller au contenu principal

RAG avancé : multi-query, HyDE, step-back

Objectifs

  • Améliorer le retrieval avec des techniques avancées
  • Implémenter multi-query, HyDE et step-back prompting
  • Savoir quand utiliser chaque technique

Le problème : une seule requête ne suffit pas

La requête de l’utilisateur n’est pas toujours formulée de manière optimale pour la recherche. Les techniques avancées transforment ou augmentent la requête pour améliorer le recall.

Multi-Query : poser la question autrement

Générez plusieurs reformulations de la question, cherchez avec chacune, puis fusionnez les résultats :

from openai import OpenAI
import numpy as np

client = OpenAI()

def generer_multi_queries(question: str, n: int = 3) -> list[str]:
    """Génère n reformulations de la question."""
    response = client.chat.completions.create(
        model="gpt-5.3",
        messages=[{
            "role": "user",
            "content": (
                f"Génère {n} reformulations différentes de cette question "
                f"pour une recherche documentaire. Chaque reformulation "
                f"doit couvrir un angle différent.\n\n"
                f"Question originale : {question}\n\n"
                f"Retourne uniquement les reformulations, une par ligne."
            )
        }],
        temperature=0.7
    )
    queries = response.choices[0].message.content.strip().split("\n")
    return [q.strip().lstrip("0123456789.-) ") for q in queries if q.strip()]

def recherche_multi_query(
    question: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5,
    n_queries: int = 3
) -> list[dict]:
    """Recherche avec plusieurs reformulations."""
    queries = [question] + generer_multi_queries(question, n_queries)
    print(f"Requêtes : {queries}")

    tous_scores = np.zeros(len(documents))
    for query in queries:
        query_emb = client.embeddings.create(
            input=query, model="text-embedding-3-large"
        ).data[0].embedding
        scores = corpus_embeddings @ np.array(query_emb)
        tous_scores = np.maximum(tous_scores, scores)

    top_k = np.argsort(tous_scores)[-k:][::-1]
    return [
        {**documents[i], "score": float(tous_scores[i])}
        for i in top_k
    ]

HyDE : Hypothetical Document Embeddings

Au lieu de chercher avec la question, générez une réponse hypothétique puis cherchez des documents similaires à cette réponse :

def recherche_hyde(
    question: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5
) -> list[dict]:
    """Recherche avec un document hypothétique (HyDE)."""
    # 1. Générer un document hypothétique
    response = client.chat.completions.create(
        model="gpt-5.3",
        messages=[{
            "role": "user",
            "content": (
                f"Écris un court paragraphe (3-5 phrases) qui répondrait "
                f"à cette question. Écris comme si c'était un extrait "
                f"de documentation technique.\n\n"
                f"Question : {question}"
            )
        }],
        temperature=0.3
    )
    doc_hypothetique = response.choices[0].message.content

    # 2. Encoder le document hypothétique
    hyde_emb = client.embeddings.create(
        input=doc_hypothetique,
        model="text-embedding-3-large"
    ).data[0].embedding

    # 3. Chercher des documents similaires
    scores = corpus_embeddings @ np.array(hyde_emb)
    top_k = np.argsort(scores)[-k:][::-1]

    return [
        {**documents[i], "score": float(scores[i])}
        for i in top_k
    ]

Pourquoi HyDE fonctionne

L’embedding d’une réponse ressemble davantage à l’embedding d’un document qu’à l’embedding d’une question. HyDE comble ce fossé « question vs document » (query-document gap).

Step-Back Prompting

Posez d’abord une question plus générale pour trouver le contexte large, puis affinez :

def recherche_step_back(
    question: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5
) -> list[dict]:
    """Recherche en deux passes : générale puis spécifique."""
    # 1. Générer une question step-back
    response = client.chat.completions.create(
        model="gpt-5.3",
        messages=[{
            "role": "user",
            "content": (
                f"Quelle est la question plus générale ou le concept "
                f"de fond derrière cette question spécifique ?\n\n"
                f"Question spécifique : {question}\n\n"
                f"Retourne uniquement la question générale."
            )
        }],
        temperature=0.3
    )
    question_generale = response.choices[0].message.content.strip()

    # 2. Chercher avec les deux questions
    resultats_combines = {}

    for q, poids in [(question, 0.7), (question_generale, 0.3)]:
        q_emb = client.embeddings.create(
            input=q, model="text-embedding-3-large"
        ).data[0].embedding
        scores = corpus_embeddings @ np.array(q_emb)

        for i, s in enumerate(scores):
            if i not in resultats_combines:
                resultats_combines[i] = 0.0
            resultats_combines[i] += float(s) * poids

    top_k = sorted(
        resultats_combines.items(),
        key=lambda x: x[1], reverse=True
    )[:k]

    return [
        {**documents[idx], "score": score}
        for idx, score in top_k
    ]

Comparaison des techniques

TechniqueQuand l’utiliserCoût supplémentaire
Multi-queryRequêtes ambiguës, vocabulaire varién appels embedding + 1 appel LLM
HyDEFossé question/document, domaines techniques1 appel LLM + 1 appel embedding
Step-backQuestions très spécifiques, besoin de contexte1 appel LLM + 2 appels embedding

Combiner les techniques

def recherche_avancee(
    question: str,
    corpus_embeddings: np.ndarray,
    documents: list[dict],
    k: int = 5,
    strategies: list[str] = ["direct", "multi_query", "hyde"]
) -> list[dict]:
    """Combine plusieurs stratégies de recherche."""
    tous_resultats = {}

    if "direct" in strategies:
        q_emb = client.embeddings.create(
            input=question, model="text-embedding-3-large"
        ).data[0].embedding
        scores = corpus_embeddings @ np.array(q_emb)
        for i, s in enumerate(scores):
            tous_resultats.setdefault(i, []).append(float(s))

    if "hyde" in strategies:
        res_hyde = recherche_hyde(
            question, corpus_embeddings, documents, k=k*2
        )
        for r in res_hyde:
            tous_resultats.setdefault(r["id"], []).append(r["score"])

    # Score final = max des scores
    scores_finaux = {
        idx: max(scores) for idx, scores in tous_resultats.items()
    }

    top_k = sorted(
        scores_finaux.items(), key=lambda x: x[1], reverse=True
    )[:k]

    return [
        {**documents[idx], "score": score}
        for idx, score in top_k
    ]

Résumé

  • Multi-query reformule la question sous plusieurs angles pour couvrir plus de vocabulaire
  • HyDE génère une réponse hypothétique qui ressemble davantage aux documents cibles
  • Step-back pose une question plus générale pour capturer le contexte large
  • Ces techniques ajoutent de la latence mais améliorent significativement le recall