Python niveau adaptateur · MacBook M2

M3 — Stack on-prem : RAG local

Premier module avec du code Python. Construire un pipeline RAG complet 100 % local — Ollama, Mistral 7B, Phi-4, FAISS, LangChain, Streamlit — et produire une démonstration commerciale prête à l'emploi.

3
Semaines
13
Sessions
S5–S7
Semaines 5 à 7
CAIP D4
Certification préparée

Environnement Python — setup et premiers scripts

Mettre en place l'environnement Python complet et écrire les trois premiers scripts d'adaptation : lire_pdf, appel_ollama, assemblage.

TerminalVS CodeOllama
2h
Durée
Objectif du jour

Mettre en place l'environnement Python complet pour le programme et écrire les trois premiers scripts d'adaptation — lire un fichier, appeler Ollama, sauvegarder un résultat. Le niveau visé est celui de l'adaptateur : comprendre un script existant, modifier ses paramètres, l'assembler avec un autre.

Notions requises en entrée
  • Ollama installé et Mistral 7B fonctionnel sur le M2 (M1-S1-J5)
  • Notions de base du terminal macOS (navigation, création de fichiers)
  • Comprendre ce qu'est une variable, une fonction, une boucle — même approximativement
Déroulé de la session
Installation
25 min
Vérifier et finaliser l'environnement. Python 3.11+ doit être présent. Installer les librairies du programme en une commande.
python3 --version pip3 install langchain langchain-community chromadb pip3 install sentence-transformers pypdf python-dotenv requests pip3 install openai # API compatible OpenAI d'Ollama
Script 1 — lire un PDF
25 min
Adapter ce script pour lire n'importe quel PDF local. Modifier le chemin du fichier, observer la structure du texte extrait.
from pypdf import PdfReader def lire_pdf(chemin): reader = PdfReader(chemin) texte = '' for page in reader.pages: texte += page.extract_text() + '\n' return texte texte = lire_pdf('/Users/philippe/Documents/test.pdf') print(texte[:500])
Script 2 — appeler Ollama
30 min
Ce script appelle Mistral local via l'API compatible OpenAI qu'Ollama expose. Modifier le modèle (mistral, phi4, llama3.2), la question et le système prompt.
from openai import OpenAI client = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama') def interroger_llm(question, modele='mistral'): reponse = client.chat.completions.create( model=modele, messages=[{'role': 'user', 'content': question}] ) return reponse.choices[0].message.content print(interroger_llm('Explique le RGPD en 3 phrases pour une PME'))
Script 3 — assembler
40 min
Assembler les deux scripts : lire un PDF et poser une question dessus à Mistral local. C'est la version la plus simple d'un RAG sans recherche vectorielle. Identifier la limite : que se passe-t-il si le PDF est très long ?
texte_pdf = lire_pdf('/Users/philippe/Documents/test.pdf') texte_court = texte_pdf[:3000] # ~750 tokens environ question = f"""Voici un document :\n{texte_court} \nQuestion : Quels sont les points clés ?""" print(interroger_llm(question))
Notions acquises en sortie
  • Environnement Python prêt avec toutes les librairies du programme M3 installées
  • Trois scripts fonctionnels et adaptés : lire_pdf(), interroger_llm(), assemblage
  • Comprendre la limite fondamentale de l'injection directe : la fenêtre de contexte — motivation du RAG
  • Niveau adaptateur confirmé : savoir modifier un chemin, un modèle, une question dans un script existant
Auto-évaluation
Q1
Qu'est-ce qu'une API compatible OpenAI et pourquoi Ollama en expose-t-il une ?
Q2
Pourquoi ne peut-on pas simplement injecter un document de 50 pages dans le prompt d'un LLM ?
Q3
Dans le script 2, que faut-il modifier pour utiliser Phi-4 au lieu de Mistral ?
Livrable du jour
🐍
Trois scripts Python fonctionnels

lire_pdf.py, appel_ollama.py, assemblage.py — testés sur un PDF réel. Rapport de test : modèle utilisé, temps de réponse mesuré, limite observée sur un long document.

Chunking et embeddings — préparer les documents pour le RAG

Implémenter les deux premières étapes du pipeline RAG : découpage en chunks et vectorisation par embeddings.

PythonOllamanomic-embed-text
2h
Durée
Objectif du jour

Comprendre et implémenter les deux premières étapes du pipeline RAG : le découpage en chunks et la vectorisation par embeddings. Ce sont les étapes les moins intuitives mais les plus importantes pour la qualité des réponses.

Notions requises en entrée
  • Script lire_pdf.py fonctionnel (S5-J1)
  • Pipeline RAG en cinq étapes — intuition (M1-S2-J2)
  • Notion d'encodeur vs décodeur (M1-S1-J2)
