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éthode | Rô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(leskplus proches),mmr(diversité maximale),similarity_score_threshold(rejette sous le seuil).search_kwargs:kle nombre de morceaux à retourner (typiquement 3 à 6),score_thresholdpour couper les résultats faibles,filterpour 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.
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
VectorStoreuniformiseadd_documents,similarity_searchetas_retriever;Chromalocal convient pour prototyper et déployer à taille d'équipe. - Le récupérateur est un
Runnable:k,score_thresholdetfiltersur métadonnées gouvernent la qualité de la remontée. MultiQueryRetrieverrattrape les écarts de formulation en générant plusieurs variantes de la question, au prix d'un appelLLMsupplémentaire.- Une chaîne RAG minimale se compose en
LCELen 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.