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
| 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
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