Déroulé de la session
Théorie chunking
20 min
Le chunking découpe un document en morceaux (chunks) de taille adaptée. Deux paramètres clés : chunk_size (taille en caractères, typiquement 500 à 1 000) et chunk_overlap (chevauchement entre chunks, typiquement 50 à 200 — évite de couper une phrase à la frontière).
Code chunking
25 min
Adapter ce script de chunking. Expérimenter avec différentes valeurs et observer l'impact sur le nombre de chunks et la cohérence de chacun.
from langchain.text_splitter import RecursiveCharacterTextSplitter from pypdf import PdfReader def charger_et_decouper(chemin, chunk_size=800, chunk_overlap=100): reader = PdfReader(chemin) texte = ''.join(p.extract_text()+'\n' for p in reader.pages) splitter = RecursiveCharacterTextSplitter( chunk_size=chunk_size, chunk_overlap=chunk_overlap, separators=['\n\n', '\n', '.', ' '] ) chunks = splitter.split_text(texte) print(f'{len(chunks)} chunks créés') return chunks
Théorie embeddings
20 min
Un embedding transforme un texte en vecteur numérique qui capture son sens sémantique. Deux textes proches en sens auront des vecteurs proches dans l'espace vectoriel. Pour le on-prem : nomic-embed-text est le modèle d'embedding recommandé via Ollama (léger, performant, français). Installer : ollama pull nomic-embed-text.
Code embeddings
35 min
Vectoriser les chunks et vérifier que deux phrases similaires produisent des vecteurs proches.
from langchain_community.embeddings import OllamaEmbeddings import numpy as np emb = OllamaEmbeddings(model='nomic-embed-text') v1 = emb.embed_query('Le RGPD protège les données personnelles') v2 = emb.embed_query('La protection des données en Europe') v3 = emb.embed_query('La recette du gâteau au chocolat') def sim(a, b): return np.dot(a,b) / (np.linalg.norm(a)*np.linalg.norm(b)) print(f'Phrases proches : {sim(v1,v2):.3f}') # attendu ~0.90 print(f'Phrases éloignées : {sim(v1,v3):.3f}') # attendu ~0.40
Notions acquises en sortie
  • Chunking : paramètres chunk_size et chunk_overlap et leur impact sur la qualité de la recherche
  • Embedding : un texte transformé en vecteur numérique qui capture le sens — pas les mots
  • nomic-embed-text installé et fonctionnel via Ollama — zéro envoi de données
  • Pipeline chunking + embedding fonctionnel et testé sur un document réel
Auto-évaluation
Q1
Pourquoi utilise-t-on un chevauchement (overlap) entre les chunks ?
Q2
Quelle est la différence fondamentale entre un modèle d'embedding et un LLM génératif ?
Q3
Si deux phrases ont une similarité cosinus de 0,92, que cela signifie-t-il sémantiquement ?
Livrable du jour
🐍
Script chunking_embeddings.py fonctionnel

Rapport de test : nombre de chunks produits sur un document réel, valeurs de similarité mesurées entre phrases proches et éloignées.

Base vectorielle FAISS — stocker et interroger les embeddings

Implémenter les troisième et quatrième étapes du pipeline RAG : stocker les embeddings dans FAISS et effectuer une recherche sémantique.

PythonFAISS
2h
Durée
Objectif du jour

Implémenter les troisième et quatrième étapes du pipeline RAG : stocker les embeddings dans une base vectorielle FAISS et effectuer une recherche sémantique. À la fin de cette session, le composant de recherche du RAG est fonctionnel.

Notions requises en entrée
  • Chunks et embeddings fonctionnels (S5-J2)
  • Notion de base vectorielle et de recherche par similarité (M1-S2-J2)
Déroulé de la session
FAISS vs Chroma
15 min
FAISS (Facebook AI Research) : rapide, en mémoire ou sur disque, idéal pour les volumes PME (< 100 000 documents). Chroma : plus facile à utiliser, persistance native. Choix pour ce module : FAISS car plus léger et mieux documenté pour le on-prem.
Indexation FAISS
35 min
Créer la base vectorielle à partir des chunks. Ce script indexe un document et sauvegarde la base sur disque — elle sera rechargée à chaque démarrage sans ré-indexer.
from langchain_community.vectorstores import FAISS from langchain_community.embeddings import OllamaEmbeddings def indexer_document(chemin_pdf, dossier='./index_faiss'): chunks = charger_et_decouper(chemin_pdf) emb = OllamaEmbeddings(model='nomic-embed-text') base = FAISS.from_texts(chunks, emb) base.save_local(dossier) print(f'Index sauvegardé : {len(chunks)} chunks') return base def charger_index(dossier='./index_faiss'): emb = OllamaEmbeddings(model='nomic-embed-text') return FAISS.load_local(dossier, emb, allow_dangerous_deserialization=True)
Recherche sémantique
35 min
Interroger la base vectorielle : pour une question, trouver les k chunks les plus pertinents. La recherche est sémantique — une question sur « protection des données » trouve des chunks contenant « confidentialité » et « RGPD ».
def rechercher(question, base, k=3): resultats = base.similarity_search(question, k=k) return [doc.page_content for doc in resultats] base = charger_index() chunks = rechercher('Quelles sont les obligations de sécurité ?', base) for i, c in enumerate(chunks, 1): print(f'\n--- Chunk {i} ---\n{c[:200]}')
Indexer un dossier
35 min
Adapter le script pour indexer un dossier entier de PDFs — cas d'usage réel PME : toute la documentation interne dans un seul index.
from pathlib import Path def indexer_dossier(dossier_pdfs, index='./index_faiss'): tous_chunks = [] for pdf in Path(dossier_pdfs).glob('*.pdf'): print(f' Traitement : {pdf.name}') tous_chunks.extend(charger_et_decouper(str(pdf))) emb = OllamaEmbeddings(model='nomic-embed-text') base = FAISS.from_texts(tous_chunks, emb) base.save_local(index) print(f'{len(tous_chunks)} chunks indexés') return base
Notions acquises en sortie
  • FAISS : indexation de chunks en base vectorielle, sauvegarde sur disque, rechargement
  • Recherche sémantique fonctionnelle : une question retourne les k chunks les plus pertinents
  • Indexation multi-documents : un dossier entier de PDFs dans un seul index
  • Composant de recherche du RAG complet — prêt à être connecté au LLM
