100 % local · terminal · sans API payante

Installer un RAG avec raisonnement IA depuis le terminal

Un guide pas-à-pas pour monter, en ligne de commande, un système de RAG (Retrieval-Augmented Generation) qui interroge vos documents et fait raisonner un modèle local (DeepSeek-R1) avant de répondre. Tout tourne sur votre machine, sans clé API ni cloud.

00Présentation

Comprendre ce que l'on va construire avant de taper la première commande.

Un RAG combine deux briques : une recherche qui retrouve les passages pertinents dans vos propres documents, et un modèle de langage qui rédige une réponse en s'appuyant uniquement sur ces passages. On évite ainsi les hallucinations et on garde la maîtrise des sources.

La nouveauté ici : on utilise un modèle de raisonnement (DeepSeek-R1). Au lieu de répondre directement, il déroule d'abord une chaîne de réflexion interne (balises <think>) puis formule sa réponse finale. On affichera les deux séparément dans le terminal.

i
La pile retenue est entièrement gratuite et locale : Ollama (moteur de modèles), DeepSeek-R1 (raisonnement), nomic-embed-text (vectorisation), ChromaDB (base vectorielle) et Python pour orchestrer le tout.

01Architecture

Le chemin d'une question, de votre clavier jusqu'à la réponse raisonnée.

Vos documentsPDF / TXT / MD
Découpagechunks
Embeddingsnomic-embed-text
ChromaDBbase vectorielle
Question (CLI)
Recherchetop-k passages
DeepSeek-R1<think> + réponse
Réponse + sources

Les trois premières cases (en haut) constituent la phase d'ingestion, faite une seule fois. La ligne du bas, c'est la phase de requête, rejouée à chaque question.

02Prérequis

Ce qu'il faut avoir avant de commencer.

  • Python 3.10 ou supérieur — vérifiez avec python3 --version.
  • 8 Go de RAM minimum (16 Go recommandés) pour faire tourner un modèle 7B confortablement.
  • ~6 Go d'espace disque pour les modèles (R1 7B ≈ 4,7 Go, embeddings ≈ 270 Mo).
  • Un terminal — Linux, macOS ou Windows (via WSL2 de préférence).
  • Aucune carte graphique n'est obligatoire : ça tourne sur CPU, simplement plus lentement.
