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.
01Architecture
Le chemin d'une question, de votre clavier jusqu'à la réponse raisonnée.
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.
deepseek-r1:1.5b (voir l'étape 05).Étape 1Environnement Python
On isole le projet dans un environnement virtuel pour ne rien polluer.
# 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.
# 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.
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.
# 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
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.
ollama==0.4.7
chromadb==0.5.23
pypdf==5.1.0
rich==13.9.4
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/
├── 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
mkdir documents
# Déposez au moins un fichier de test, par ex. documents/notes.txt
config.py
# 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.
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 :
python ingest.py
# → documents/notes.txt → 12 fragments
# → Terminé : 12 fragments indexés.
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.
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))
<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.
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é.
# 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 :
╭─ 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
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_SIZEplus petits (400–600) ciblent mieux les réponses courtes ; plus grands (1000+) conservent davantage de contexte narratif. - Modèle plus léger —
deepseek-r1:1.5brépond en quelques secondes sur CPU. - Température — passez
options={"temperature": 0.2}àollama.chatpour 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.
ollama serve dans un terminal séparé.ollama pull deepseek-r1:7b et vérifiez que le nom dans config.py correspond.python ingest.py.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.