Auto-évaluation
Q1
Quelle est la différence entre une recherche par mots-clés (grep) et une recherche vectorielle sémantique ?
Q2
Pourquoi sauvegarde-t-on l'index FAISS sur disque plutôt que de le recréer à chaque démarrage ?
Q3
Dans le script de recherche, que signifie le paramètre k=3 et quand l'augmenter ?
Livrable du jour
🐍
Script index_faiss.py fonctionnel

Index FAISS créé sur un dossier de PDFs réels. Test de recherche sémantique documenté avec cinq questions et les chunks trouvés.

Pipeline RAG complet — assembler les cinq étapes

Assembler le premier pipeline RAG complet et fonctionnel : lire des documents, indexer, rechercher, générer une réponse avec Mistral local.

PythonOllamaFAISS
2h30
Durée
Objectif du jour

Assembler le premier pipeline RAG complet et fonctionnel : lire des documents, indexer, rechercher, générer une réponse avec Mistral local. C'est le livrable technique central du Module 3 — la démonstration live en rendez-vous commercial.

Notions requises en entrée
  • Scripts lire_pdf.py, chunking_embeddings.py, index_faiss.py fonctionnels (S5-J1 à J3)
  • Script appel_ollama.py fonctionnel (S5-J1)
  • Pipeline RAG en cinq étapes compris (M1-S2-J2)
Déroulé de la session
Architecture finale
15 min
Schéma du pipeline : Documents PDF → Chunking → Embeddings nomic-embed-text (Ollama local) → Index FAISS (disque local) → Question utilisateur → Recherche sémantique (k=3 chunks) → Prompt construit avec les chunks → Mistral 7B (Ollama local) → Réponse. Zéro donnée ne sort du MacBook M2.
Script RAG complet
45 min
Assembler tous les composants dans un script unique rag_local.py.
from langchain_community.vectorstores import FAISS from langchain_community.embeddings import OllamaEmbeddings from langchain.text_splitter import RecursiveCharacterTextSplitter from pypdf import PdfReader from openai import OpenAI from pathlib import Path import os DOSSIER_DOCS = './documents' DOSSIER_INDEX = './index_faiss' MODELE_EMB = 'nomic-embed-text' MODELE_LLM = 'mistral' def obtenir_index(): emb = OllamaEmbeddings(model=MODELE_EMB) if os.path.exists(DOSSIER_INDEX): return FAISS.load_local(DOSSIER_INDEX, emb, allow_dangerous_deserialization=True) chunks = [] for pdf in Path(DOSSIER_DOCS).glob('*.pdf'): reader = PdfReader(str(pdf)) texte = ''.join(p.extract_text()+'\n' for p in reader.pages) sp = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100) chunks.extend(sp.split_text(texte)) base = FAISS.from_texts(chunks, emb) base.save_local(DOSSIER_INDEX) return base def repondre(question, base, k=3): chunks = [d.page_content for d in base.similarity_search(question, k=k)] contexte = '\n---\n'.join(chunks) client = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama') prompt = (f'Contexte :\n{contexte}\n\n' f'Question : {question}\n' 'Réponds uniquement sur la base du contexte fourni.') r = client.chat.completions.create( model=MODELE_LLM, messages=[{'role': 'user', 'content': prompt}]) return r.choices[0].message.content if __name__ == '__main__': base = obtenir_index() print('RAG local prêt. q pour quitter.') while True: q = input('> ') if q.lower() == 'q': break print(repondre(q, base))
Test et ajustements
40 min
Tester avec 10 questions variées : questions factuelles, questions de synthèse, questions hors du corpus (vérifier que le RAG dit « je ne sais pas »). Ajuster chunk_size et k selon la qualité des réponses.
Préparation démo
30 min
Préparer la démo commerciale : choisir 3 à 5 documents PME fictifs pertinents, créer un dossier documents_demo/, re-indexer, préparer cinq questions démonstratives. La démo dure cinq minutes : (1) les documents sont locaux, (2) Ollama tourne localement, (3) aucune donnée ne sort.
Notions acquises en sortie
  • Script rag_local.py fonctionnel — pipeline complet en ~60 lignes de Python
  • RAG local testé sur 10 questions avec documentation des résultats
  • Démo commerciale préparée : 3 à 5 documents PME fictifs, cinq questions démonstratives
  • Paramètres optimaux identifiés (chunk_size, overlap, k)
Auto-évaluation
Q1
Explique le pipeline RAG complet en cinq étapes à un dirigeant PME non-technique.
Q2
Pourquoi le RAG local est-il plus sécurisé qu'envoyer les documents directement dans le prompt d'un LLM cloud ?
Q3
Dans le script rag_local.py, où exactement les données restent-elles sur le MacBook M2 ?
Livrable du jour
🚀
Script rag_local.py complet et commenté

