Retrieval-augmented prompting
Mis à jour le 28 juillet 2026
Retrieval-augmented prompting
Le Retrieval-Augmented Prompting (RAP) combine la recherche documentaire avec le prompting pour fournir au modèle un contexte pertinent et à jour. C’est l’évolution naturelle du grounding : au lieu d’injecter manuellement le contexte, vous automatisez la recherche des passages pertinents avant chaque appel au LLM.
La différence se mesure très vite à l’échelle. Coller à la main les trois paragraphes utiles de votre documentation fonctionne pour une démonstration ; dès que votre base documentaire atteint quelques centaines de pages et que les questions arrivent en continu, plus personne ne sait quels passages coller. Le RAP délègue ce choix à un composant automatique, appelé avant chaque génération, dont le seul rôle est de trouver quoi mettre dans le prompt.
Architecture d’un système RAP
Un pipeline RAP se décompose en trois étapes qui n’ont ni le même rythme ni le même coût. L’indexation transforme vos documents en embeddings vectoriels ; elle se fait une fois, hors ligne, et chaque document y devient un vecteur numérique qui encode son sens, stocké à côté du texte d’origine. Le retrieval s’exécute au contraire à chaque question : on transforme la question en vecteur avec le même modèle d’embedding, puis on cherche les vecteurs documentaires les plus proches. L’augmentation, enfin, n’est que du prompting classique — on construit un system prompt qui contient les passages retrouvés et qui interdit au modèle de sortir de ce périmètre.
Retenez ce partage : tout ce qui coûte cher se fait à l’indexation, où l’on peut être exigeant, tandis que ce qui se répète à chaque question doit rester rapide. Le code ci-dessous implémente les deux premières étapes avec la similarité cosinus comme mesure de proximité.
from openai import OpenAI
import numpy as np
client = OpenAI()
# Étape 1 : Générer les embeddings des documents
def embed_texts(texts: list[str]) -> list[list[float]]:
"""Génère les embeddings pour une liste de textes."""
response = client.embeddings.create(
model="text-embedding-3-large",
input=texts
)
return [item.embedding for item in response.data]
# Étape 2 : Recherche par similarité cosinus
def cosine_similarity(a: list[float], b: list[float]) -> float:
"""Calcule la similarité cosinus entre deux vecteurs."""
a, b = np.array(a), np.array(b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def retrieve(query: str, documents: list[str],
doc_embeddings: list[list[float]],
top_k: int = 3) -> list[str]:
"""Retourne les top_k documents les plus pertinents."""
query_emb = embed_texts([query])[0]
scores = [
(i, cosine_similarity(query_emb, doc_emb))
for i, doc_emb in enumerate(doc_embeddings)
]
scores.sort(key=lambda x: x[1], reverse=True)
return [documents[i] for i, _ in scores[:top_k]]
Pipeline complet
Assembler ces briques donne une classe qui tient en une trentaine de lignes. L’essentiel des décisions se joue dans le system prompt généré, et il vaut la peine de le relire ligne à ligne. Le modèle y reçoit d’abord l’ordre de répondre uniquement à partir des passages fournis, ce qui limite les réponses inventées quand la documentation est muette. Il reçoit ensuite celui de citer le numéro du passage pour chaque affirmation, et c’est cette consigne qui rend la vérification possible : un relecteur remonte à la source en un coup d’œil. La temperature à 0.1 complète le dispositif — sur une réponse ancrée dans des documents, la créativité est un défaut.
class RAGPipeline:
"""Pipeline RAP minimaliste."""
def __init__(self, documents: list[str]):
self.documents = documents
self.embeddings = embed_texts(documents)
def query(self, question: str, top_k: int = 3) -> str:
"""Répond à une question en s'appuyant sur les documents."""
# Retrieval
relevant_docs = retrieve(
question, self.documents,
self.embeddings, top_k
)
# Augmentation du prompt
context = "\n\n---\n\n".join(
f"[Passage {i+1}]\n{doc}"
for i, doc in enumerate(relevant_docs)
)
system = f"""Tu es un assistant technique.
Réponds UNIQUEMENT à partir des passages fournis.
Si les passages ne contiennent pas la réponse, dis-le.
Cite le numéro du passage pour chaque affirmation.
PASSAGES PERTINENTS :
{context}"""
# Génération
response = client.responses.create(
model="gpt-5.6-terra",
instructions=system,
input=question,
temperature=0.1
)
return response.output_text
Chunking : découper les documents
Le découpage des documents en chunks est crucial pour la qualité du retrieval. Trop grands, les chunks noient l’information pertinente ; trop petits, ils perdent le contexte. Un manuel d’API découpé en blocs de dix pages ramènera toujours le même bloc générique, quelle que soit la question posée. Découpé en phrases isolées, il ramènera une phrase qui commence par « cette méthode » sans que le modèle sache de quelle méthode il s’agit. Le chevauchement amortit précisément ce risque : chaque chunk reprend la fin du précédent, si bien qu’une information à cheval sur une frontière reste lisible d’au moins un côté.
Quand vos documents ont une structure de titres, le découpage par sections est presque toujours supérieur au découpage à taille fixe : une section « Authentification » reste entière, avec son introduction et ses exemples.
def chunk_document(text: str, chunk_size: int = 500,
overlap: int = 50) -> list[str]:
"""Découpe un texte en chunks avec chevauchement."""
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = " ".join(words[i:i + chunk_size])
if chunk.strip():
chunks.append(chunk)
return chunks
def chunk_by_sections(text: str) -> list[str]:
"""Découpe par sections (titres Markdown)."""
sections = []
current = []
for line in text.split("\n"):
if line.startswith("## ") and current:
sections.append("\n".join(current))
current = [line]
else:
current.append(line)
if current:
sections.append("\n".join(current))
return sections
Stratégies de retrieval avancées
Reranking
Après le retrieval initial, utilisez le LLM pour réordonner les résultats. La logique est celle d’un entonnoir : la recherche vectorielle est rapide mais approximative, elle ramène une dizaine de candidats plausibles ; le modèle, plus lent mais plus fin, relit ces candidats en connaissant la question et les classe. Vous ne gardez ensuite que le haut du classement pour la génération. Le gain se voit surtout sur les questions où plusieurs passages parlent du même sujet et où un seul répond réellement — trois sections mentionnent le mot « quota », une seule explique comment le relever.
def rerank(question: str, passages: list[str]) -> list[str]:
"""Réordonne les passages par pertinence avec le LLM."""
prompt = f"""Question : {question}
Voici des passages candidats. Classe-les du plus pertinent
au moins pertinent pour répondre à la question.
Retourne uniquement les numéros dans l'ordre (ex: 3, 1, 2).
{chr(10).join(f"[{i+1}] {p[:200]}" for i, p in enumerate(passages))}"""
response = client.responses.create(
model="gpt-5.6-terra",
input=prompt,
temperature=0.0
)
# Parser l'ordre retourné et réorganiser
return response.output_text
Hypothetical Document Embedding (HyDE)
Générez un document hypothétique idéal, puis cherchez des documents similaires. Le problème que cette technique résout est celui du décalage de vocabulaire : une question posée en langage courant ne ressemble pas, vectoriellement, à un paragraphe de documentation technique. En demandant d’abord au modèle d’écrire la réponse qu’il imagine, vous obtenez un texte rédigé dans le registre de vos documents, et c’est ce texte-là que vous utilisez comme requête. Peu importe que le paragraphe hypothétique contienne des approximations : il ne sert jamais de réponse, seulement de sonde.
def hyde_retrieve(question: str, documents: list[str],
doc_embeddings: list[list[float]]) -> list[str]:
"""Retrieval via HyDE."""
# Générer un passage hypothétique
hypo_response = client.responses.create(
model="gpt-5.6-terra",
input=f"Écris un court paragraphe technique qui répondrait "
f"parfaitement à cette question : {question}",
temperature=0.5
)
hypothetical_doc = hypo_response.output_text
# Chercher des documents similaires au passage hypothétique
return retrieve(hypothetical_doc, documents,
doc_embeddings, top_k=3)
Utiliser le File Search natif d’OpenAI
Pour un déploiement rapide, l’API propose un retrieval intégré. Vous uploadez le fichier, créez un vector store, y rattachez le fichier et déclarez l’outil file_search dans l’appel : chunking, embedding et recherche sont pris en charge côté serveur. C’est le bon choix quand vos documents sont standards et que l’endroit où vivent les données vous est indifférent. Le pipeline maison garde l’avantage dès que vous devez maîtriser le découpage, héberger l’index vous-même ou brancher un reranking spécifique.
# Upload du fichier
file = client.files.create(
file=open("documentation.pdf", "rb"),
purpose="assistants"
)
# Créer un vector store
vector_store = client.vector_stores.create(
name="Documentation technique"
)
client.vector_stores.files.create(
vector_store_id=vector_store.id,
file_id=file.id
)
# Query avec File Search
response = client.responses.create(
model="gpt-5.6-terra",
instructions="Réponds à partir de la documentation fournie.",
input="Comment configurer l'authentification ?",
tools=[{
"type": "file_search",
"vector_store_ids": [vector_store.id]
}]
)
Se donner un chiffre de départ
Prenez cinq à dix pages de documentation technique et implémentez le pipeline complet, du chunking à la génération. Rédigez dix questions dont la réponse figure dans les documents, faites tourner le pipeline sur chacune, puis recommencez avec le reranking activé : c’est en observant sur quelles questions le classement change que l’on comprend ce que la recherche vectorielle seule laisse passer. Terminez en mesurant le pourcentage de réponses correctement ancrées, celles dont chaque affirmation renvoie effectivement à un passage fourni. Ce chiffre est votre point de départ ; toutes les optimisations ultérieures se jugeront par rapport à lui, et non à l’impression laissée par deux ou trois réponses lues au hasard.
Points clés à retenir
- Le RAP automatise l’injection de contexte pertinent dans le prompt
- Le chunking est critique : expérimentez avec différentes tailles
- Le reranking avec le LLM améliore significativement la pertinence
- HyDE est utile quand la question est très différente du vocabulaire des documents
- Le File Search d’OpenAI simplifie le déploiement pour les cas standards