RAG Vectoriel : Déployer un Assistant IA Privé avec Ollama et Claude API

Comment construire un système RAG complet qui exploite vos documents internes avec un LLM local (Ollama) et l'API Claude d'Anthropic. Architecture hybride, base vectorielle ChromaDB, chunking intelligent et recherche sémantique — déployé sur votre propre serveur Linux.

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 :

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 :

Astuce : Le fine-tuning reste pertinent pour modifier le comportement d'un modèle (ton, style, format de réponse). Pour injecter des connaissances, le RAG est presque toujours supérieur.

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

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
}'
Astuce GPU : Avec un GPU NVIDIA de 8 Go+ de VRAM, Ollama charge automatiquement le modèle sur le GPU. Sans GPU, les modèles 7B fonctionnent sur CPU avec 16 Go de RAM, mais les réponses sont plus lentes (5-15 secondes vs 1-3 secondes sur GPU).

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
Sécurité : Par défaut, Ollama écoute sur 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")
Astuce : ChromaDB utilise par défaut son propre modèle d'embeddings (all-MiniLM-L6-v2). Pour notre système, nous remplacerons cela par les embeddings d'Ollama (nomic-embed-text), bien plus performants en français.

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)
Attention au chunk_size : Un chunk_size de 800 caracteres est un bon compromis. En dessous de 400, vous perdez trop de contexte. Au-dessus de 1500, la recherche devient moins précise car les chunks mélangent plusieurs sujets. Testez avec vos documents pour trouver l'optimum.

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 !")
Performance : Sur un serveur avec GPU, l'ingestion de 1000 pages PDF prend environ 2 à 5 minutes. Sur CPU, comptez 10 à 20 minutes. Les embeddings sont calculés une seule fois — les requêtes suivantes sont quasi-instantanées.

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
MMR vs Similarite pure : Nous utilisons la recherche MMR (Maximal Marginal Relevance) plutot que la similarité cosinus pure. MMR équilibre pertinence et diversité : si 3 chunks disent la même chose, MMR en sélectionne un et préfère des chunks complémentaires. Cela donne des réponses plus complètes.

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

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']}")
Économie réelle : Sur un usage typique de 500 questions par jour, cette architecture hybride réduit les couts API de 70 à 85% tout en maintenant une qualité de réponse excellente. Les questions simples (80% du volume) sont traitees gratuitement par Ollama, et seules les analyses complexes (20%) passent par Claude API.

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
Sécurité critique : En production, ajoutez impérativement une couche d'authentification (JWT, API keys) sur l'endpoint /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

Précision des réponses

Coûts mensuels typiques

Comparaison : Un service équivalent base uniquement sur l'API Claude (toutes les requêtes en cloud) coûterait environ 450 à 700 EUR/mois pour le même volume. L'architecture hybride divise la facture par 3 à 4 tout en préservant la confidentialité des données.

Cas d'usage déployés

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 :

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.

Discutons de votre projet RAG

Découvrir nos services en Intelligence Artificielle  |  Voir nos réalisations

Davy Abderrahman

Davy Abderrahman

Expert Développeur Full-Stack & IA — Fondateur d'AI Labs Solutions

Spécialiste en développement logiciel, intelligence artificielle et visibilité IA (SEO/GEO/AEO). 20+ ans d'expérience, certifié Microsoft MCSD et Alyra IA.

LinkedIn Profil complet