Dossier démo avec 3 à 5 documents PME fictifs et cinq questions démonstratives. Rapport de test (10 questions, qualité des réponses, paramètres retenus).

Interface Streamlit — rendre le RAG utilisable par une PME

Ajouter une interface web minimaliste au RAG local avec Streamlit — transformer le script terminal en une application accessible à tous.

PythonStreamlit
2h
Durée
Objectif du jour

Ajouter une interface web minimaliste au RAG local avec Streamlit — transformer le script en terminal en une application utilisable par n'importe quel employé de PME sans passer par le terminal.

Notions requises en entrée
  • Script rag_local.py fonctionnel (S5-J4)
  • Notion d'interface web (culture générale)
Déroulé de la session
Streamlit — kesako
10 min
Streamlit est une librairie Python qui transforme un script en application web en quelques lignes. Zéro HTML, zéro JavaScript. L'application tourne localement et s'ouvre dans le navigateur. Installation : pip3 install streamlit.
Interface RAG
60 min
Adapter le script rag_local.py en application Streamlit affichant le champ de saisie, la réponse et les chunks sources utilisés — l'utilisateur voit d'où vient la réponse.
import streamlit as st # from rag_local import obtenir_index, repondre st.set_page_config(page_title='Assistant documentaire local', page_icon='🔒') st.title('Assistant documentaire — 100 % local') st.caption('Vos documents restent sur votre machine.') @st.cache_resource def charger_base(): return obtenir_index() base = charger_base() question = st.text_input('Posez votre question sur vos documents :') if question: with st.spinner('Recherche en cours...'): chunks = [d.page_content for d in base.similarity_search(question, k=3)] rep = repondre(question, base) st.markdown('### Réponse') st.write(rep) with st.expander('Sources utilisées'): for i, c in enumerate(chunks, 1): st.markdown(f'**Source {i}**') st.text(c[:300]+'...') # Lancement : streamlit run rag_streamlit.py
Test et finitions
30 min
Lancer l'application : streamlit run rag_streamlit.py. Tester avec les questions de la démo. Vérifier que les sources s'affichent correctement.
Bilan S5
20 min
Bilan de la semaine 5 : les cinq composants du RAG local sont fonctionnels. Identifier les deux points techniques encore flous. Planifier les approfondissements pour S6 : améliorer la qualité du RAG, tester avec Phi-4, ajouter la mémoire de conversation.
Notions acquises en sortie
  • Application Streamlit fonctionnelle : interface web locale pour le RAG
  • Affichage des sources : l'utilisateur voit les chunks qui ont généré la réponse
  • Démo complète et présentable : application + documents PME fictifs + questions démonstratives
Auto-évaluation
Q1
Pourquoi afficher les sources est-il important pour la confiance de l'utilisateur ?
Q2
Qu'est-ce que @st.cache_resource et pourquoi est-il utilisé pour le chargement de l'index ?
Q3
Comment lancer l'application Streamlit et sur quel port s'ouvre-t-elle par défaut ?
Livrable du jour
🖥️
Application rag_streamlit.py complète et fonctionnelle

L'ensemble des scripts S5 (rag_local.py + rag_streamlit.py + documents_demo/) constitue le livrable démontrable n°2 du programme.

Phi-4 — installer et comparer avec Mistral 7B

Installer Phi-4 (14B paramètres, MIT license) et effectuer une comparaison systématique avec Mistral 7B sur les tâches PME.

OllamaPythonPhi-4
2h
Durée
Objectif du jour

Installer Phi-4 (14B paramètres, MIT license) sur le MacBook M2 et effectuer une comparaison systématique avec Mistral 7B sur les tâches PME. Comprendre quand utiliser quel modèle — notamment Phi-4 comme pré-filtre d'anonymisation contextuelle en M4.

Notions requises en entrée
  • Ollama et Mistral 7B fonctionnels (M1-S1-J5)
  • Script appel_ollama.py (S5-J1)
  • Notion de SLM et de taille de modèle (M1-S1-J5)
Déroulé de la session
Installation Phi-4
20 min
Installer Phi-4 via Ollama. Taille : ~9 Go en Q4 quantizé. Vérifier la RAM disponible : Phi-4 nécessite environ 10 à 12 Go de RAM unifiée.
ollama pull phi4 ollama list # vérifier la liste des modèles installés
Benchmark systématique
50 min
Comparer Phi-4 vs Mistral 7B sur six tâches clés. Pour chaque tâche : noter la qualité (1 à 5), la vitesse (tokens/sec observés) et le verdict.
import time from openai import OpenAI client = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama') def tester(modele, prompt, label): t0 = time.time() r = client.chat.completions.create( model=modele, messages=[{'role': 'user', 'content': prompt}]) print(f'{label} — {modele} ({time.time()-t0:.1f}s)') print(r.choices[0].message.content[:200], '\n') taches = [ ('Détection PII', 'Identifie toutes les données personnelles : Jean Martin, joignable au 06 12 34 56 78, jean@dupont.fr'), ('Raisonnement', 'Si A>B et B>C, peut-on conclure A>C ? Explique.'), ('Conseil PME', 'Quels risques RGPD pour une PME qui utilise Claude ?'), ('Code simple', 'Fonction Python qui vérifie si un email est valide.'), ] for label, prompt in taches: for m in ['mistral', 'phi4']: tester(m, prompt, label)
Tableau comparatif
30 min
Construire le tableau comparatif Phi-4 vs Mistral 7B sur les six tâches avec les scores observés. Conclusion : Phi-4 pour le raisonnement et la détection PII contextuelle — Mistral 7B pour la vitesse et les tâches simples de génération.
RAG avec Phi-4
20 min
Modifier rag_local.py pour tester avec Phi-4 : changer MODELE_LLM = 'phi4'. Tester sur les cinq questions de la démo. Comparer la qualité des réponses avec Mistral 7B.
Notions acquises en sortie
  • Phi-4 installé et fonctionnel sur le MacBook M2
  • Tableau comparatif Phi-4 vs Mistral 7B sur six tâches avec scores et verdict
  • Positionnement stratégique : Phi-4 pour la détection PII contextuelle (M4), Mistral pour la génération RAG
  • RAG testé avec Phi-4 — comparaison qualité documentée