!
Sans GPU, comptez quelques dizaines de secondes par réponse avec un modèle 7B. Pour tester rapidement, utilisez la variante deepseek-r1:1.5b (voir l'étape 05).

Étape 1Environnement Python

On isole le projet dans un environnement virtuel pour ne rien polluer.

terminalbash
# Créer le dossier du projet
mkdir rag-reason && cd rag-reason

# Créer et activer l'environnement virtuel
python3 -m venv .venv

# Linux / macOS
source .venv/bin/activate

# Windows (PowerShell)
.venv\Scripts\Activate.ps1

Une fois activé, votre invite de commande affiche (.venv) au début de la ligne. C'est le signe que vous travaillez dans l'environnement isolé.

Étape 2Installer Ollama

Ollama est le moteur qui télécharge et fait tourner les modèles localement.

terminalbash
# Linux / macOS — script d'installation officiel
curl -fsSL https://ollama.com/install.sh | sh

# Vérifier que le service répond
ollama --version

Sur Windows, téléchargez l'installeur depuis ollama.com/download. Une fois lancé, Ollama tourne en arrière-plan et expose une API locale sur http://localhost:11434.

i
Si ollama serve ne tourne pas automatiquement, ouvrez un terminal dédié et lancez-le. Laissez-le ouvert pendant toute la suite.

Étape 3Télécharger les modèles

Deux modèles : un pour raisonner, un pour vectoriser le texte.

terminalbash
# Modèle de RAISONNEMENT (≈ 4,7 Go)
ollama pull deepseek-r1:7b

# Modèle d'EMBEDDINGS pour la recherche (≈ 270 Mo)
ollama pull nomic-embed-text

# Lister ce qui est installé
ollama list
!
Machine modeste ? Remplacez par deepseek-r1:1.5b — beaucoup plus rapide, raisonnement plus léger. Pensez à reporter le même nom dans le fichier config.py de l'étape 07.

Étape 4Dépendances Python

Les bibliothèques qui orchestrent l'ingestion, la recherche et l'affichage.

requirements.txttext
ollama==0.4.7
chromadb==0.5.23
pypdf==5.1.0
rich==13.9.4
terminalbash
pip install -r requirements.txt

ollama parle au moteur, chromadb stocke les vecteurs, pypdf lit les PDF et rich donne une jolie interface en couleurs dans le terminal.

Étape 5Structure du projet

Quatre fichiers Python et un dossier pour vos documents.

rag-reason/tree
rag-reason/
├── config.py        # réglages centraux
├── ingest.py        # lit les docs → base vectorielle
├── engine.py        # recherche + raisonnement
├── cli.py           # interface en ligne de commande
├── requirements.txt
├── documents/       # déposez vos PDF / TXT / MD ici
└── chroma_db/       # créé automatiquement
terminalbash
mkdir documents
# Déposez au moins un fichier de test, par ex. documents/notes.txt

config.py

config.pypython
# Réglages centraux du projet
LLM_MODEL = "deepseek-r1:7b"      # modèle de raisonnement
EMBED_MODEL = "nomic-embed-text"  # modèle d'embeddings

DOCS_DIR = "documents"           # dossier des sources
DB_DIR = "chroma_db"             # base vectorielle persistée
COLLECTION = "connaissances"

CHUNK_SIZE = 800                 # caractères par fragment
CHUNK_OVERLAP = 120              # chevauchement entre fragments
TOP_K = 4                        # passages récupérés par question

Étape 6Ingestion des documents

Découper vos fichiers, les vectoriser, et les ranger dans ChromaDB.

ingest.pypython
import os, glob, uuid
import ollama, chromadb
from pypdf import PdfReader
import config as cfg


def lire_fichier(chemin):
    """Extrait le texte brut d'un PDF, TXT ou MD."""
    if chemin.endswith(".pdf"):
        lecteur = PdfReader(chemin)
        return "\n".join(page.extract_text() or "" for page in lecteur.pages)
    with open(chemin, encoding="utf-8") as f:
        return f.read()


def decouper(texte, taille, chevauchement):
    """Découpe le texte en fragments qui se chevauchent."""
    morceaux, i = [], 0
    while i < len(texte):
        morceaux.append(texte[i:i + taille])
        i += taille - chevauchement
    return [m.strip() for m in morceaux if m.strip()]


def embed(texte):
    """Transforme un texte en vecteur via Ollama."""
    r = ollama.embeddings(model=cfg.EMBED_MODEL, prompt=texte)
    return r["embedding"]


def main():
    client = chromadb.PersistentClient(path=cfg.DB_DIR)
    # On repart d'une base propre à chaque ingestion complète
    try:
        client.delete_collection(cfg.COLLECTION)
    except Exception:
        pass
    col = client.create_collection(cfg.COLLECTION)

    fichiers = glob.glob(os.path.join(cfg.DOCS_DIR, "**/*"), recursive=True)
    fichiers = [f for f in fichiers if f.endswith((".pdf", ".txt", ".md"))]
    if not fichiers:
        print("Aucun document dans", cfg.DOCS_DIR); return

    total = 0
    for chemin in fichiers:
        texte = lire_fichier(chemin)
        morceaux = decouper(texte, cfg.CHUNK_SIZE, cfg.CHUNK_OVERLAP)
        for m in morceaux:
            col.add(
                ids=[str(uuid.uuid4())],
                embeddings=[embed(m)],
                documents=[m],
                metadatas=[{"source": os.path.basename(chemin)}],
            )
        total += len(morceaux)
        print(f"  {chemin} → {len(morceaux)} fragments")

    print(f"\nTerminé : {total} fragments indexés.")


if __name__ == "__main__":
    main()

Lancez l'ingestion :

terminalbash
python ingest.py
# → documents/notes.txt → 12 fragments
# → Terminé : 12 fragments indexés.
i
À relancer chaque fois que vous ajoutez ou modifiez des documents. La base est persistée dans chroma_db/ : pas besoin de réindexer à chaque démarrage.

Étape 7Moteur RAG + raisonnement

Le cœur du système : retrouver les passages, faire raisonner R1, séparer réflexion et réponse.

engine.pypython
import re
import ollama, chromadb
import config as cfg

_client = chromadb.PersistentClient(path=cfg.DB_DIR)


def _embed(texte):
    return ollama.embeddings(model=cfg.EMBED_MODEL, prompt=texte)["embedding"]


def rechercher(question, k=cfg.TOP_K):
    """Retrouve les k passages les plus proches de la question."""
    col = _client.get_collection(cfg.COLLECTION)
    res = col.query(query_embeddings=[_embed(question)], n_results=k)
    passages = res["documents"][0]
    sources = [m["source"] for m in res["metadatas"][0]]
    return passages, sources


def construire_prompt(question, passages):
    contexte = "\n\n---\n\n".join(passages)
    return f"""Tu es un assistant rigoureux. Réponds à la QUESTION
en t'appuyant UNIQUEMENT sur le CONTEXTE ci-dessous.
Si le contexte ne suffit pas, dis-le clairement.

CONTEXTE :
{contexte}

QUESTION : {question}"""


def repondre(question):
    """Renvoie (raisonnement, reponse, sources)."""
    passages, sources = rechercher(question)
    prompt = construire_prompt(question, passages)

    sortie = ollama.chat(
        model=cfg.LLM_MODEL,
        messages=[{"role": "user", "content": prompt}],
    )["message"]["content"]

    # DeepSeek-R1 place sa réflexion entre <think>...</think>
    m = re.search(r"<think>(.*?)</think>", sortie, re.DOTALL)
    raisonnement = m.group(1).strip() if m else ""
    reponse = re.sub(r"<think>.*?</think>", "", sortie, flags=re.DOTALL).strip()

    return raisonnement, reponse, sorted(set(sources))
i
La clé du « raisonnement » : DeepSeek-R1 émet spontanément un bloc <think> contenant son cheminement, puis sa réponse. On les sépare avec une simple expression régulière pour les afficher distinctement.

Étape 8Interface CLI

Une boucle de questions/réponses colorée, directement dans le terminal.

cli.pypython
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
import engine

console = Console()


def main():
    console.print(Panel.fit(
        "[bold cyan]RAG + Raisonnement[/]\n"
        "[dim]Tapez votre question — 'exit' pour quitter.[/]",
        border_style="cyan"))

    while True:
        question = console.input("\n[bold green]?[/] ").strip()
        if question.lower() in {"exit", "quit", "q"}:
            break
        if not question:
            continue

        with console.status("[cyan]Recherche et raisonnement…[/]"):
            raisonnement, reponse, sources = engine.repondre(question)

        if raisonnement:
            console.print(Panel(raisonnement, title="🧠 Raisonnement",
                                border_style="yellow", expand=False))
        console.print(Panel(Markdown(reponse), title="✅ Réponse",
                            border_style="green"))
        console.print(f"[dim]Sources : {', '.join(sources)}[/]")


if __name__ == "__main__":
    main()

Étape 9Lancer & tester

Le moment de vérité.

terminalbash
# 1) Vérifier qu'Ollama tourne (sinon : ollama serve)
ollama list

# 2) Indexer vos documents
python ingest.py

# 3) Démarrer l'assistant
python cli.py

Exemple de session dans le terminal :

sessionoutput
╭─ RAG + Raisonnement ─────────────────────────╮
│ Tapez votre question — 'exit' pour quitter.  │
╰──────────────────────────────────────────────╯

? Quelle est la politique de remboursement ?

╭─ 🧠 Raisonnement ────────────────────────────╮
│ Le contexte mentionne un délai de 30 jours    │
│ et exige un justificatif d'achat. Je combine  │
│ ces deux éléments pour la réponse.            │
╰──────────────────────────────────────────────╯
╭─ ✅ Réponse ─────────────────────────────────╮
│ Le remboursement est possible sous 30 jours,  │
│ sur présentation d'un justificatif d'achat.   │
╰──────────────────────────────────────────────╯
Sources : notes.txt
Ça fonctionne ! Vous avez un RAG local qui cite ses sources et expose son raisonnement, le tout sans aucun service en ligne.

12Réglages avancés

Quelques leviers pour améliorer la qualité ou la vitesse.

  • Précision vs vitesse — augmentez TOP_K (5–6) pour donner plus de contexte au modèle ; réduisez-le si les réponses se diluent.
  • Granularité — des CHUNK_SIZE plus petits (400–600) ciblent mieux les réponses courtes ; plus grands (1000+) conservent davantage de contexte narratif.
  • Modèle plus légerdeepseek-r1:1.5b répond en quelques secondes sur CPU.
  • Température — passez options={"temperature": 0.2} à ollama.chat pour des réponses plus déterministes.
  • Masquer le raisonnement — n'affichez pas le panneau jaune si vous ne voulez que la réponse finale.

13Dépannage

Les erreurs les plus courantes et leur solution.

×
Connection refused (port 11434) — Ollama n'est pas démarré. Lancez ollama serve dans un terminal séparé.
×
model not found — le modèle n'est pas téléchargé. Refaites ollama pull deepseek-r1:7b et vérifiez que le nom dans config.py correspond.
×
Collection does not exist — vous interrogez avant d'avoir indexé. Lancez d'abord python ingest.py.
!
Réponses lentes — normal sur CPU avec un 7B. Basculez sur deepseek-r1:1.5b ou utilisez une machine avec GPU.

14Pour aller plus loin

Des évolutions naturelles une fois la base maîtrisée.

  • Mémoire de conversation — conservez l'historique des messages pour des échanges multi-tours.
  • Re-ranking — ajoutez un second modèle qui reclasse les passages récupérés avant la génération.
  • Interface web — exposez engine.repondre() via FastAPI ou Streamlit.
  • Découpage sémantique — remplacez le découpage par caractères par un découpage par phrases/paragraphes.
  • Filtrage par métadonnées — interrogez ChromaDB en filtrant par source, date ou type de document.