1. Introduction — Pourquoi le RAG change la donne en 2026
Les grands modèles de langage (LLM) sont devenus extraordinairement puissants. Claude, GPT, Llama, Mistral… ils rédigent, analysent, codent et raisonnent avec une précision qui était inimaginable il y a trois ans. Mais ils ont tous un angle mort critique : ils ne connaissent pas vos données.
Votre documentation interne, vos procédures métier, vos rapports d'audit, vos contrats, vos bases de connaissances propriétaires — tout cela est invisible pour un LLM standard. Vous pouvez poser la question la plus pertinente du monde, le modèle vous répondra avec des connaissances génériques, ou pire, il halluciner une réponse qui semble crédible mais qui est fausse.
Le RAG (Retrieval-Augmented Génération) résout ce problème de manière élégante. Au lieu de fine-tuner un modèle (coûteux, complexe, périssable), le RAG injecté dynamiquement les informations pertinentes de vos documents dans le contexte du LLM au moment de la requête. C'est la différence entre un expert qui a mémorisé un livre il y a six mois et un expert qui a le livre ouvert devant lui.
En 2026, le RAG n'est plus un concept expérimental. C'est une architecture de production déployée par des entreprises de toutes tailles — des startups aux groupes du CAC 40. Et avec des outils comme Ollama (LLM local, gratuit, privé) et l'API Claude d'Anthropic (raisonnement avancé), vous pouvez construire un assistant IA qui :
- Connaît vos documents — Il répond en citant vos sources internes
- Reste privé — Les données sensibles ne quittent jamais votre serveur (via Ollama)
- S'adapté en temps réel — Ajoutez un document, il est immédiatement accessible
- Coûte une fraction du fine-tuning — Pas de GPU A100, pas d'entraînement de plusieurs jours
Dans ce guide, nous allons construire ce système de A a Z, avec du vrai code Python fonctionnel que vous pouvez déployer aujourd'hui.
2. Qu'est-ce que le RAG ?
Le principe en une phrase
Le RAG est une architecture qui augmente les réponses d'un LLM en y injectant des informations récupérées depuis une base de connaissances externe. Au lieu de compter uniquement sur les connaissances figées du modèle, on lui fournit du contexte frais et pertinent a chaque requête.
Schema conceptuel du pipeline RAG
┌─────────────────────────────────────────────────────────────────┐
│ PIPELINE RAG COMPLET │
│ │
│ [Documents] ──▶ [Chunking] ──▶ [Embeddings] ──▶ [VectorDB]
│ PDF, DOCX, Decoupage Vectorisation ChromaDB
│ TXT, HTML intelligent (nomic-embed) Stockage
│ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ [Question] ──▶ [Embedding] ──▶ [Recherche] ──▶ [Contexte]
│ Utilisateur Vectorisation Cosinus Top-K docs │
│ de la query Similarite │
│ │
│ [Contexte + Question] ──▶ [LLM] ──▶ [Reponse source] │
│ Prompt enrichi Ollama/Claude Avec references │
└─────────────────────────────────────────────────────────────────┘
RAG vs Fine-tuning : pourquoi le RAG gagne
Le fine-tuning consiste a ré-entraîner un modèle sur vos données. Le RAG, lui, fournit les données au moment de la requête. Voici pourquoi le RAG est souvent le meilleur choix :
- Mise à jour instantanée — Ajoutez un document, il est disponible immédiatement. Avec le fine-tuning, il faut ré-entraîner (heures ou jours).
- Traçabilité — Le RAG cite ses sources. Vous savez exactement d'ou vient l'information. Le fine-tuning est une boîte noire.
- Coût — Le RAG fonctionne avec n'importe quel LLM standard. Le fine-tuning nécessite des GPU puissants et du temps ingénieur.
- Pas d'hallucination structurelle — Le LLM répond sur la base de vrais documents, pas de souvenirs approximatifs.
- Multi-domaines — Un seul système RAG peut couvrir RH, juridique, technique, commercial… Il suffit d'ajouter les documents.
3. Architecture du système
Notre système RAG complet repose sur cinq étapes distinctes, chacune jouant un rôle critique dans la qualité des réponses finales.
Étape 1 : Ingestion
Les documents sources (PDF, DOCX, TXT, HTML, Markdown) sont lus et convertis en texte brut. C'est l'entrée du pipeline. La qualité de l'extraction détermine la qualité de tout le reste.
Étape 2 : Chunking (Découpage)
Les documents sont découpés en chunks (morceaux) de taille optimale. Trop petit, on perd le contexte. Trop grand, on dilue la pertinence. Un bon chunking vise 500 à 1000 tokens avec un chevauchement (overlap) de 100 à 200 tokens pour préserver la continuité sémantique.
Étape 3 : Vectorisation (Embeddings)
Chaque chunk est transformé en un vecteur numérique de haute dimension (768 ou 1024 dimensions) par un modèle d'embeddings. Ces vecteurs capturent le sens sémantique du texte : deux phrases qui disent la même chose auront des vecteurs proches, même si les mots sont différents.
Étape 4 : Stockage vectoriel
Les vecteurs sont stockés dans une base de données vectorielle (ChromaDB dans notre cas). Cette base permet des recherches par similarité cosinus extrêmement rapides, même avec des millions de documents.
Étape 5 : Recherche et Génération
Quand l'utilisateur pose une question, elle est vectorisée avec le même modèle d'embeddings, puis comparée aux vecteurs stockés. Les chunks les plus similaires sont récupérés et injectés dans le prompt du LLM, qui génère une réponse sourcée.
Stack technique retenue
- LLM local : Ollama (llama3, mistral, qwen2.5)
- LLM cloud : Claude API (Anthropic) pour les requêtes complexes
- Embeddings : nomic-embed-text (via Ollama, local)
- Base vectorielle : ChromaDB
- Orchestration : LangChain (Python)
- Serveur : Linux (Ubuntu 22.04+)
- Conteneurisation : Docker Compose
4. Installer Ollama sur Linux
Ollama est un outil open-source qui permet de faire tourner des LLM localement avec une simplicité remarquable. Pas besoin de configurer CUDA manuellement, de télécharger des poids depuis Hugging Face ou de gérer les quantizations — Ollama fait tout ça pour vous.
Installation en une commande
# Installation d'Ollama (Linux / macOS)
curl -fsSL https://ollama.com/install.sh | sh
# Verifier l'installation
ollama --version
# Le service demarre automatiquement
systemctl status ollama
Télécharger les modèles
Pour notre système RAG, nous avons besoin d'un modèle de langage et d'un modèle d'embeddings :
# Modeles de langage (choisissez selon votre GPU/RAM)
ollama pull llama3.1:8b # 4.7 GB - Excellent rapport qualite/taille
ollama pull mistral:7b # 4.1 GB - Tres bon en francais
ollama pull qwen2.5:7b # 4.4 GB - Performant en multilangue
# Modele d'embeddings (indispensable pour le RAG)
ollama pull nomic-embed-text # 274 MB - Embeddings de qualite
# Lister les modeles installes
ollama list
Test rapide
# Test en ligne de commande
ollama run llama3.1:8b "Explique le RAG en 3 phrases."
# Test de l'API REST (port 11434 par defaut)
curl http://localhost:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "Qu est-ce que le RAG en intelligence artificielle ?",
"stream": false
}'
Configuration avancée
# Fichier /etc/systemd/system/ollama.service.d/override.conf
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
# Recharger et redemarrer
sudo systemctl daemon-reload
sudo systemctl restart ollama
localhost uniquement. Si vous exposez le port 11434, placez-le derrière un reverse proxy avec authentification. Ne laissez jamais l'API Ollama ouverte sur Internet sans protection.
5. Configurer la base vectorielle ChromaDB
ChromaDB est une base de données vectorielle open-source, légère et performante. Elle est parfaitement adaptée aux projets RAG car elle gere nativement les embeddings, les métadonnées et la recherche par similarité.
Installation
# Installer ChromaDB et les dependances
pip install chromadb langchain langchain-community langchain-chroma
# Verifier l'installation
python -c "import chromadb; print(chromadb.__version__)"
Création d'une collection et insertion de documents
import chromadb
from chromadb.config import Settings
# Initialiser le client ChromaDB (persistant sur disque)
client = chromadb.PersistentClient(
path="./chroma_db",
settings=Settings(anonymized_telemetry=False)
)
# Creer une collection pour nos documents
collection = client.get_or_create_collection(
name="documents_entreprise",
metadata={
"hnsw:space": "cosine", # Metrique de similarite
"hnsw:M": 16, # Precision de l'index
"hnsw:construction_ef": 200 # Qualite de construction
}
)
# Inserer des documents manuellement (pour comprendre le mecanisme)
collection.add(
ids=["doc1", "doc2", "doc3"],
documents=[
"Notre politique de teletravail autorise 3 jours par semaine.",
"Les conges annuels sont de 25 jours ouvrables pour tous les salaries.",
"La procedure d'onboarding dure 2 semaines avec un mentor designe."
],
metadatas=[
{"source": "politique_rh.pdf", "page": 5, "type": "rh"},
{"source": "convention_collective.pdf", "page": 12, "type": "rh"},
{"source": "processus_onboarding.pdf", "page": 1, "type": "rh"}
]
)
print(f"Collection creee avec {collection.count()} documents.")
Recherche par similarité
# Rechercher les documents les plus pertinents
results = collection.query(
query_texts=["Combien de jours de conges ai-je droit ?"],
n_results=3,
include=["documents", "metadatas", "distances"]
)
for i, doc in enumerate(results["documents"][0]):
distance = results["distances"][0][i]
source = results["metadatas"][0][i]["source"]
print(f"[{i+1}] Score: {1 - distance:.3f} | Source: {source}")
print(f" {doc}\n")
6. Pipeline d'ingestion de documents
Le pipeline d'ingestion est le cœur du système RAG. C'est lui qui transformé vos documents bruts en vecteurs interrogeables. La qualité de cette étape détermine directement la pertinence des réponses.
Installation des dépendances
# Toutes les dependances necessaires
pip install langchain langchain-community langchain-chroma
pip install langchain-ollama anthropic
pip install pypdf python-docx unstructured
pip install tiktoken sentence-transformers
Charger des documents (PDF, DOCX, TXT)
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
TextLoader,
DirectoryLoader
)
import os
def load_documents(directory: str) -> list:
"""Charge tous les documents d'un repertoire."""
documents = []
# Charger les PDF
pdf_loader = DirectoryLoader(
directory,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True
)
documents.extend(pdf_loader.load())
# Charger les DOCX
docx_loader = DirectoryLoader(
directory,
glob="**/*.docx",
loader_cls=Docx2txtLoader,
show_progress=True
)
documents.extend(docx_loader.load())
# Charger les fichiers texte
txt_loader = DirectoryLoader(
directory,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
loader_kwargs={"encoding": "utf-8"}
)
documents.extend(txt_loader.load())
print(f"{len(documents)} documents charges depuis {directory}")
return documents
# Utilisation
docs = load_documents("./documents_entreprise/")
Chunking intelligent avec chevauchement
from langchain.text_splitter import RecursiveCharacterTextSplitter
def chunk_documents(documents: list, chunk_size: int = 800,
chunk_overlap: int = 150) -> list:
"""Decoupe les documents en chunks avec chevauchement."""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=[
"\n\n", # Paragraphes
"\n", # Lignes
". ", # Phrases
", ", # Propositions
" ", # Mots
"" # Caracteres (dernier recours)
],
is_separator_regex=False
)
chunks = text_splitter.split_documents(documents)
# Enrichir les metadonnees de chaque chunk
for i, chunk in enumerate(chunks):
chunk.metadata["chunk_id"] = i
chunk.metadata["chunk_size"] = len(chunk.page_content)
# Garder le nom du fichier source
source = chunk.metadata.get("source", "inconnu")
chunk.metadata["filename"] = os.path.basename(source)
print(f"{len(documents)} documents decoupes en {len(chunks)} chunks")
print(f"Taille moyenne : {sum(len(c.page_content) for c in chunks) // len(chunks)} caracteres")
return chunks
# Utilisation
chunks = chunk_documents(docs, chunk_size=800, chunk_overlap=150)
Générer les embeddings et stocker dans ChromaDB
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
def create_vector_store(chunks: list, persist_dir: str = "./chroma_db") -> Chroma:
"""Cree la base vectorielle a partir des chunks."""
# Utiliser les embeddings d'Ollama (local, gratuit, prive)
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url="http://localhost:11434"
)
# Creer la base vectorielle ChromaDB
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=persist_dir,
collection_name="rag_documents",
collection_metadata={"hnsw:space": "cosine"}
)
print(f"Base vectorielle creee : {len(chunks)} vecteurs stockes dans {persist_dir}")
return vector_store
# Pipeline complet d'ingestion
docs = load_documents("./documents_entreprise/")
chunks = chunk_documents(docs)
vector_store = create_vector_store(chunks)
print("Pipeline d'ingestion termine avec succes !")
7. Recherche sémantique et génération
Maintenant que nos documents sont vectorisés et stockés, nous pouvons construire le cœur du système : la chaîne de recherche et génération. C'est ici que la magie du RAG prend forme.
Chaîne RAG avec Ollama (LLM local)
from langchain_ollama import OllamaLLM, OllamaEmbeddings
from langchain_chroma import Chroma
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA
def create_rag_chain(persist_dir: str = "./chroma_db",
model: str = "llama3.1:8b"):
"""Cree une chaine RAG complete avec Ollama."""
# Charger les embeddings
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url="http://localhost:11434"
)
# Charger la base vectorielle existante
vector_store = Chroma(
persist_directory=persist_dir,
embedding_function=embeddings,
collection_name="rag_documents"
)
# Configurer le retriever
retriever = vector_store.as_retriever(
search_type="mmr", # Maximal Marginal Relevance
search_kwargs={
"k": 5, # Nombre de chunks retournes
"fetch_k": 20, # Candidats avant re-ranking
"lambda_mult": 0.7 # Diversite (0=max diversite, 1=max pertinence)
}
)
# Prompt template optimise pour le RAG
prompt_template = """Tu es un assistant IA expert qui repond aux questions
en te basant UNIQUEMENT sur le contexte fourni ci-dessous.
REGLES :
- Reponds en francais
- Cite les sources (nom de fichier, page) quand c'est possible
- Si l'information n'est pas dans le contexte, dis-le clairement
- Ne fabrique JAMAIS d'information
- Sois precis et structure ta reponse
CONTEXTE :
{context}
QUESTION : {question}
REPONSE :"""
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# Initialiser le LLM local
llm = OllamaLLM(
model=model,
base_url="http://localhost:11434",
temperature=0.1, # Basse temperature pour la precision
num_ctx=4096, # Fenetre de contexte
num_predict=1024 # Longueur max de la reponse
)
# Creer la chaine RAG
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": prompt}
)
return qa_chain
# Utilisation
rag = create_rag_chain()
# Poser une question
result = rag.invoke({"query": "Quelle est la politique de teletravail ?"})
print("REPONSE :")
print(result["result"])
print("\nSOURCES :")
for doc in result["source_documents"]:
print(f" - {doc.metadata.get('filename', 'N/A')} (p.{doc.metadata.get('page', '?')})")
Recherche sémantique avancée avec scores
def search_with_scores(query: str, vector_store, k: int = 5):
"""Recherche semantique avec scores de similarite."""
results = vector_store.similarity_search_with_relevance_scores(
query=query,
k=k
)
print(f"Resultats pour : '{query}'\n")
for doc, score in results:
filename = doc.metadata.get("filename", "N/A")
page = doc.metadata.get("page", "?")
print(f"[Score: {score:.4f}] {filename} (p.{page})")
print(f" {doc.page_content[:200]}...")
print()
# Filtrer par seuil de pertinence
relevant = [(doc, score) for doc, score in results if score > 0.3]
print(f"{len(relevant)}/{len(results)} documents au-dessus du seuil de pertinence (0.3)")
return relevant
8. Architecture hybride : Ollama + Claude API
Voici l'architecture qui fait la différence. Au lieu de choisir entre un LLM local (rapide, privé, gratuit) et un LLM cloud (plus intelligent, payant), nous utilisons les deux avec un routage intelligent.
Le principe du routage
- Ollama (local) — pour les questions factuelles simples, les recherches dans la documentation, les réponses courtes. Gratuit, privé, rapide.
- Claude API (cloud) — pour l'analyse complexe, la synthese de plusieurs documents, le raisonnement multi-étapes, les comparaisons. Plus coûteux mais nettement plus intelligent.
Routeur intelligent
import anthropic
from langchain_ollama import OllamaLLM
class HybridRAGRouter:
"""Routeur intelligent qui choisit entre Ollama et Claude API."""
def __init__(self, vector_store):
self.vector_store = vector_store
# LLM local (Ollama)
self.local_llm = OllamaLLM(
model="llama3.1:8b",
base_url="http://localhost:11434",
temperature=0.1,
num_ctx=4096
)
# LLM cloud (Claude API)
self.claude_client = anthropic.Anthropic(
api_key="votre-cle-api-anthropic" # ou variable d'env
)
# Mots-cles indiquant une requete complexe
self.complex_keywords = [
"compare", "analyse", "synthese", "resume",
"avantages et inconvenients", "strategie",
"recommande", "evaluer", "critique", "planifier"
]
def classify_query(self, query: str) -> str:
"""Determine si la requete necessite Claude ou Ollama."""
query_lower = query.lower()
# Critere 1 : mots-cles de complexite
if any(kw in query_lower for kw in self.complex_keywords):
return "claude"
# Critere 2 : longueur de la question
if len(query.split()) > 30:
return "claude"
# Critere 3 : question avec plusieurs sous-questions
if query.count("?") > 1:
return "claude"
return "ollama"
def retrieve_context(self, query: str, k: int = 5) -> str:
"""Recupere le contexte pertinent depuis la base vectorielle."""
docs = self.vector_store.similarity_search(query, k=k)
context_parts = []
for doc in docs:
source = doc.metadata.get("filename", "source inconnue")
page = doc.metadata.get("page", "?")
context_parts.append(
f"[Source: {source}, p.{page}]\n{doc.page_content}"
)
return "\n\n---\n\n".join(context_parts)
def query_ollama(self, query: str, context: str) -> dict:
"""Genere une reponse avec Ollama (local)."""
prompt = f"""Contexte :\n{context}\n\nQuestion : {query}\n\nReponse :"""
response = self.local_llm.invoke(prompt)
return {
"answer": response,
"model": "ollama/llama3.1:8b",
"cost": 0.0,
"private": True
}
def query_claude(self, query: str, context: str) -> dict:
"""Genere une reponse avec Claude API (cloud)."""
message = self.claude_client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{
"role": "user",
"content": f"""Tu es un assistant expert. Reponds a la question
en te basant UNIQUEMENT sur le contexte fourni.
Cite tes sources. Reponds en francais.
CONTEXTE :
{context}
QUESTION : {query}"""
}]
)
response_text = message.content[0].text
# Estimation du cout (approximatif)
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.003 + output_tokens * 0.015) / 1000
return {
"answer": response_text,
"model": "claude-sonnet-4-20250514",
"cost": cost,
"private": False
}
def ask(self, query: str) -> dict:
"""Point d'entree principal : route et repond."""
# 1. Classifier la requete
route = self.classify_query(query)
# 2. Recuperer le contexte
context = self.retrieve_context(query)
# 3. Generer la reponse avec le bon modele
if route == "claude":
result = self.query_claude(query, context)
else:
result = self.query_ollama(query, context)
result["route"] = route
result["context_chunks"] = len(context.split("---"))
return result
Utilisation du routeur hybride
# Initialiser le routeur
router = HybridRAGRouter(vector_store=vector_store)
# Question simple → Ollama (local, gratuit)
result1 = router.ask("Combien de jours de teletravail par semaine ?")
print(f"Modele: {result1['model']} | Cout: {result1['cost']:.4f}€")
print(f"Prive: {result1['private']}")
print(f"Reponse: {result1['answer']}\n")
# Question complexe → Claude API (cloud, intelligent)
result2 = router.ask(
"Compare notre politique de teletravail avec les meilleures pratiques "
"du secteur et recommande des ameliorations."
)
print(f"Modele: {result2['model']} | Cout: {result2['cost']:.4f}€")
print(f"Prive: {result2['private']}")
print(f"Reponse: {result2['answer']}")
API FastAPI pour exposer le système
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import uvicorn
app = FastAPI(title="RAG Assistant API", version="1.0.0")
# Initialiser le routeur au demarrage
router = None
@app.on_event("startup")
async def startup():
global router
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url="http://localhost:11434"
)
vector_store = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
collection_name="rag_documents"
)
router = HybridRAGRouter(vector_store=vector_store)
class QueryRequest(BaseModel):
question: str
force_model: str | None = None # "ollama" ou "claude" pour forcer
class QueryResponse(BaseModel):
answer: str
model: str
route: str
cost: float
private: bool
context_chunks: int
@app.post("/ask", response_model=QueryResponse)
async def ask_question(request: QueryRequest):
if router is None:
raise HTTPException(status_code=503, detail="Service not ready")
if request.force_model:
# Permettre de forcer un modele specifique
context = router.retrieve_context(request.question)
if request.force_model == "claude":
result = router.query_claude(request.question, context)
else:
result = router.query_ollama(request.question, context)
result["route"] = f"forced:{request.force_model}"
result["context_chunks"] = len(context.split("---"))
else:
result = router.ask(request.question)
return QueryResponse(**result)
@app.get("/health")
async def health():
return {"status": "ok", "models": ["ollama/llama3.1:8b", "claude-sonnet-4-20250514"]}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
9. Sécurisation et déploiement
Un système RAG en production doit etre securise, monitore et facilement deployable. Voici une configuration Docker Compose complète avec reverse proxy Nginx et SSL.
Structure du projet
rag-assistant/
├── docker-compose.yml
├── nginx/
│ └── nginx.conf
├── app/
│ ├── main.py # API FastAPI
│ ├── rag_router.py # Routeur hybride
│ ├── ingestion.py # Pipeline d'ingestion
│ └── requirements.txt
├── documents/ # Vos documents source
├── chroma_db/ # Base vectorielle (persistent)
└── .env # Variables d'environnement
Fichier docker-compose.yml
version: "3.8"
services:
# === LLM LOCAL (Ollama) ===
ollama:
image: ollama/ollama:latest
container_name: rag-ollama
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
interval: 30s
timeout: 10s
retries: 3
# === API RAG (FastAPI) ===
rag-api:
build: ./app
container_name: rag-api
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- CHROMA_PERSIST_DIR=/data/chroma_db
volumes:
- ./chroma_db:/data/chroma_db
- ./documents:/data/documents
depends_on:
ollama:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 15s
timeout: 5s
retries: 3
# === REVERSE PROXY (Nginx) ===
nginx:
image: nginx:alpine
container_name: rag-nginx
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
rag-api:
condition: service_healthy
restart: unless-stopped
volumes:
ollama_data:
Configuration Nginx avec SSL
# nginx/nginx.conf
events {
worker_connections 1024;
}
http {
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# Upstream
upstream rag_backend {
server rag-api:8000;
}
# Redirection HTTP → HTTPS
server {
listen 80;
server_name rag.votre-domaine.fr;
return 301 https://$host$request_uri;
}
# Serveur HTTPS
server {
listen 443 ssl http2;
server_name rag.votre-domaine.fr;
ssl_certificate /etc/letsencrypt/live/rag.votre-domaine.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rag.votre-domaine.fr/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Securite
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# API endpoint
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://rag_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts pour les requetes LLM (peuvent etre longues)
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
}
}
Variables d'environnement (.env)
# .env - NE JAMAIS commiter ce fichier
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxxxxxx
OLLAMA_MODEL=llama3.1:8b
EMBEDDING_MODEL=nomic-embed-text
CHROMA_COLLECTION=rag_documents
LOG_LEVEL=INFO
MAX_CONTEXT_CHUNKS=5
RATE_LIMIT_PER_MINUTE=60
Demarrage du système
# Cloner et configurer
cp .env.example .env
# Editer .env avec votre cle API Anthropic
# Lancer le systeme
docker compose up -d
# Telecharger les modeles Ollama dans le conteneur
docker exec rag-ollama ollama pull llama3.1:8b
docker exec rag-ollama ollama pull nomic-embed-text
# Verifier que tout tourne
docker compose ps
curl -k https://rag.votre-domaine.fr/api/health
/ask. Sans cela, n'importe qui peut interroger vos documents internes. Pensez egalement a chiffrer la base ChromaDB au repos si les documents sont sensibles.
10. Résultats et performances
Nous avons déployé cette architecture pour plusieurs clients d'AI Labs Solutions. Voici les métriques réelles observées en production.
Benchmarks de temps de réponse
- Ollama (llama3.1:8b, GPU RTX 4070) — Temps moyen : 1.8 secondes (ingestion du contexte + génération)
- Ollama (llama3.1:8b, CPU Xeon) — Temps moyen : 8.2 secondes
- Claude API (claude-sonnet-4-20250514) — Temps moyen : 2.5 secondes (réseau inclus)
- Recherche vectorielle ChromaDB — Temps moyen : 15 ms pour 50 000 chunks
Précision des réponses
- Taux de réponses pertinentes : 94% (contre 67% sans RAG, sur les memes questions)
- Taux d'hallucination : Réduit de 31% à 4% grâce au contexte injecté
- Citations correctes : 89% des réponses citent la bonne source
Coûts mensuels typiques
- Serveur Linux (GPU RTX 4070) : ~120 EUR/mois chez un hébergeur cloud
- API Claude (500 requêtes complexes/jour) : ~45 EUR/mois
- Ollama : 0 EUR (open-source, tourne sur votre serveur)
- ChromaDB : 0 EUR (open-source, stockage disque inclus dans le serveur)
- Total : ~165 EUR/mois pour un assistant IA privé complet
Cas d'usage déployés
- Assistant RH interne — 2000+ pages de documentation RH, convention collective, procédures. Les salariés obtiennent des réponses instantanées avec les références exactes du document source.
- Support technique produit — Manuels techniques, FAQ, tickets résolus. Réduction de 60% du temps de résolution de premier niveau.
- Analyse de contrats — Extraction d'informations spécifiques dans des contrats de 50+ pages en quelques secondes.
- Base de connaissances réglementaire — Textes de loi, jurisprudence, normes ISO. Réponses sourcées avec numéro d'article.
11. Conclusion
Le RAG vectoriel n'est plus une technologie expérimentale. C'est une architecture de production éprouvée qui permet à n'importe quelle organisation d'exploiter la puissance des LLM sur ses propres données, en toute confidentialité.
L'architecture hybride Ollama + Claude API que nous avons construite dans ce guide offre le meilleur des deux mondes :
- Confidentialité totale pour les requêtes courantes (Ollama local)
- Intelligence de pointe pour les analyses complexes (Claude API)
- Coûts maîtrisés grâce au routage intelligent
- Mise à jour instantanée des connaissances sans re-entraînement
- Traçabilité complète avec citations des sources
Chez AI Labs Solutions, nous déployons cette architecture pour des entreprises de toutes tailles — PME industrielles, cabinets de conseil, établissements publics. Chaque système est adapté aux besoins spécifiques : choix des modèles, stratégie de chunking, règles de routage, intégration avec les outils existants (Slack, Teams, CRM).
Vos données sont votre avantage concurrentiel. Le RAG est la clé pour les exploiter avec l'IA, sans jamais les perdre de vue.
Vous souhaitez déployer un assistant IA privé pour votre organisation ? Nous concevons, développons et déployons des systèmes RAG clé en main, de l'audit documentaire initial jusqu'au monitoring en production.
Découvrir nos services en Intelligence Artificielle | Voir nos réalisations