Auto-évaluation
Q1
Sur quelle tâche Phi-4 surpasse-t-il clairement Mistral 7B selon ton benchmark ?
Q2
Pourquoi Phi-4 est-il le candidat naturel pour le rôle de pré-filtre d'anonymisation contextuelle en M4 ?
Q3
Sur le M2, quel est le compromis RAM entre Phi-4 et Mistral 7B ?
Livrable du jour
📊
Script benchmark_modeles.py avec résultats documentés

Tableau comparatif Phi-4 vs Mistral 7B (six tâches, scores, verdict).

Mémoire de conversation — rendre le RAG conversationnel

Ajouter la mémoire de conversation au RAG — permettre des échanges multi-tours où chaque nouvelle question tient compte des réponses précédentes.

PythonLangChain
1h45
Durée
Objectif du jour

Ajouter la mémoire de conversation au RAG — permettre des échanges multi-tours où chaque nouvelle question tient compte des réponses précédentes. C'est ce qui transforme un outil de recherche en un véritable assistant.

Notions requises en entrée
  • Script rag_local.py fonctionnel (S5-J4)
  • Notion d'absence de mémoire persistante des LLM (M1-S1-J4)
Déroulé de la session
Le problème
10 min
Sans mémoire, chaque question est indépendante. Si l'utilisateur demande « résume le contrat » puis « quelles sont les pénalités ? », la deuxième question ignore le contexte de la première. La mémoire de conversation injecte l'historique des échanges dans chaque nouveau prompt.
Implémentation
50 min
Adapter rag_local.py avec ConversationBufferWindowMemory et ConversationalRetrievalChain.
from langchain.memory import ConversationBufferWindowMemory from langchain.chains import ConversationalRetrievalChain from langchain_community.llms import Ollama def creer_assistant(dossier_index='./index_faiss', modele='mistral', k_mem=5): emb = OllamaEmbeddings(model='nomic-embed-text') base = FAISS.load_local(dossier_index, emb, allow_dangerous_deserialization=True) llm = Ollama(model=modele) mem = ConversationBufferWindowMemory( memory_key='chat_history', return_messages=True, k=k_mem) return ConversationalRetrievalChain.from_llm( llm=llm, retriever=base.as_retriever(search_kwargs={'k': 3}), memory=mem, verbose=False) assistant = creer_assistant() while True: q = input('Vous : ') if q.lower() == 'q': break r = assistant({'question': q}) print(f'Assistant : {r["answer"]}\n')
Test multi-tours
35 min
Tester une conversation de cinq échanges où chaque question fait référence à la précédente. Documenter comment la mémoire améliore la cohérence.
Notions acquises en sortie
  • ConversationBufferWindowMemory : mémoire glissante sur k échanges précédents
  • ConversationalRetrievalChain : intégration RAG + mémoire dans une chaîne unique
  • Comportement testé sur cinq échanges consécutifs — amélioration de cohérence documentée
Auto-évaluation
Q1
Pourquoi utilise-t-on une fenêtre glissante (k derniers échanges) plutôt que tout l'historique ?
Q2
Que se passe-t-il si la mémoire de conversation est trop longue par rapport à la fenêtre de contexte du LLM ?
Livrable du jour
🐍
Script rag_conversationnel.py fonctionnel

Log d'une conversation de cinq échanges démontrant la cohérence apportée par la mémoire.

Sources et citations — rendre le RAG vérifiable

Ajouter les métadonnées de source (nom du fichier, numéro de page) aux chunks pour que chaque réponse soit vérifiable.

PythonLangChain
1h30
Durée
Objectif du jour

Ajouter les métadonnées de source (nom du fichier, numéro de page) aux chunks pour que chaque réponse du RAG soit vérifiable. Un élément de confiance essentiel en contexte PME.

Notions requises en entrée
  • Script rag_local.py (S5-J4)
  • Notion de chunk et de metadata (S5-J2)
