RAG avancé : multi-query, HyDE, step-back
Mis à jour le 29 juillet 2026
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
Le reranking de la leçon précédente améliorait le classement des candidats, mais il ne pouvait rien pour un document que le retrieval n’avait pas remonté. Or la cause la plus fréquente de ces oublis n’est ni le modèle ni l’index : c’est la requête elle-même. L’utilisateur écrit « ça marche pas sur mon tel » quand la documentation parle d’« incompatibilité des versions Android antérieures à 12 ». Aucun modèle d’embedding ne rapprochera spontanément ces deux formulations.
Les techniques qui suivent partagent donc un même principe : au lieu de subir la requête telle qu’elle arrive, on la transforme ou on la démultiplie avant de chercher. Elles agissent toutes sur le recall, c’est-à-dire sur la capacité à faire entrer le bon document dans la liste des candidats.
Multi-Query : poser la question autrement
La première approche est la plus directe. On demande au LLM de reformuler la question sous plusieurs angles, on lance une recherche avec chaque variante, et on fusionne. La question « comment annuler mon abonnement » devient aussi « procédure de résiliation », « arrêter le prélèvement mensuel » et « supprimer mon compte payant » — trois vocabulaires différents, trois chances de tomber sur celui qu’a employé le rédacteur de la documentation.
La fusion mérite un mot : la ligne np.maximum conserve, pour chaque document, le meilleur score obtenu toutes requêtes confondues. Un document qui ne répond bien qu’à une seule reformulation remonte donc quand même, ce qui ne serait pas le cas avec une moyenne. La température de 0,7 est volontaire, car on cherche ici de la variété, pas de la reproductibilité.
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.6-terra",
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
HyDE prend le problème par l’autre bout. Plutôt que de multiplier les questions, on fait générer au modèle une réponse plausible — même si elle est partiellement fausse, cela n’a aucune importance — et l’on cherche les documents proches de cette réponse. La question « comment configurer le SSO ? » produit un paragraphe qui parle de fournisseur d’identité, de SAML et de métadonnées, exactement le vocabulaire de la vraie documentation.
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.6-terra",
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
]
Si la technique fonctionne, c’est que l’embedding d’une réponse ressemble davantage à l’embedding d’un document que ne le fait l’embedding d’une question. Une question est courte, interrogative, souvent formulée dans les mots de l’utilisateur ; un document est long, affirmatif, écrit dans le jargon du domaine. HyDE comble ce fossé « question vs document » (query-document gap) en amenant la requête sur le terrain du corpus. C’est aussi pourquoi la consigne précise « comme si c’était un extrait de documentation technique » : on imite le registre de la cible.
Step-Back Prompting
La troisième technique traite le cas inverse du précédent : celui des questions si spécifiques qu’aucun document ne les traite frontalement. « Puis-je déduire un vélo électrique acheté en février ? » n’existe nulle part, mais la section sur les frais professionnels déductibles, elle, existe. On demande donc au modèle la question générale qui se cache derrière, puis on cherche avec les deux, en pondérant 0,7 pour la question originale et 0,3 pour la question de fond — le contexte large aide, il ne doit pas dominer.
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.6-terra",
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
Chacune répond à une pathologie précise du retrieval, et chacune se paie en appels supplémentaires donc en latence.
| Technique | Quand l’utiliser | Coût supplémentaire |
|---|---|---|
| Multi-query | Requêtes ambiguës, vocabulaire varié | n appels embedding + 1 appel LLM |
| HyDE | Fossé question/document, domaines techniques | 1 appel LLM + 1 appel embedding |
| Step-back | Questions très spécifiques, besoin de contexte | 1 appel LLM + 2 appels embedding |
Combiner les techniques
Rien n’oblige à choisir. La fonction suivante accepte une liste de stratégies, accumule les scores obtenus par chacune dans un dictionnaire indexé par document, puis retient le maximum. Un document trouvé par HyDE seul ressort donc au même titre qu’un document trouvé par la recherche directe. Gardez toutefois à l’esprit que cumuler trois stratégies triple le budget et allonge la réponse : réservez cette combinaison aux requêtes difficiles, ou aux traitements hors ligne où la latence ne coûte rien.
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