Un RAG (retrieval-augmented generation) répond à une question à partir de vos documents plutôt que de la seule mémoire du modèle. Tout peut tourner sur la machine : Ollama pour les embeddings et la génération, Chroma pour la base vectorielle.
Le principe en cinq étapes
- Découper les documents en morceaux.
- Vectoriser chaque morceau avec un modèle d’embeddings.
- Indexer vecteurs, textes et métadonnées dans une base vectorielle.
- Récupérer les morceaux les plus proches de la question, vectorisée avec le même modèle.
- Générer la réponse en donnant ces morceaux au LLM comme contexte.
Les embeddings avec Ollama
POST /api/embed prend un model et un input, chaîne ou liste de chaînes, et renvoie un tableau embeddings. La documentation donne deux règles : utiliser le même modèle pour indexer et pour interroger, et préférer la similarité cosinus pour la recherche sémantique. Les vecteurs renvoyés sont normalisés L2, et leur longueur dépend du modèle. Les exemples utilisent embeddinggemma, à télécharger d’abord :
ollama pull embeddinggemma
Le code
Chroma s’installe avec pip install chromadb. PersistentClient écrit l’index sur disque et le recharge au démarrage. On lui passe nos propres vecteurs, et l’espace cosine se déclare à la création de la collection : la valeur par défaut est l2. Attention, get_or_create_collection ignore ses arguments si la collection existe déjà : une collection créée en l2 le reste.
import requests
import chromadb
OLLAMA = "http://localhost:11434"
def embed(textes):
r = requests.post(f"{OLLAMA}/api/embed",
json={"model": "embeddinggemma", "input": textes})
r.raise_for_status()
return r.json()["embeddings"]
client = chromadb.PersistentClient(path="./index")
col = client.get_or_create_collection(
name="docs", configuration={"hnsw": {"space": "cosine"}})
# morceaux : liste de (id, texte, fichier source), produite par votre découpage
col.upsert(
ids=[m[0] for m in morceaux],
embeddings=embed([m[1] for m in morceaux]),
documents=[m[1] for m in morceaux],
metadatas=[{"source": m[2]} for m in morceaux],
)
question = "Quelle est la procédure de redémarrage ?"
res = col.query(query_embeddings=embed([question]), n_results=4)
extraits, metas = res["documents"][0], res["metadatas"][0]
contexte = "\n\n".join(f"[{i}] {t}" for i, t in enumerate(extraits, 1))
r = requests.post(f"{OLLAMA}/api/chat", json={
"model": "gemma4",
"stream": False,
"messages": [
{"role": "system", "content": "Réponds uniquement à partir du contexte. "
"Indique le numéro des extraits utilisés. Si la réponse n'y est pas, dis-le."},
{"role": "user", "content": f"Contexte :\n{contexte}\n\nQuestion : {question}"},
],
})
print(r.json()["message"]["content"])
for i, m in enumerate(metas, 1):
print(f"[{i}] {m['source']}")
Sans n_results, Chroma renvoie 10 résultats par requête. Le cookbook Hugging Face note qu’augmenter ce nombre améliore les chances de ramener le bon passage, mais qu’un contexte trop chargé noie le modèle.
Limite 1 : le découpage
Le cookbook Hugging Face le résume : des morceaux trop petits tronquent les idées, trop grands les diluent. Il conseille un chevauchement entre morceaux voisins, pour ne pas couper une idée en deux, et le fixe arbitrairement, selon ses propres termes, à un dixième de leur taille. Il découpe enfin de façon récursive, par séparateurs, ce qui préserve la structure.
Limite 2 : les citations
Le prompt demande de citer les extraits, mais rien ne l’impose : dans le cookbook lui-même, aucune des réponses d’exemple ne cite ses documents. La liste fiable est celle que le code affiche à partir des métadonnées des morceaux récupérés. Stockez-y au minimum le fichier source, et la page ou la section si possible.
Limite 3 : la mise à jour
L’index ne suit pas vos fichiers tout seul. Dans Chroma, add ignore sans erreur un id déjà présent : pour réindexer un document modifié, upsert crée ou met à jour. Un document supprimé se retire avec col.delete(where={"source": ...}). Changer de modèle d’embeddings impose de tout réindexer : Chroma lève une exception si la dimension des vecteurs ne correspond pas à celle de la collection. Et l’espace de distance ne figure pas parmi les réglages modifiables après création.
Open WebUI intègre son propre RAG (billet dédié) ; l’exposition réseau d’Ollama est traitée ici.
Ce qu’il faut retenir
- Découper, vectoriser, indexer, récupérer, générer : le même modèle d’embeddings à l’indexation et à la requête.
/api/embedrenvoie des vecteurs normalisés L2 et tronque par défaut les entrées trop longues : tailles mesurées en tokens.- Dans Chroma, déclarer l’espace
cosineà la création et réindexer avecupsert, pasadd. - Les citations sûres viennent des métadonnées, pas de la bonne volonté du modèle.
In English
Chunk, embed with /api/embed, index in Chroma, retrieve, generate. A local RAG in one page of Python, and its three weak points: chunking, citations and updates. The same model must serve indexing and querying, Chroma's add silently ignores an existing id while upsert updates it, and reliable citations come from metadata.
Sources & vérification
Relevé sur : Documentation Ollama, Chroma et Hugging Face, relevé 2026-10
- Ollama, Embeddings docs.ollama.com
- Ollama, référence API (docs/api.md) github.com
- Ollama, CLI reference docs.ollama.com
- Chroma, Getting Started docs.trychroma.com
- Chroma, Clients (PersistentClient) docs.trychroma.com
- Chroma, Configure (espace HNSW) docs.trychroma.com
- Chroma, Manage Collections docs.trychroma.com
- Chroma, Add Data docs.trychroma.com
- Chroma, Update Data (upsert) docs.trychroma.com
- Chroma, Delete Data docs.trychroma.com
- Chroma, Query and Get docs.trychroma.com
- Hugging Face Cookbook, Advanced RAG huggingface.co
