Aller au contenu principal

Module 5 — Bases vectorielles et récupérateurs

Notre politique de remboursement, découpée au module 4 en 180 morceaux, est prête. Il reste à en faire une base cherchable par sens, pas seulement par mots-clés : c'est la promesse du vecteur. Ce module fixe l'interface VectorStore, met en place deux implémentations concrètes (Chroma local et FAISS en mémoire), puis compose la chaîne RAG minimale qui répond à « le taxi vers un aéroport est-il remboursable ? ».

L'interface VectorStore

langchain-core définit trois méthodes qui suffisent :

MéthodeRôle
.add_documents([Document, ...])Calcule les plongements et les insère
.similarity_search(query, k=4)Retourne les k Document les plus proches
.as_retriever(**kwargs)Renvoie un Retriever composable avec `

Un VectorStore a besoin de deux choses : un modèle de plongements (le cours 18 sur le RAG en détaille le choix) et une cible de stockage. Chroma stocke sur disque, FAISS en mémoire, Pinecone et Weaviate chez un fournisseur externe. L'interface est la même ; on change une ligne pour changer de cible.

Chroma local, cas par défaut

Chroma s'installe en une commande (pip install langchain-chroma), persiste dans un répertoire local, et convient jusqu'à quelques millions de vecteurs. C'est la base par défaut pour prototyper et pour tout déploiement à taille d'équipe :

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

plongements = OpenAIEmbeddings(model="text-embedding-3-small")

magasin = Chroma.from_documents(
documents=morceaux, # les 180 morceaux du module 4
embedding=plongements,
collection_name="politique-notes-frais-2026",
persist_directory="./chroma_db",
)

Une fois construite, la collection se recharge sans réindexer :

magasin = Chroma(
collection_name="politique-notes-frais-2026",
embedding_function=plongements,
persist_directory="./chroma_db",
)

Pour un modèle ouvert local, OllamaEmbeddings(model="nomic-embed-text") remplace OpenAIEmbeddings sans autre modification — c'est l'apport concret de l'abstraction.

FAISS en mémoire

FAISS (Facebook AI Similarity Search) offre l'indexation la plus rapide en mémoire pure. Utile pour un lot ponctuel qui ne survit pas au processus, ou pour un service à faible latence avec un index qui tient en RAM :

from langchain_community.vectorstores import FAISS

magasin = FAISS.from_documents(morceaux, plongements)
magasin.save_local("faiss_index")

# Recharger
magasin = FAISS.load_local("faiss_index", plongements, allow_dangerous_deserialization=True)

Le paramètre allow_dangerous_deserialization=True reflète le fait qu'un index FAISS sérialisé contient du pickle : ne jamais recharger un fichier venu d'un tiers.

Le récupérateur, Runnable prêt à l'emploi

magasin.as_retriever() renvoie un Runnable composable dans une chaîne LCEL. Deux paramètres décident du comportement :

retriever = magasin.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"k": 4, "score_threshold": 0.75, "filter": {"annee": 2026}},
)
  • search_type : similarity (les k plus proches), mmr (diversité maximale), similarity_score_threshold (rejette sous le seuil).
  • search_kwargs : k le nombre de morceaux à retourner (typiquement 3 à 6), score_threshold pour couper les résultats faibles, filter pour les métadonnées.

Le seuil est le paramètre qui sépare un assistant utile d'un assistant inventif. Sans seuil, le récupérateur retourne toujours ses k meilleurs, même mauvais, et le modèle brode dessus. Un seuil de 0,7 à 0,8 (sur une similarité cosinus 0-1) permet à la chaîne de reconnaître « je ne sais pas » et de le dire.

MultiQueryRetriever : rattraper les reformulations

Une question posée dans les mots de l'utilisateur ne matche pas toujours les mots du document. « Le taxi vers un aéroport est-il remboursable ? » cherche « article 3.1 — transports professionnels — véhicule avec chauffeur ». MultiQueryRetriever demande au LLM de reformuler la question en trois variantes, lance trois recherches, puis dédoublonne :

from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI

retriever_multi = MultiQueryRetriever.from_llm(
retriever=magasin.as_retriever(search_kwargs={"k": 4}),
llm=ChatOpenAI(temperature=0),
)

Le rappel augmente sensiblement — au prix d'un appel LLM supplémentaire par question. ContextualCompressionRetriever fait l'inverse en filtrant les morceaux non pertinents avant génération.

La chaîne RAG minimale

Tout se compose en LCEL :

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

consigne = ChatPromptTemplate.from_template("""
Répondez à la question de l'employé en vous appuyant STRICTEMENT sur les extraits ci-dessous.
Citez l'article dans votre réponse. Si les extraits ne suffisent pas, dites-le clairement.

Extraits :
{contexte}

Question : {question}
""")

def formater(docs):
return "\n\n".join(f"[p.{d.metadata.get('page')}] {d.page_content}" for d in docs)

chaine_rag = (
{"contexte": retriever | formater, "question": RunnablePassthrough()}
| consigne
| ChatOpenAI(temperature=0)
| StrOutputParser()
)

print(chaine_rag.invoke("Le taxi vers un aéroport est-il remboursable ?"))

Trois choses à remarquer. Le dictionnaire en tête est un RunnableParallel implicite : contexte et question sont calculés en parallèle. La fonction formater transforme la liste de Document en texte avec numéros de page — c'est ce détail qui rend la réponse citable. Enfin, la chaîne est appelée avec une chaîne de caractères directement, parce que RunnablePassthrough accepte n'importe quel type.

Le piège du seuil de récupération

Sans score_threshold, la chaîne trouve toujours quelque chose à répondre, y compris pour une question hors périmètre (« quelle est la météo ? »). Un seuil bien calibré transforme les réponses inventées en un « aucun passage pertinent n'a été trouvé », qui est la bonne réponse.

En résumé

  • L'interface VectorStore uniformise add_documents, similarity_search et as_retriever ; Chroma local convient pour prototyper et déployer à taille d'équipe.
  • Le récupérateur est un Runnable : k, score_threshold et filter sur métadonnées gouvernent la qualité de la remontée.
  • MultiQueryRetriever rattrape les écarts de formulation en générant plusieurs variantes de la question, au prix d'un appel LLM supplémentaire.
  • Une chaîne RAG minimale se compose en LCEL en cinq lignes ; la fonction qui formate les documents avec leurs pages est ce qui rend la réponse citable.

Module suivant : ajouter la mémoire de conversation pour qu'une question de suivi soit comprise dans son contexte.