📍 Guide principal : Développer avec l’API Google Gemini — le RAG est la voie pour répondre sur vos propres données.
Un modèle de langage ne connaît que ce qu’il a vu pendant son entraînement. Il ignore le contenu de votre documentation interne, de vos contrats, de votre base de connaissances. Lui poser une question dessus, c’est l’inviter à inventer. La solution standard porte un nom : RAG, pour retrieval-augmented generation — génération augmentée par la récupération. L’idée tient en une phrase : avant de répondre, on retrouve dans vos documents les passages pertinents, et on les fournit au modèle comme contexte. Il ne devine plus, il s’appuie sur vos textes. Ce tutoriel construit un mini moteur de questions-réponses sur une base de connaissances, de la vectorisation jusqu’à la réponse ancrée.
Ce que vous allez apprendre
- Comprendre le principe du RAG et ce que sont des embeddings.
- Transformer vos documents en vecteurs avec le modèle d’embedding de Gemini.
- Mesurer la proximité de sens entre deux textes avec la similarité cosinus.
- Retrouver les passages les plus pertinents pour une question donnée.
- Générer une réponse ancrée uniquement sur le contexte récupéré.
Ce que vous allez construire
Un assistant qui répond aux questions sur une petite base de connaissances — par exemple les conditions de service d’une entreprise. Vous tapez « quel est le délai de livraison ? » ; le système retrouve le passage qui en parle, le donne au modèle, et obtient une réponse fidèle, sans hallucination. La même architecture, branchée sur une vraie base de vecteurs, alimente des assistants documentaires en production.
Prérequis
- L’environnement du premier tutoriel avec
google-genai. - La bibliothèque NumPy :
pip install numpy. - Des notions de listes et de fonctions Python.
- ⏱️ Temps estimé : environ 35 minutes.
Étape 1 — Embeddings et similarité, l’intuition
Tout repose sur une idée : transformer un texte en une liste de nombres — un vecteur — qui capture son sens. C’est ce qu’on appelle un embedding. La propriété magique : deux textes de sens proche produisent des vecteurs proches dans l’espace, même s’ils n’emploient pas les mêmes mots. « Quand serai-je livré ? » et « délai d’expédition » n’ont aucun mot en commun, mais leurs vecteurs se ressemblent, parce que leur sens se ressemble.
Pour répondre à une question sur vos documents, on procède donc ainsi : on calcule le vecteur de chaque document à l’avance, puis le vecteur de la question, et on cherche les documents dont le vecteur est le plus proche de celui de la question. C’est de la recherche par le sens, pas par les mots-clés — une différence de nature avec une recherche textuelle classique, qui raterait « expédition » quand l’utilisateur tape « livraison ».
Étape 2 — Préparer la base de connaissances
Notre base sera une simple liste de courts passages. En conditions réelles, ces passages viendraient de vos documents découpés en morceaux ; ici, on les écrit à la main pour se concentrer sur la mécanique.
documents = [
"Les commandes sont livrees sous 3 a 5 jours ouvres dans les grandes villes.",
"Le paiement est accepte par carte, virement et paiement mobile.",
"Tout produit peut etre retourne sous 14 jours s'il est intact.",
"Le service client repond du lundi au vendredi, de 9h a 18h.",
"La livraison est gratuite a partir de 50 000 FCFA d'achat.",
]
Chaque entrée est un fragment autonome, compréhensible isolément. C’est un point important du RAG : on indexe des morceaux assez petits pour être précis, mais assez complets pour avoir du sens seuls. Un découpage trop fin perd le contexte ; trop gros, il noie l’information utile. Pour ce tutoriel, ces cinq phrases suffisent à tout illustrer.
✅ Point d’étape — Vous avez une liste de chaînes en mémoire. Rien d’autre n’est requis à ce stade.
Étape 3 — Vectoriser les documents
On calcule maintenant le vecteur de chaque document avec le modèle gemini-embedding-001. Un réglage compte : le type de tâche. Pour des documents destinés à être recherchés, on indique RETRIEVAL_DOCUMENT ; cela optimise les vecteurs pour la récupération.
from dotenv import load_dotenv
from google import genai
from google.genai import types
load_dotenv()
client = genai.Client()
reponse = client.models.embed_content(
model="gemini-embedding-001",
contents=documents,
config=types.EmbedContentConfig(
task_type="RETRIEVAL_DOCUMENT",
output_dimensionality=768,
),
)
vecteurs_docs = [e.values for e in reponse.embeddings]
print("Nombre de vecteurs :", len(vecteurs_docs))
print("Dimension d'un vecteur :", len(vecteurs_docs[0]))
On passe toute la liste documents en une fois ; response.embeddings renvoie un vecteur par document, qu’on récupère via .values. Le paramètre output_dimensionality=768 demande des vecteurs de 768 nombres : un bon compromis entre précision et légèreté, bien plus maniable que la taille maximale. À l’exécution, vous obtenez cinq vecteurs de dimension 768.
✅ Point d’étape — Vous voyez « Nombre de vecteurs : 5 » et « Dimension : 768 ». En production, on calcule ces vecteurs une seule fois et on les stocke, pour ne pas les recalculer à chaque question.
Étape 4 — Mesurer la proximité de sens
Pour comparer deux vecteurs, on utilise la similarité cosinus : elle vaut 1 quand deux vecteurs pointent dans la même direction (sens très proche), 0 quand ils sont perpendiculaires (sans rapport). Elle se calcule comme le produit scalaire divisé par le produit des longueurs — et ce dénominateur joue un rôle précieux : il normalise les vecteurs au passage, ce qui nous évite tout souci de mise à l’échelle lié à la dimension choisie.
import numpy as np
def similarite_cosinus(a, b):
a = np.array(a)
b = np.array(b)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
Cette petite fonction est le cœur de la recherche. np.dot calcule le produit scalaire, np.linalg.norm la longueur de chaque vecteur. Mathématiquement, ce score varie de -1 à 1 ; mais pour des embeddings de texte, qui pointent rarement en sens opposé, on le lit en pratique entre 0 et 1 : plus il est haut, plus les deux textes parlent de la même chose. Parce qu’on divise par les longueurs, peu importe que les vecteurs aient été tronqués à 768 dimensions : la mesure reste juste.
✅ Point d’étape — La fonction renvoie 1.0 (ou presque) quand on lui passe deux fois le même vecteur. Testez-le pour vous en assurer.
Étape 5 — Retrouver les passages pertinents
On vectorise la question, cette fois avec le type RETRIEVAL_QUERY — le pendant côté requête du type utilisé pour les documents — puis on classe les documents par similarité décroissante et on garde les meilleurs.
def recuperer(question, k=2):
rep = client.models.embed_content(
model="gemini-embedding-001",
contents=question,
config=types.EmbedContentConfig(
task_type="RETRIEVAL_QUERY",
output_dimensionality=768,
),
)
vecteur_question = rep.embeddings[0].values
scores = []
for i, vecteur_doc in enumerate(vecteurs_docs):
score = similarite_cosinus(vecteur_question, vecteur_doc)
scores.append((score, documents[i]))
scores.sort(reverse=True)
return [texte for _, texte in scores[:k]]
passages = recuperer("Au bout de combien de temps serai-je livre ?")
for p in passages:
print("-", p)
La fonction calcule le vecteur de la question, le compare à chaque vecteur de document, trie par score et renvoie les k meilleurs. Sur « au bout de combien de temps serai-je livré ? », elle remonte le passage sur les délais de livraison en tête — alors même que la question n’emploie pas le mot « délai ». C’est la recherche sémantique en action : on a trouvé par le sens. Utiliser RETRIEVAL_QUERY pour la question et RETRIEVAL_DOCUMENT pour les documents n’est pas un détail : c’est ce qui aligne au mieux les deux côtés de la comparaison.
✅ Point d’étape — Le premier passage retourné concerne bien la livraison. Sinon, vérifiez que vous avez bien utilisé deux types de tâche distincts pour la question et les documents.
Étape 6 — Générer une réponse ancrée
Dernière étape, la plus satisfaisante : on donne au modèle la question et les passages récupérés, avec une consigne stricte de ne répondre qu’à partir de ce contexte. C’est ce qui transforme une recherche en réponse, et ce qui bride les inventions.
def repondre(question):
passages = recuperer(question)
contexte = "\n".join(passages)
instruction = (
"Tu reponds uniquement a partir du CONTEXTE ci-dessous. "
"Si la reponse ne s'y trouve pas, dis-le clairement.\n\n"
f"CONTEXTE :\n{contexte}\n\nQUESTION : {question}"
)
rep = client.models.generate_content(
model="gemini-2.5-flash",
contents=instruction,
)
return rep.text
print(repondre("Au bout de combien de temps serai-je livre ?"))
print(repondre("Acceptez-vous le paiement mobile ?"))
print(repondre("Quelle est votre politique sur les animaux ?"))
Les deux premières questions reçoivent une réponse exacte, tirée des passages. La troisième — sur un sujet absent de la base — reçoit un honnête « cette information ne figure pas dans le contexte ». C’est précisément le comportement recherché : le modèle s’appuie sur vos données et reconnaît ses limites au lieu d’inventer. Vous tenez là un assistant documentaire complet, en une cinquantaine de lignes.
✅ Point d’étape — Les réponses présentes dans la base sont correctes, et la question hors-sujet est refusée proprement. C’est le signe que l’ancrage fonctionne.
Une question légitime : pourquoi monter un RAG plutôt qu’activer l’ancrage sur la recherche Google, évoqué dans le guide principal ? Les deux ancrent la réponse sur des faits, mais leurs sources diffèrent. L’ancrage sur la recherche puise dans le web public et convient aux questions d’actualité générale. Le RAG, lui, puise dans vos documents — privés, internes, propres à votre métier — auxquels la recherche web n’a pas accès. On choisit le RAG dès que la connaissance utile n’est pas publique. Et rien n’interdit de combiner les deux selon la nature de la question.
Étape 7 — Passer à l’échelle
Notre base tient en mémoire et on compare la question à chaque document un par un. Cela suffit pour quelques dizaines de passages, pas pour des dizaines de milliers. À l’échelle, on stocke les vecteurs dans une base spécialisée qui retrouve les plus proches sans tout parcourir. L’extension pgvector de PostgreSQL est une option éprouvée et accessible ; l’article PostgreSQL pgvector pour le RAG montre comment la mettre en place. La logique reste identique : on y range les vecteurs de l’étape 3, et la base remplace notre boucle de l’étape 5.
Bien découper ses documents
Dans un vrai projet, vos sources ne sont pas cinq phrases mais des pages entières. La qualité du RAG dépend alors fortement de la façon dont on les découpe en morceaux — l’étape qu’on appelle le chunking. Un découpage trop grossier (des pages entières) noie l’information précise dans du bruit : le passage récupéré contient la réponse, mais aussi beaucoup de hors-sujet. Un découpage trop fin (une phrase isolée) perd le contexte : « il est de 14 jours » ne veut rien dire sans la phrase qui précède. La pratique consiste à viser des morceaux de l’ordre d’un paragraphe, parfois avec un léger chevauchement d’une phrase entre morceaux voisins pour ne pas couper une idée en deux. On découpe de préférence aux frontières naturelles — titres, paragraphes — plutôt qu’à un nombre de caractères arbitraire. Ce travail de préparation, peu spectaculaire, fait souvent plus pour la qualité des réponses que le choix du modèle lui-même.
🐞 Pièges fréquents
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Récupération peu pertinente | Même type de tâche pour question et documents. | Utiliser RETRIEVAL_QUERY pour la question, RETRIEVAL_DOCUMENT pour les documents. |
| Le modèle répond hors contexte | Instruction d’ancrage trop molle. | Renforcer : « réponds uniquement à partir du contexte, sinon dis que tu ne sais pas ». |
| Scores de similarité aberrants | Comparaison de vecteurs de dimensions différentes. | Utiliser la même output_dimensionality partout. |
| Passages trop longs ou trop courts | Découpage des documents mal calibré. | Viser des morceaux autonomes de quelques phrases. |
| Lenteur sur grande base | Comparaison linéaire en mémoire. | Passer à une base de vecteurs (pgvector ou équivalent). |
✅ Récapitulatif
Vous avez construit un système qui répond sur vos propres données. Vous savez ce qu’est un embedding et pourquoi des sens proches donnent des vecteurs proches ; vous vectorisez documents et questions avec les bons types de tâche ; vous mesurez la proximité par la similarité cosinus ; vous récupérez les meilleurs passages et vous générez une réponse strictement ancrée sur eux. C’est le pattern qui sous-tend la majorité des assistants documentaires d’entreprise, et vous en tenez la version complète, prête à brancher sur une vraie base de vecteurs.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
embed_content(model="gemini-embedding-001", contents, config) |
Calculer des vecteurs. |
EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT") |
Vectoriser des documents à indexer. |
EmbedContentConfig(task_type="RETRIEVAL_QUERY") |
Vectoriser une question. |
response.embeddings[i].values |
Récupérer un vecteur. |
| Similarité cosinus | Mesurer la proximité de sens (0 à 1). |
💪 À vous de jouer
Ajoutez à la base un passage sur les horaires d’ouverture, puis posez une question reformulée (« êtes-vous ouverts le samedi ? ») et vérifiez que le système retrouve le bon passage et répond correctement, y compris quand l’information est partielle.
Voir une solution
# 1. Ajouter le passage a la liste 'documents', PUIS recalculer 'vecteurs_docs'
# avec le bloc de l'etape 3 (sinon le nouveau passage n'est pas indexe).
documents.append("La boutique est ouverte du lundi au samedi, dimanche ferme.")
# ... relancer l'embedding des documents ...
print(repondre("Etes-vous ouverts le samedi ?"))
# Reponse attendue : oui, ouvert le samedi (le dimanche etant ferme).
Le piège classique : ajouter un document sans recalculer les vecteurs. La base de vecteurs et la liste de textes doivent toujours rester synchronisées.
Tutoriels suivants
- Sorties structurées JSON — renvoyer la réponse et ses sources dans un format exploitable.
- Chatbot en streaming Node.js — exposer cet assistant derrière une interface web.
Pour aller plus loin
🔝 Retour au guide principal de l’API Gemini. La documentation officielle détaille les types de tâche d’embedding et les dimensions recommandées.
Questions fréquentes
Pourquoi ne pas tout mettre dans le contexte du modèle ? Sur une petite base, on peut. Mais au-delà d’un certain volume, c’est coûteux et l’information utile se dilue. Le RAG ne fournit que les passages pertinents, ce qui est plus précis et plus économique.
Faut-il recalculer les vecteurs des documents à chaque question ? Non. On les calcule une fois et on les stocke ; seule la question est vectorisée à chaque requête.
Quelle dimension choisir ? 768 est un bon point de départ. On peut monter à 1536 ou 3072 pour un peu plus de finesse, au prix de vecteurs plus lourds à stocker.
Le RAG empêche-t-il toute hallucination ? Il la réduit fortement en ancrant la réponse, mais une instruction claire et une vérification des passages restent nécessaires sur les sujets sensibles.