Aller au contenu principal

Retrieval et Génération

Mis à jour le 29 juillet 2026

Du vecteur à la réponse

Vous savez indexer des documents et retrouver les plus proches d’une question. Il manque la dernière marche, celle qui transforme une liste de chunks en réponse lisible : injecter le contexte retrouvé dans un prompt pour que le LLM formule une réponse fondée sur vos données plutôt que sur ses souvenirs d’entraînement. C’est une étape modeste en lignes de code et décisive en qualité, car un contexte bien présenté et une consigne claire pèsent autant que la finesse du retrieval.

Construire le prompt RAG

La structure du prompt est simple, mais chacun de ses choix a une conséquence. Les chunks sont numérotés et annotés de leur source, ce qui donne au modèle de quoi citer précisément ; ils sont séparés par un délimiteur visible, ---, pour qu’il ne fusionne pas deux documents contradictoires en une affirmation unique. Surtout, la consigne restreint explicitement la réponse au contexte fourni et autorise le modèle à dire qu’il ne sait pas. Sans cette autorisation, un LLM sollicité sur une information absente comblera le vide avec ce qui lui semble plausible : c’est le mécanisme même de l’hallucination.

def build_rag_prompt(question, retrieved_chunks):
    """Construire le prompt RAG avec le contexte retrouvé."""
    # Formater le contexte
    context_parts = []
    for i, chunk in enumerate(retrieved_chunks):
        source = chunk.get("source", "source inconnue")
        context_parts.append(f"[Document {i+1} - {source}]\n{chunk['text']}")

    context = "\n\n---\n\n".join(context_parts)

    prompt = f"""Vous êtes un assistant expert. Répondez à la question en vous basant
UNIQUEMENT sur le contexte fourni ci-dessous. Si l'information n'est pas dans
le contexte, dites-le clairement.

## Contexte

{context}

## Question

{question}

## Réponse"""

    return prompt

Générer la réponse

L’appel de génération n’a rien d’exotique : un message système qui rappelle le rôle, un message utilisateur qui porte le prompt assemblé. Le paramètre à surveiller est la température, fixée ici à 0.1. En rédaction créative, on cherche de la variété ; en RAG, on cherche l’inverse — que le modèle reste au plus près de ce que dit le contexte et que deux exécutions sur la même question donnent la même réponse. Une température élevée pousserait le modèle à reformuler, extrapoler, et à s’éloigner peu à peu de la source.

from mistralai import Mistral

client = Mistral(api_key=api_key)

def generate_answer(question, retrieved_chunks, model="mistral-large-latest"):
    """Générer une réponse RAG avec Mistral."""
    prompt = build_rag_prompt(question, retrieved_chunks)

    response = client.chat.complete(
        model=model,
        messages=[
            {
                "role": "system",
                "content": "Vous êtes un assistant précis qui répond en se basant sur le contexte fourni."
            },
            {
                "role": "user",
                "content": prompt,
            }
        ],
        temperature=0.1,  # Basse température pour des réponses factuelles
    )

    return response.choices[0].message.content

# Exemple
question = "Quelles sont les étapes principales du RAG ?"
results = search_index(question, index, chunks, k=3)
answer = generate_answer(question, results)
print(answer)

Encapsuler le flux dans une classe

Les fonctions éparpillées suffisent pour explorer, pas pour livrer. La classe RAGPipeline réunit l’état — les chunks, la matrice d’embeddings, l’index FAISS construit une fois pour toutes à l’instanciation — et les trois opérations : retrieve encode la question et interroge l’index, generate assemble le prompt et appelle le modèle, ask enchaîne les deux. Notez ce que renvoie ask : pas une chaîne de caractères, mais un dictionnaire contenant la réponse, la liste des sources et le nombre de chunks utilisés. Cette traçabilité est ce qui distingue un assistant qu’on peut auditer d’une boîte noire ; le jour où un utilisateur conteste une réponse, vous saurez sur quels passages elle reposait.

import numpy as np
import faiss
from mistralai import Mistral

client = Mistral(api_key=api_key)