Déroulé de la session
Métadonnées dans les chunks
20 min
Modifier le chargement des documents pour stocker le nom du fichier et le numéro de page dans chaque chunk.
from langchain.schema import Document def charger_avec_metadata(dossier): docs = [] for pdf in Path(dossier).glob('*.pdf'): reader = PdfReader(str(pdf)) sp = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100) for num, page in enumerate(reader.pages): for chunk in sp.split_text(page.extract_text() or ''): docs.append(Document( page_content=chunk, metadata={'source': pdf.name, 'page': num+1})) print(f'{len(docs)} chunks chargés avec métadonnées') return docs
RAG avec sources
40 min
Modifier la fonction de réponse pour retourner les sources avec la réponse.
def repondre_avec_sources(question, base, k=3): resultats = base.similarity_search(question, k=k) contexte = '\n---\n'.join(d.page_content for d in resultats) sources = list({f"{d.metadata['source']} (p.{d.metadata['page']})" for d in resultats}) # ... appel LLM comme dans rag_local.py ... return {'reponse': '...', 'sources': sources}
Streamlit mis à jour
30 min
Mettre à jour l'interface Streamlit pour afficher les sources sous la réponse. L'utilisateur voit « Source : contrat_client.pdf (p. 3) » et peut retrouver le passage original.
Notions acquises en sortie
  • Chunks avec métadonnées : nom du fichier source et numéro de page
  • Réponses sourcées : chaque réponse indique les documents et pages qui l'ont générée
  • Interface Streamlit mise à jour avec affichage des sources
Auto-évaluation
Q1
Pourquoi afficher les sources est-il particulièrement important dans un contexte juridique ou médical ?
Q2
Que se passe-t-il si le RAG répond sur la base d'un chunk dont la source est incorrecte ?
Livrable du jour
🐍
Scripts rag_avec_sources.py et rag_streamlit_v2.py

Test documenté sur cinq questions avec vérification manuelle des sources citées.

Ressources externes

Sécurité du RAG local — ce qui peut mal tourner

Identifier les risques de sécurité spécifiques à un déploiement RAG local et les mesures de protection correspondantes.

ClaudeTests manuels
1h30
Durée
Objectif du jour

Identifier les risques de sécurité spécifiques à un déploiement RAG local et les mesures de protection correspondantes. Ce module nourrit directement la section vecteurs d'attaque de la formation client.

Notions requises en entrée
  • RAG local fonctionnel (S5-J4 à S6-J3)
  • Six vecteurs d'attaque ANSSI (M2-S4-J2)
Déroulé de la session
Quatre risques RAG
25 min
(1) Empoisonnement de l'index : un document externe malveillant injecte des instructions qui seront récupérées lors de la recherche. Mesure : valider les sources d'indexation. (2) Prompt injection via document : un PDF reçu d'un tiers contient des instructions cachées. Mesure : ne pas indexer de documents non vérifiés. (3) Fuite par la réponse : le LLM inclut des informations de chunks non pertinents. Mesure : isolation des index par utilisateur si multi-tenant. (4) Persistance de données sensibles dans l'index : si le dossier index n'est pas protégé. Mesure : chiffrement du dossier index_faiss/.
« Liste les cinq principales menaces de sécurité spécifiques à un déploiement RAG local en PME, avec pour chacune : le scénario d'attaque, la probabilité pour une PME standard et la mesure de protection la plus accessible. »
Test d'empoisonnement
30 min
Reproduire une injection via document : (1) Créer un PDF contenant un texte normal puis une section cachée avec des instructions. (2) Indexer ce document. (3) Poser une question normale. (4) Observer si l'instruction est exécutée. Démo reproductible en formation client.
Mesures de protection
35 min
Implémenter les trois mesures les plus accessibles : (1) Validation des sources avant indexation. (2) Détection de patterns suspects dans les chunks. (3) Log des documents indexés avec hash SHA-256.
import hashlib from pathlib import Path def valider_document(chemin): taille = Path(chemin).stat().st_size if taille > 50*1024*1024: raise ValueError(f'Document trop volumineux : {chemin}') with open(chemin, 'rb') as f: h = hashlib.sha256(f.read()).hexdigest()[:16] print(f'Document validé : {Path(chemin).name} (sha256 : {h})') return True
Notions acquises en sortie
  • Quatre risques spécifiques du RAG local identifiés avec scénario et mesure de protection
  • Expérience directe de l'empoisonnement par document — démo reproductible en formation
  • Trois mesures de protection implémentées dans le script d'indexation
Auto-évaluation
Q1
Qu'est-ce que l'empoisonnement de l'index RAG et comment s'en protéger concrètement ?
Q2
Pourquoi ne faut-il pas indexer automatiquement des PDFs reçus par email sans examen préalable ?
Livrable du jour
🛡️
Script rag_securise.py avec validations d'indexation

Démo d'empoisonnement documentée. Fiche « Quatre risques RAG local et protections » pour la formation client Module 5.

Ressources externes

Consolidation M3 semaines 5-6 — bilan et préparation S7

Consolider tous les acquis techniques des semaines 5 et 6 dans NotebookLM, identifier les fragilités et préparer le plan de la semaine 7.

NotebookLMMistral local
1h30
Durée
Objectif du jour

Consolider tous les acquis techniques des semaines 5 et 6 dans NotebookLM, identifier les fragilités et préparer le plan de la semaine 7.

Notions requises en entrée
  • Tous les scripts et livrables S5-S6
