MonEspaceSanté FAQ — Mistral Small 3.2 24B (QLoRA, GGUF)
Assistant conversationnel
francophone, spécialisé sur la FAQ du service public
Mon espace santé. Le modèle est un
fine-tuning
QLoRA de
Mistral Small 3.2 24B Instruct (2506), fusionné en pleine précision puis
exporté au format
GGUF pour une inférence locale (llama.cpp / Ollama).
- Quantification fournie ici :
Q4_K_M (≈ 14,3 Go) — le meilleur compromis
taille / qualité / vitesse, recommandé pour un usage mono-utilisateur sur GPU 24–32 Go.
- Langue : français uniquement.
- Domaine : questions des usagers sur Mon espace santé (création/opposition de profil,
dossier médical, ajout de documents, messagerie sécurisée, confidentialité, accès des
professionnels, etc.).
Ce dépôt ne contient que le décodeur de texte quantifié. Le modèle de base
Mistral Small 3.2 est multimodal ; le mode vision a été retiré à l'export car
inutile pour un assistant FAQ textuel.
Utilisation
Ollama (recommandé)
Le system prompt et les hyperparamètres d'inférence sont embarqués dans le Modelfile :
inutile d'ajouter un quelconque pré-prompt, il suffit de poser la question.
1# Modelfile
2FROM ./faqmes-clean-q4.gguf
3TEMPLATE """{{ if .System }}[SYSTEM_PROMPT]{{ .System }}[/SYSTEM_PROMPT]{{ end }}[INST]{{ .Prompt }}[/INST]"""
4PARAMETER stop "</s>"
5PARAMETER temperature 0.3
6PARAMETER top_p 0.9
7PARAMETER repeat_penalty 1.1
8PARAMETER num_ctx 4096
9SYSTEM """Tu es un assistant qui répond en français aux questions sur MonEspaceSanté. Réponds de façon directe, factuelle et concise, en te basant uniquement sur les informations officielles du service."""
1ollama create faqmes -f Modelfile
2ollama run faqmes "Comment puis-je m'opposer à la création de Mon espace santé ?"
llama.cpp
1./llama-cli -m faqmes-clean-q4.gguf -c 4096 \
2 --temp 0.3 --top-p 0.9 --repeat-penalty 1.1 \
3 -p "[SYSTEM_PROMPT]Tu es un assistant qui répond en français aux questions sur MonEspaceSanté. Réponds de façon directe, factuelle et concise, en te basant uniquement sur les informations officielles du service.[/SYSTEM_PROMPT][INST]Mon espace santé est-il obligatoire ?[/INST]"
Gabarit de prompt : Mistral v3 ([SYSTEM_PROMPT]…[/SYSTEM_PROMPT][INST]…[/INST]),
token d'arrêt </s>.
Comment ce modèle a été produit (pipeline complet)
Tout le pipeline — collecte, curation, génération synthétique, fine-tuning, fusion,
quantification, évaluation — est reproductible et a tourné en local sous WSL2 sur un
GPU NVIDIA RTX 5090 (32 Go).
1. Collecte des données — accès au CMS plutôt que scraping du SPA
Le site monespacesante.fr est une SPA Vite : le HTML rendu ne contient pas le texte
de la FAQ. Plutôt que de piloter un navigateur headless, il suffit d'inspecter /js/config.js et
de découvrir l'API CMS Drupal (JSON:API) qui alimente le site
(…/gateway/cms/jsonapi/faq). Un seul appel structuré renvoie l'intégralité du corpus
FAQ — catégories, questions, réponses HTML — bien plus propre que du scraping.
- Le HTML des réponses est converti en texte lisible en préservant la structure
(paragraphes, listes), puis déséchappé.
- Sorties :
faq_pairs.jsonl — 89 paires question/réponse officielles (la « vérité terrain »).
documents.jsonl — un document par réponse officielle, servant de contexte à la
génération synthétique.
2. Curation — NeMo Curator
Curation avec NVIDIA NeMo Curator (pipeline Ray / exécuteur Xenna, sur CPU) :
- Normalisation Unicode NFC + écrasement des espaces parasites en conservant les sauts
de paragraphe (
DocumentModifier).
- Filtrage des documents quasi vides (
ScoreFilter sur le nombre de mots).
- Les étapes GPU lourdes (déduplication floue / sémantique) sont volontairement omises :
le corpus est une seule petite FAQ déjà structurée et exempte de doublons, donc l'unicité
exacte est garantie par construction. Inutile d'ajouter de la complexité qui n'apporte rien.
3. Génération synthétique (SDG) — closed-QA ancré, sans CoT
Les 89 paires officielles ne couvrent qu'une formulation par question. Or les usagers
reformulent de mille façons. On augmente donc le corpus par génération synthétique
ancrée (closed-QA) :
- Modèle enseignant :
qwen3.5:27b servi localement par Ollama.
- Chaîne de pensée désactivée : on appelle l'API native
/api/chat avec
think=false. C'est le seul moyen fiable de couper le reasoning de qwen3.5 (la route
OpenAI /v1 ignore le drapeau) — on veut des réponses directes, pas des traces de CoT.
- Stratégie : pour chaque réponse officielle, on demande k = 6 paires
question/réponse distinctes, avec consigne stricte de ne rien inventer :
- questions variées telles qu'un usager les poserait réellement (reformulations,
synonymes, formulations directes/indirectes) ;
- réponses factuelles, concises (1 à 4 phrases), entièrement justifiées par le
contexte fourni.
- Échantillonnage :
temperature=0.4, top_p=0.9, num_predict=1536.
- Robustesse : parsing tolérant du tableau JSON (suppression d'éventuels
<think>…</think>,
extraction [ … ]), concurrence asynchrone (8 requêtes simultanées), et
déduplication sur la question normalisée (casse/espaces).
L'ancrage strict sur le texte source est essentiel : c'est ce qui évite que l'enseignant
n'« hallucine » des procédures ou des chiffres absents de la FAQ officielle.
4. Mise en forme du dataset de fine-tuning
- Fusion : paires officielles d'abord (qualité maximale ; elles gagnent les
égalités de déduplication), puis l'augmentation synthétique.
- Déduplication finale sur la question normalisée.
- Format chat à trois tours
{system, user, assistant}, le system étant le prompt de
domaine (cf. ci-dessus).
- Découpage 95 % / 5 % (graine 42) → 584 exemples d'entraînement / 30 de validation
(614 paires uniques : 89 officielles + 525 synthétiques après déduplication).
📦 Le jeu de données complet (questions réelles + synthétiques, avec provenance et
catégorie) est publié séparément :
fenyo/MonEspaceSante-FAQ-QA.
5. Fine-tuning — QLoRA avec Unsloth
Entraînement avec Unsloth (patches d'optimisation appliqués avant trl/transformers/
peft), apprentissage sur les réponses uniquement.
| Paramètre | Valeur | Pourquoi |
|---|
| Base | Mistral-Small-3.2-24B-Instruct-2506 (BnB NF4 4-bit) | non-raisonneur, francophone natif, tient en 32 Go en QLoRA |
max_seq_len | 4096 | réponses FAQ courtes, mais marge confortable |
LoRA r | 16 | capacité suffisante sans surapprendre ~600 exemples |
LoRA alpha | 32 (facteur d'échelle 2.0) | convention alpha = 2·r |
LoRA dropout | 0.0 | dataset propre et homogène |
| Modules cibles | q,k,v,o,gate,up,down_proj (les 7) | adaptation complète des blocs attention + MLP |
| Optimiseur | paged_adamw_8bit | empreinte mémoire minimale |
| LR | 2e-4, cosine, warmup_ratio=0.05 | standard QLoRA, décroissance douce |
| Batch | 2 × grad_accum 8 = batch effectif 16 | stabilité du gradient sur petit corpus |
| Époques | 2 (74 steps) | la perte plafonne ; au-delà, surapprentissage d'un 24B sur ~600 ex. |
| Précision | bf16 | supportée par la RTX 5090 |
| Masquage | train_on_responses_only ([INST]/[/INST]) | n'apprend que les tours assistant |
| Graine | 42 | reproductibilité |
gradient_checkpointing | "unsloth" | mémoire d'activations réduite |
Courbe de perte (train) — convergence nette et saine :
| step | 10 | 20 | 30 | 40 | 50 | 60 | 70 |
|---|
| loss | 1.41 | 1.08 | 0.91 | 0.79 | 0.49 | 0.41 | 0.38 |
Le choix de 2 époques est délibéré : la perte est déjà à ~0.38 et continue de descendre,
mais pousser davantage sur un dataset de ~600 exemples ferait surapprendre un modèle de 24 B
(perte de fluidité générale). Le r=16 / alpha=32 et le dropout=0 ont été retenus pour
le même équilibre capacité ↔ généralisation.
6. Fusion (merge) — dans la base fp16, pas dans la base 4-bit
C'est le point technique le plus important de l'export. Un adaptateur QLoRA est entraîné
au-dessus d'une base quantifiée 4-bit. Deux stratégies de fusion sont alors possibles :
- Fusionner l'adaptateur dans la base 4-bit déquantifiée — pratique, mais cela fige
le bruit de quantification de la base dans tous les poids du modèle fusionné. À
l'inférence, ce bruit s'accumule et dégrade nettement les générations longues
(répétitions, dérives), et ce quel que soit le niveau de quantification GGUF choisi
ensuite (de
q4 à q8).
- Fusionner l'adaptateur dans la base fp16 d'origine (pleine précision) — seules les
couches réellement adaptées par la LoRA changent, le reste du réseau reste intact.
On retient la stratégie 2. Concrètement :
- chargement de la base fp16
unsloth/Mistral-Small-3.2-24B-Instruct-2506 (≈ 48 Go) sur
CPU (device_map="cpu", bf16, low_cpu_mem_usage), application de l'adaptateur via
PEFT puis merge_and_unload() ;
tie_word_embeddings=False (la base possède un lm_head distinct des embeddings) ;
- ré-agencement du décodeur de texte en
MistralForCausalLM standard (suppression de la
tour de vision et du multi-modal projector, inutiles pour un GGUF textuel ; correction du
préfixe de tenseurs introduit par l'empaquetage multimodal de transformers).
Cette fusion en pleine précision est ce qui rend le modèle stable même en q4.
7. Conversion & quantification GGUF — llama.cpp
convert_hf_to_gguf.py → GGUF F16 (≈ 47 Go).
llama-quantize → plusieurs niveaux testés et comparés :
| Quant | Taille | Verdict |
|---|
Q4_K_M | 14,3 Go | livrable recommandé : propre, rapide, sans dégénérescence |
Q5_K_M | 16,8 Go | gain de précision marginal ; option « qualité max » |
Q6_K / Q8_0 | 19 / 25 Go | non nécessaires : aucun gain perceptible après la fusion fp16 |
Après la fusion fp16, Q4_K_M est déjà parfaitement stable ; c'est donc lui qui est
publié ici comme livrable principal.
8. Évaluation
Deux approches complémentaires ont été employées :
a) Évaluation autonome de robustesse (eval/run_eval.ps1) — 12 questions
représentatives de la FAQ, posées sans pré-prompt (le system embarqué suffit). Pour
chaque réponse on mesure automatiquement :
- l'arrêt naturel (
done_reason == "stop", pas de coupure par limite de tokens) ;
- un ratio de répétition = mots / mots distincts ;
- la plus longue répétition de mots consécutifs.
Une réponse est marquée FAIL en cas de boucle (ratio ≥ 3.0, ou run ≥ 4, ou arrêt par
longueur avec ratio ≥ 2.0).
Résultat : 12 / 12 PASS pour Q4_K_M. Toutes les réponses s'arrêtent naturellement,
ratio de répétition compris entre 1.07 et 2.08, plus longue répétition = 1 (aucune),
longueurs de 30 à 217 tokens. Le Q5_K_M obtient lui aussi 12 / 12.
b) Juge LLM sur jeu de validation tenu à l'écart (eval/eval.py) — chaque question de
validation est posée au modèle candidat, puis la réponse est notée de 1 à 5 par un juge
(qwen3.5:27b) contre la réponse de référence (5 = factuellement correcte et complète).
L'orchestration enchaîne d'abord toutes les réponses du candidat puis tous les jugements,
pour ne charger qu'un seul modèle à la fois en VRAM (le candidat ~14–25 Go et le juge
~17 Go ne tiennent pas ensemble dans 32 Go).
Inspection manuelle des réponses : elles sont factuelles, concises, en bon français, et
restent strictement dans le périmètre Mon espace santé même sur des questions ambiguës
ne mentionnant jamais le service (« C'est obligatoire ? », « Comment ajouter un document ? »).
Limites & usage prévu
- Domaine fermé : conçu pour la FAQ Mon espace santé. Hors de ce périmètre, le modèle de
base répond de façon générique — il ne s'agit pas d'un assistant médical et il ne donne
aucun conseil de santé.
- Connaissances figées à la date de collecte de la FAQ ; les procédures du service réel
peuvent évoluer. Vérifiez toujours sur le site officiel.
- Français uniquement.
- Petit corpus (~600 paires) : excellent sur les intentions couvertes par la FAQ, moins
pertinent sur des questions très éloignées.
Attribution & licence
- Modèle de base :
mistralai/Mistral-Small-3.2-24B-Instruct-2506 — licence Apache 2.0.
- Données : contenu public de la FAQ Mon espace santé (service public français).
- Outils : Unsloth, NVIDIA NeMo Curator, llama.cpp, Ollama, qwen3.5 (enseignant SDG).
- Modèle dérivé distribué sous Apache 2.0.