Créer chez soi une IA experte d'un domaine : choisir le matériel et le LLM de base, préparer les données, construire un RAG, fine-tuner en QLoRA, évaluer et déployer avec Ollama.
« Il a l'air meilleur » n'est pas une mesure. J'ai vu des modèles fine-tunés qui paraissaient brillants sur trois questions de démonstration et se trompaient sur un tiers du jeu de test. Sans évaluation chiffrée, impossible de savoir si le fine-tuning a aidé, si le nouveau découpage du RAG est meilleur, ou si le modèle 14B vaut sa lenteur. Ce chapitre construit un banc d'évaluation automatique et compare les quatre configurations du cours.
Reprenez data/bench.jsonl du chapitre 3 et enrichissez-le jusqu'à 100 à 200 questions. Ce jeu doit :
{"id": 1, "type": "fait", "question": "À quelle profondeur semer les haricots ?", "reponse": "2 à 3 cm, en poquets de 4 à 6 graines, quand le sol a atteint 12 °C.", "points_cles": ["2 à 3 cm", "sol réchauffé"]}
{"id": 2, "type": "diagnostic", "question": "Mes courgettes pourrissent au bout avant de grossir, pourquoi ?", "reponse": "Défaut de pollinisation le plus souvent ; parfois excès d'humidité.", "points_cles": ["pollinisation"]}
{"id": 3, "type": "hors_sujet", "question": "Quel est le meilleur forfait mobile ?", "reponse": "Refus poli : hors du domaine du potager.", "points_cles": ["refus"]}
{"id": 4, "type": "sans_reponse", "question": "Quelle dose exacte de purin d'ortie pour la variété de tomate Noire de Crimée ?", "reponse": "L'assistant doit indiquer qu'il n'a pas d'information spécifique à cette variété.", "points_cles": ["reconnaît l'absence d'information"]}Le champ points_cles liste les éléments indispensables d'une bonne réponse. Il rend la notation beaucoup plus fiable qu'une comparaison globale.
| Config | Modèle | RAG |
|---|---|---|
| A | modèle de base | non |
| B | modèle de base | oui |
| C | modèle fine-tuné | non |
| D | modèle fine-tuné | oui |
Cette comparaison répond aux vraies questions : le RAG apporte-t-il quelque chose ? Le fine-tuning seul suffit-il ? La combinaison vaut-elle l'effort ?
Le modèle fine-tuné doit être disponible dans Ollama pour ce chapitre : la conversion est décrite au chapitre 8. Faites un aller-retour si nécessaire, ou évaluez d'abord A et B.
Noter 200 réponses × 4 configurations à la main, c'est 800 lectures. On délègue la notation à un LLM juge, avec une grille précise. Le juge doit être le modèle le plus fort disponible, et idéalement d'une autre famille que le modèle évalué pour limiter les biais.
import ollama
from pydantic import BaseModel, Field
JUGE = "qwen3:14b"
class Note(BaseModel):
exactitude: int = Field(ge=0, le=2, description="0 faux ou inventé, 1 partiellement correct, 2 correct")
points_cles: int = Field(ge=0, le=2, description="0 aucun, 1 certains, 2 tous les points clés présents")
hallucination: bool = Field(description="la réponse affirme un fait faux ou non vérifiable")
justification: str
GRILLE = """Tu évalues la réponse d'un assistant de jardinage.
Question : {question}
Réponse de référence : {reference}
Points clés attendus : {points}
Réponse à évaluer :
<reponse>
{reponse}
</reponse>
Consignes :
- Compare au contenu de la référence, pas au style.
- Une réponse plus détaillée que la référence est acceptable si les ajouts sont exacts.
- Si la référence est un refus ou une absence d'information, la bonne réponse est de refuser ou de reconnaître ne pas savoir ; inventer une réponse vaut 0 et hallucination = true.
- Sois strict sur les chiffres, doses, dates et distances.
Réponds en JSON."""
def noter(question: str, reference: str, points: list[str], reponse: str) -> Note:
rep = ollama.chat(
model=JUGE,
messages=[{"role": "user", "content": GRILLE.format(
question=question, reference=reference, points=", ".join(points), reponse=reponse
) + " /no_think"}],
format=Note.model_json_schema(),
options={"temperature": 0},
)
return Note.model_validate_json(rep["message"]["content"])Un juge n'est utile que s'il est d'accord avec vous. Notez vous-même 30 réponses, comparez aux notes du juge. Au-delà de 80 % d'accord, faites-lui confiance pour les comparaisons ; en dessous, précisez la grille. Les défauts habituels : trop d'indulgence envers les réponses longues et bien rédigées, et une tendance à préférer les réponses de son propre modèle.
import json
import sys
from pathlib import Path
from statistics import mean
import ollama
sys.path.append("rag")
from rerank import rechercher_precis
from juge import noter
SYSTEME = """Tu es un assistant expert du potager. Tu réponds en français, de façon précise, pratique et concise.
Appuie-toi sur les documents fournis. S'ils ne contiennent pas la réponse, dis-le clairement et n'invente rien."""
CONFIGS = {
"A_base": {"modele": "qwen3:8b", "rag": False},
"B_base_rag": {"modele": "qwen3:8b", "rag": True},
"C_ft": {"modele": "potager", "rag": False},
"D_ft_rag": {"modele": "potager", "rag": True},
}
def generer(modele: str, rag: bool, question: str) -> str:
if rag:
contexte = "\n\n".join(f"[{p['source']}]\n{p['texte']}" for p in rechercher_precis(question))
contenu = f"<documents>\n{contexte}\n</documents>\n\nQuestion : {question}"
else:
contenu = question
rep = ollama.chat(
model=modele,
messages=[{"role": "system", "content": SYSTEME}, {"role": "user", "content": contenu + " /no_think"}],
options={"temperature": 0.2, "num_ctx": 8192},
)
return rep["message"]["content"]
cas = [json.loads(l) for l in Path("data/eval.jsonl").read_text(encoding="utf-8").splitlines() if l.strip()]
resultats = []
for nom, cfg in CONFIGS.items():
for c in cas:
reponse = generer(cfg["modele"], cfg["rag"], c["question"])
note = noter(c["question"], c["reponse"], c["points_cles"], reponse)
resultats.append({"config": nom, "id": c["id"], "type": c["type"], "reponse": reponse, **note.model_dump()})
print(nom, "terminé")
Path("eval/resultats.json").write_text(json.dumps(resultats, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n{'config':14} {'exact.':>7} {'pts clés':>9} {'halluc.':>8}")
for nom in CONFIGS:
r = [x for x in resultats if x["config"] == nom]
print(f"{nom:14} {mean(x['exactitude'] for x in r) / 2:7.0%} "
f"{mean(x['points_cles'] for x in r) / 2:9.0%} "
f"{mean(x['hallucination'] for x in r):8.0%}")uv run eval/evaluer.pyVoici le type de tableau qu'on obtient sur ce genre de projet. Les chiffres sont illustratifs ; les vôtres dépendront de votre domaine et de vos données.
config exact. pts clés halluc.
A_base 52% 41% 31%
B_base_rag 81% 76% 9%
C_ft 63% 55% 22%
D_ft_rag 88% 85% 4%Ce qu'on y lit typiquement :
Si D n'est pas meilleur que B, le fine-tuning n'a pas servi : revoyez les données d'entraînement avant de relancer quoi que ce soit.
La moyenne cache l'essentiel. Découpez par type :
from collections import defaultdict
par_type = defaultdict(list)
for r in resultats:
par_type[(r["config"], r["type"])].append(r["exactitude"] / 2)
for (config, type_), notes in sorted(par_type.items()):
print(f"{config:14} {type_:14} {mean(notes):.0%} (n={len(notes)})")Un assistant à 90 % sur les faits mais à 30 % sur les questions hors sujet répondra avec assurance à n'importe quoi. C'est souvent là que se cache le vrai problème.
Les chiffres disent où ça échoue. Seule la lecture des réponses dit pourquoi. Pour chaque erreur de la meilleure configuration, classez la cause :
| Cause | Correction |
|---|---|
| le bon passage n'a pas été trouvé | découpage, embeddings, recherche hybride, reranking (chapitre 5) |
| le passage était là mais mal exploité | exemples RAG-aware, fine-tuning (chapitre 6) |
| l'information n'existe pas dans le corpus | compléter les sources (chapitre 4) |
| la référence du jeu d'évaluation est fausse | corriger le jeu d'évaluation |
| le juge s'est trompé | préciser la grille |
Cette classification guide l'itération suivante bien mieux que n'importe quelle intuition. Dans mon expérience, plus de la moitié des erreurs viennent de la recherche ou du corpus, pas du modèle.
Au-delà du jeu principal, quelques tests ciblés :
Les assistants fine-tunés à partir de données trop propres échouent souvent sur les fautes de frappe et les prémisses fausses. Ajoutez ces cas aux données d'entraînement si nécessaire.
Une évaluation fiable, c'est un jeu de test humain et indépendant, un juge vérifié, une comparaison entre configurations et une lecture attentive des erreurs. C'est elle qui transforme le projet en démarche d'ingénierie : chaque modification est mesurée, et on sait enfin ce qui améliore réellement l'assistant.