Déroulé de la session
Documentation NotebookLM
25 min
Créer la section « Module 3 — RAG local » dans NotebookLM. Importer les scripts clés (en tant que fichiers texte) et les rapports de test.
Flashcards techniques
20 min
Générer 15 flashcards sur les concepts techniques du M3 : chunking, embedding, FAISS, RAG, Streamlit, sécurité. Focus sur les questions « pourquoi » plutôt que « comment ».
« Génère 15 flashcards sur les concepts du RAG local : chunking, embeddings, base vectorielle, pipeline RAG, mémoire de conversation, sécurité. Les questions doivent être de type CAIP (justification de choix technique, identification de risque, recommandation PME). »
Bilan technique
25 min
Inventaire des sept scripts produits : lire_pdf.py, appel_ollama.py, chunking_embeddings.py, index_faiss.py, rag_local.py, rag_streamlit.py, rag_securise.py. Pour chacun : statut (fonctionnel / à débugger), paramètres à adapter, cas d'usage PME.
Plan S7
20 min
La semaine 7 couvre LangGraph pour un agent simple et la préparation de la démonstration finale complète. Identifier les prérequis techniques à consolider avant S7.
Notions acquises en sortie
  • Section M3 complète dans NotebookLM avec scripts documentés
  • 15 flashcards techniques M3
  • Inventaire des sept scripts avec statut et plan de résolution des problèmes
  • Plan de travail S7 avec prérequis identifiés
Auto-évaluation
Q1
Cite les cinq étapes du pipeline RAG local avec l'outil Python utilisé à chaque étape.
Q2
Quels sont les deux paramètres les plus importants à ajuster pour améliorer la qualité du RAG ?
Livrable du jour
🗂️
Section M3 NotebookLM complète

15 flashcards. Inventaire des sept scripts avec statuts. Plan S7.

Ressources externes

LangGraph — construire un agent simple avec LLM local

Comprendre l'architecture LangGraph et implémenter un agent minimal avec deux outils : recherche RAG et réponse directe — entièrement on-prem.

PythonLangGraphOllama
2h
Durée
Objectif du jour

Comprendre l'architecture LangGraph et implémenter un agent minimal avec deux outils : recherche RAG et réponse directe. L'agent tourne entièrement on-prem — zéro donnée vers l'extérieur. C'est la base du M3b.

Notions requises en entrée
  • RAG local fonctionnel (S5-J4)
  • Architecture d'un agent LLM : boucle percevoir-raisonner-agir (M1-S2-J3)
  • Notion de tool calling (M1-S2-J3)
Déroulé de la session
LangGraph — concepts
20 min
LangGraph modélise un agent comme un graphe d'états : chaque nœud est une action (appel LLM, appel outil, décision), chaque arête est une transition. L'agent parcourt le graphe jusqu'à atteindre un état terminal. Avantage pour la souveraineté : les nœuds peuvent être des LLM locaux via Ollama.
Agent minimal
60 min
Implémenter un agent à deux nœuds : le LLM qui décide (appeler le RAG ou répondre directement) et le RAG qui récupère les informations.
from langgraph.graph import Graph, END from langchain_community.llms import Ollama llm = Ollama(model='mistral') # base = obtenir_index() # depuis rag_local.py def noeud_llm(etat): q = etat['question'] ctx = etat.get('contexte', '') if ctx: p = f'Contexte :\n{ctx}\n\nQuestion : {q}' else: p = f'Question : {q}\nRéponds CHERCHER si tu as besoin des docs.' etat['reponse'] = llm.invoke(p) return etat def noeud_rag(etat): chunks = rechercher(etat['question'], base, k=3) etat['contexte'] = '\n---\n'.join(chunks) return etat def router(etat): return 'rag' if 'CHERCHER' in etat.get('reponse', '') else END g = Graph() g.add_node('llm', noeud_llm) g.add_node('rag', noeud_rag) g.set_entry_point('llm') g.add_conditional_edges('llm', router, {'rag': 'rag', END: END}) g.add_edge('rag', 'llm') agent = g.compile() r = agent.invoke({'question': 'Quelles sont les clauses de confidentialité ?'}) print(r['reponse'])
Test et observation
40 min
Tester l'agent sur cinq questions : deux qui nécessitent le RAG, deux répondables directement, une hors corpus. Observer le comportement de routage : l'agent appelle-t-il le RAG au bon moment ?
Notions acquises en sortie
  • Architecture LangGraph : graphe d'états, nœuds, arêtes, routage conditionnel
  • Agent minimal fonctionnel : LLM local + RAG, zéro envoi externe
  • Comportement de routage observé et documenté
  • Fondation pour M3b : cet agent sera étendu avec d'autres outils
Auto-évaluation
Q1
Quelle est la différence entre une chaîne LangChain (linéaire) et un graphe LangGraph (conditionnel) ?
Q2
Dans l'agent minimal, qu'est-ce qui décide si le RAG est appelé ou non ?
Q3
Comment rendre cet agent entièrement on-prem ? Quels composants doivent rester locaux ?
Livrable du jour
🤖
Script agent_local.py fonctionnel

Log de cinq tests avec décisions de routage documentées.

Cas d'usage PME — adapter le RAG au secteur juridique

Spécialiser le RAG local pour le secteur juridique et générer des documents fictifs avec PII qui serviront de base de test pour M4.

PythonClaude
2h
Durée
Objectif du jour