class RAGPipeline:
    """Pipeline RAG complet avec Mistral et FAISS."""

    def __init__(self, chunks, embeddings):
        self.chunks = chunks
        self.embeddings = embeddings.astype(np.float32)

        # Créer l'index FAISS
        dimension = embeddings.shape[1]
        self.index = faiss.IndexFlatL2(dimension)
        self.index.add(self.embeddings)

    def retrieve(self, question, k=3):
        """Étape 1 : retrouver les chunks pertinents."""
        response = client.embeddings.create(
            model="mistral-embed",
            inputs=[question],
        )
        query_vector = np.array(
            [response.data[0].embedding], dtype=np.float32
        )

        distances, indices = self.index.search(query_vector, k)

        results = []
        for dist, idx in zip(distances[0], indices[0]):
            if idx >= 0:
                results.append({
                    "text": self.chunks[idx]["text"],
                    "source": self.chunks[idx].get("source", ""),
                    "distance": float(dist),
                })
        return results

    def generate(self, question, context_chunks):
        """Étape 2 : générer la réponse avec le contexte."""
        prompt = build_rag_prompt(question, context_chunks)

        response = client.chat.complete(
            model="mistral-large-latest",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
        )
        return response.choices[0].message.content

    def ask(self, question, k=3):
        """Pipeline complet : question -> réponse sourcée."""
        # Retrieval
        context = self.retrieve(question, k=k)

        # Generation
        answer = self.generate(question, context)

        return {
            "question": question,
            "answer": answer,
            "sources": [c["source"] for c in context],
            "n_chunks_used": len(context),
        }

# Utilisation
rag = RAGPipeline(chunks, embeddings)
result = rag.ask("Comment fonctionne l'indexation dans FAISS ?")

print(f"Question: {result['question']}")
print(f"Réponse: {result['answer']}")
print(f"Sources: {result['sources']}")

Trois variantes de prompt selon l’usage

Le prompt de base peut se spécialiser selon ce que vous attendez de l’assistant. Si vos utilisateurs doivent pouvoir vérifier chaque affirmation, demandez explicitement la citation entre crochets : le modèle rattache alors chaque phrase à un document numéroté. Si la réponse alimente une interface ou un rapport, le format structuré impose des rubriques stables — réponse concise, détails, sources, et un niveau de confiance qui reflète la couverture du contexte. La troisième variante joue un rôle différent : elle apprend au modèle à découper l’incertitude, en distinguant ce qu’il peut affirmer, ce qui manque, et où chercher la suite. C’est souvent celle qui produit les assistants les plus utiles en pratique, parce qu’un « il me manque tel élément » vaut mieux qu’une réponse confiante et fausse.

# 1. Prompt avec instruction de citation
prompt_cite = """Répondez en citant les documents sources entre crochets.
Exemple : "Le RAG améliore la précision [Document 1]."

Contexte: {context}
Question: {question}"""

# 2. Prompt avec format structuré
prompt_structured = """Répondez au format suivant :

**Réponse** : [votre réponse concise]
**Détails** : [explication détaillée]
**Sources** : [liste des documents utilisés]
**Confiance** : [haute/moyenne/basse selon la couverture du contexte]

Contexte: {context}
Question: {question}"""

# 3. Prompt avec gestion de l'absence d'information
prompt_honest = """Si le contexte ne contient pas assez d'information pour
répondre complètement, indiquez clairement :
- Ce que vous pouvez affirmer avec certitude
- Ce qui manque dans le contexte
- Des suggestions pour obtenir l'information manquante

Contexte: {context}
Question: {question}"""

Côté paramètres d’appel, trois réglages encadrent le comportement en RAG. La température basse verrouille le côté factuel, max_tokens empêche les réponses qui dérivent en dissertation, et un top_p conservateur restreint le vocabulaire échantillonné aux continuations les plus probables.

# Pour le RAG, utilisez une température basse
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": prompt}],
    temperature=0.1,   # Factuel, peu de créativité
    max_tokens=1024,    # Limiter la longueur de la réponse
    top_p=0.95,         # Nucleus sampling conservateur
)

Points clés à retenir

  • Le prompt RAG injecte le contexte retrouvé avant la question
  • Utilisez une température basse (0.1-0.3) pour des réponses factuelles
  • Demandez au modèle de citer ses sources pour la traçabilité
  • La classe RAGPipeline encapsule tout le flux : retrieve, generate, ask
  • Gérez le cas où le contexte ne contient pas assez d’information