Spécialiser le RAG local pour un cas d'usage PME réel — le secteur juridique (contrats, actes) — et générer des documents fictifs avec PII qui serviront de base de test pour M4 (Presidio).

Notions requises en entrée
  • RAG local complet et sécurisé (S5-J4 à S6-J4)
  • Connaissance des profils de données par secteur (M2-S3-J1)
  • Arbre de décision de souveraineté (M2-S3-J4)
Déroulé de la session
Spécification sectorielle
15 min
Pour le secteur juridique : les documents sont bien structurés (articles, clauses) mais courts. Paramètres optimaux : chunk_size=600 (les articles de contrat sont courts), chunk_overlap=150 (les références entre articles sont fréquentes), k=4 (les contrats contiennent souvent plusieurs clauses pertinentes).
« Pour un RAG local spécialisé en analyse de contrats juridiques pour cabinet d'avocats, quels sont : les cinq types de documents à indexer en priorité, les cinq questions les plus fréquentes, les spécificités de chunking, les risques RGPD spécifiques et le niveau de garantie recommandé ? »
Documents fictifs PME juridique
30 min
Générer avec Claude trois documents fictifs réalistes contenant des PII fictives — ces documents serviront à tester Presidio en M4.
« Génère un contrat de prestation de services fictif entre "Cabinet Dupont Associés" et "Entreprise Martin SAS" de trois pages. Le contrat doit contenir : des noms de personnes physiques, des adresses, des montants, des IBAN, des dates, des clauses de confidentialité et des clauses de résiliation. Données plausibles mais entièrement fictives. »
Adaptation et test
50 min
Adapter les paramètres du RAG pour les documents juridiques. Indexer les trois documents fictifs. Tester avec cinq questions. Évaluer la qualité des réponses.
Fiche déploiement
25 min
Rédiger la fiche de déploiement pour un cabinet d'avocats : architecture recommandée selon l'arbre de décision, paramètres RAG optimaux, mesures de sécurité spécifiques, coût estimatif, formation nécessaire.
Notions acquises en sortie
  • RAG spécialisé pour le secteur juridique avec paramètres optimaux documentés
  • Trois documents fictifs PME juridique avec PII — base de test réutilisable pour M4
  • Fiche de déploiement sectorielle — template généralisable à tous les secteurs
Auto-évaluation
Q1
Pourquoi un chunk_size plus petit est-il préférable pour des documents juridiques ?
Q2
Pour un cabinet d'avocats avec des données de clients (PII + IP), quel niveau de garantie recommandes-tu ?
Livrable du jour
⚖️
RAG juridique configuré

Paramètres + trois documents fictifs avec PII (réutilisés en M4 pour tester Presidio). Fiche de déploiement cabinet d'avocats.

Démonstration finale M3 — répéter et documenter

Préparer, répéter et documenter la démonstration commerciale complète du Module 3. La démo doit tenir en huit minutes.

StreamlitScripts M3
2h
Durée
Objectif du jour

Préparer, répéter et documenter la démonstration commerciale complète du Module 3. La démo doit tenir en huit minutes et convaincre un dirigeant PME sans compétence technique.

Notions requises en entrée
  • Tous les scripts M3 fonctionnels. Documents fictifs PME (S7-J2).
Déroulé de la session
Script de démo
30 min
Rédiger le script verbal de la démonstration en huit minutes : (1) 1 min — accroche. (2) 2 min — montrer l'architecture (Ollama local, Streamlit local, FAISS sur le disque local). (3) 3 min — démo live sur trois questions. (4) 1 min — couper internet : le RAG fonctionne toujours. (5) 1 min — questions / réponses.
« Aide-moi à rédiger le script verbal d'une démonstration de huit minutes d'un RAG local pour un dirigeant PME non-technique. Message principal : "La puissance de l'IA avec la sécurité d'un déploiement 100 % local." Inclure les transitions, les points d'insistance et les anticipations d'objections. »
Répétition technique
40 min
Répéter la démo trois fois : (1) Solo — vérifier que tout fonctionne, chronométrer. (2) Avec objections simulées — « et si j'ai 10 000 documents ? », « quel est le coût ? », « et si le modèle fait une erreur ? ». (3) Avec un interlocuteur fictif.
Documentation
30 min
Rédiger la fiche technique à laisser au prospect : ce que vous venez de voir (cinq points), comment ça fonctionne (schéma simplifié), ce dont vous avez besoin pour déployer (matériel, temps, budget), les prochaines étapes.
Bilan M3
20 min
Bilan complet du Module 3 : les sept scripts produits, les deux livrables démontrables, les compétences acquises. Identifier ce qui reste à apprendre en M3b.
Notions acquises en sortie
  • Démo commerciale M3 rodée : script verbal, timing, anticipations d'objections
  • Fiche technique prospect (leave-behind) prête à imprimer
  • Bilan M3 complet
Auto-évaluation
Q1
Peux-tu présenter la démo en huit minutes sans notes ?
Q2
Quelles sont les trois objections les plus fréquentes sur le RAG local et tes réponses ?
Livrable du jour
🏆
Démo commerciale M3 complète

Script verbal (huit minutes). Fiche technique prospect. Bilan M3 complet. Le Module 3 est entièrement finalisé — démonstration commerciale opérationnelle.

Ressources externes
← Module 2 Module 3b →