Intelligence Artificielle angularforall.com

- Fine-tuning vs RAG : quand utiliser quoi en IA

Fine-TuningRagLlmIaOpenaiEmbeddingsModeles-PersonnalisesIa-GenerativeComparaison-IaTrainingVector-StoreCout-LlmBest-PracticesArchitecture-Ia
Fine-tuning vs RAG : quand utiliser quoi en IA

Comprenez les différences entre fine-tuning et RAG pour adapter les LLM à votre contexte : quand choisir chaque approche et comment combiner les deux.

Principes fondamentaux

RAG et fine-tuning répondent à deux questions différentes. RAG répond à « que sait le modèle à l'instant T ? » en lui injectant des connaissances externes à chaque requête. Le fine-tuning répond à « comment le modèle doit-il se comporter ? » en modifiant ses poids de façon permanente.

RAG — Retrieval-Augmented Generation

Le modèle de base (GPT-4o, Claude, Llama) reste inchangé. Avant chaque appel, un pipeline récupère des passages pertinents depuis une base documentaire et les injecte dans le prompt. Le modèle génère sa réponse en s'appuyant sur ces passages, pas uniquement sur sa mémoire paramétrique.

Fine-tuning — Adaptation des poids

On continue l'entraînement du modèle sur un dataset de paires (prompt, completion) qui représentent le comportement souhaité. Les poids du réseau neuronal sont modifiés. Après fine-tuning, le modèle "sait" se comporter différemment même sans contexte supplémentaire dans le prompt.

Analogie concrète : RAG = donner un dossier documentaire à un expert généraliste avant une réunion. Fine-tuning = recruter et former un expert spécialisé qui intègre ce savoir de façon permanente.
DimensionRAGFine-tuning
Ce qu'on modifieLe contexte (prompt)Les poids du modèle
PersistancePar requêtePermanente
Données nécessairesDocuments bruts (PDF, MD, HTML)Paires input/output annotées
Mise à jourImmédiate (réindexation)Réentraînement complet
TracabilitéSources citablesOpaque (mémoire paramétrique)
HallucinationRéduite (ancré dans docs)Possible si dataset biaisé

Architecture RAG complète

Un pipeline RAG en production comporte 4 phases : indexation des documents, retrieval, reranking optionnel, puis génération augmentée.

Phase 1 — Indexation (offline)

Les documents sont découpés en chunks (fragments), transformés en vecteurs d'embeddings par un modèle d'embedding, puis stockés dans une base vectorielle. Le chunking est souvent la partie la plus critique : des chunks trop grands diluent l'information pertinente, des chunks trop petits perdent le contexte.

# Stratégies de chunking courantes
# Fixed-size: découpage tous les N tokens avec overlap
chunk_size = 512          # tokens par chunk
chunk_overlap = 50        # tokens partagés entre chunks adjacents

# Semantic chunking: découpage aux limites naturelles (paragraphes, sections)
# → meilleure cohérence sémantique mais tailles variables

# Parent-child chunking: chunks petits pour le retrieval,
#                        chunks parents (plus larges) injectés en contexte
# → meilleure précision + meilleur contexte

Phase 2 — Retrieval (online, par requête)

La question de l'utilisateur est transformée en vecteur d'embedding (même modèle que l'indexation). Une recherche de similarité cosinus ou produit scalaire retrouve les K chunks les plus proches dans l'espace vectoriel.

# Exemple de recherche vectorielle avec Chroma
import chromadb
from chromadb.utils import embedding_functions

client = chromadb.PersistentClient(path="./chroma_db")
ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-3-small"  # 1536 dimensions, $0.02/1M tokens
)

collection = client.get_collection("docs", embedding_function=ef)

# Recherche des 5 chunks les plus pertinents
results = collection.query(
    query_texts=["Comment configurer le SSO avec Azure AD ?"],
    n_results=5,
    include=["documents", "metadatas", "distances"]
)

# distances: 0.0 = identique, 2.0 = opposé (cosine distance)
# Filtrer les résultats trop éloignés (distance > 0.7 = peu pertinent)
relevant = [
    (doc, meta)
    for doc, meta, dist in zip(
        results["documents"][0],
        results["metadatas"][0],
        results["distances"][0]
    )
    if dist < 0.7
]

Phase 3 — Reranking (optionnel mais recommandé)

La recherche vectorielle est rapide mais imprécise — elle mesure la proximité sémantique globale, pas la pertinence pour la question précise. Un reranker (cross-encoder) relit chaque paire (question, chunk) et attribue un score de pertinence plus fin.

# Reranking avec Cohere Rerank ou cross-encoder local
from cohere import Client

co = Client(os.environ["COHERE_API_KEY"])

results = co.rerank(
    query="Comment configurer le SSO avec Azure AD ?",
    documents=[chunk["text"] for chunk in retrieved_chunks],
    model="rerank-multilingual-v3.0",
    top_n=3  # garder seulement les 3 meilleurs après reranking
)

# reranked_chunks[0] est maintenant le plus pertinent selon le cross-encoder
reranked_chunks = [retrieved_chunks[r.index] for r in results.results]

Pipeline RAG complet avec LangChain

LangChain simplifie l'assemblage d'un pipeline RAG en fournissant des abstractions pour le chargement de documents, le splitting, l'embedding, le vector store et la chaîne de génération.

from langchain_community.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# 1. Chargement des documents (PDF, MD, TXT dans un dossier)
loader = DirectoryLoader("./docs", glob="**/*.md")
documents = loader.load()

# 2. Découpage en chunks avec overlap pour ne pas couper les phrases
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(documents)

# 3. Embeddings et stockage vectoriel
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
    chunks,
    embeddings,
    persist_directory="./chroma_db"
)

# 4. Template de prompt qui force le modèle à s'appuyer sur le contexte
template = """Tu es un assistant expert. Utilise UNIQUEMENT le contexte fourni
pour répondre. Si l'information n'est pas dans le contexte, dis-le clairement.

Contexte:
{context}

Question: {question}

Réponse (cite les sources si possible):"""

prompt = PromptTemplate(
    input_variables=["context", "question"],
    template=template
)

# 5. Chaîne RAG complète
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",      # "stuff" = injecte tous les chunks dans un seul prompt
    retriever=vectorstore.as_retriever(
        search_type="mmr",   # Maximum Marginal Relevance = diversité + pertinence
        search_kwargs={"k": 5, "fetch_k": 20}
    ),
    chain_type_kwargs={"prompt": prompt},
    return_source_documents=True  # pour pouvoir citer les sources
)

# 6. Requête
result = qa_chain.invoke({"query": "Comment configurer le SSO ?"})
print(result["result"])          # réponse du LLM
print(result["source_documents"]) # chunks utilisés comme contexte
search_type="mmr" vs "similarity" : La similarité pure retourne souvent les 5 chunks les plus semblables qui disent la même chose. MMR (Maximum Marginal Relevance) équilibre pertinence et diversité : les chunks retournés couvrent différents aspects de la question.

Techniques de fine-tuning

Le fine-tuning peut aller du réentraînement complet de tous les poids (coûteux) à l'adaptation de quelques millions de paramètres seulement (PEFT). En pratique, PEFT avec LoRA ou QLoRA est devenu le standard pour les modèles open source.

Full fine-tuning vs PEFT

Le full fine-tuning met à jour tous les poids du modèle. Pour un modèle de 7 milliards de paramètres en fp16, cela nécessite environ 14 Go juste pour les poids + l'optimiseur Adam demande 3× supplémentaires = ~56 Go de VRAM minimum. C'est prohibitif sur des GPU grand public.

PEFT (Parameter-Efficient Fine-Tuning) gèle les poids originaux et n'entraîne qu'un petit sous-ensemble de paramètres supplémentaires. LoRA (Low-Rank Adaptation) est la technique PEFT la plus utilisée.

LoRA — Low-Rank Adaptation

LoRA décompose chaque matrice de poids W (d×k) en deux matrices de bas rang : W + ΔW où ΔW = A × B, A étant de dimension (d×r) et B de dimension (r×k) avec r ≪ min(d,k). Seules A et B sont entraînées. Si r=16 et d=k=4096, on passe de 4096×4096 = 16.7M paramètres à 2×4096×16 = 131k — 127× moins de paramètres à entraîner.

# Configuration LoRA typique pour Llama 3 8B
from peft import LoraConfig, get_peft_model

lora_config = LoraConfig(
    r=16,              # rang de la décomposition (8-64 selon la complexité de la tâche)
    lora_alpha=32,     # scaling factor = alpha/r → garde le même ordre de grandeur
    target_modules=[   # quelles couches adapter
        "q_proj",      # queries des têtes d'attention
        "k_proj",      # keys
        "v_proj",      # values
        "o_proj",      # output projection
        "gate_proj",   # MLP gate (Llama utilise SwiGLU)
        "up_proj",
        "down_proj"
    ],
    lora_dropout=0.05, # regularisation légère
    bias="none",       # ne pas entraîner les biais (économie paramètres)
    task_type="CAUSAL_LM"
)

model = get_peft_model(base_model, lora_config)
model.print_trainable_parameters()
# → trainable params: 41,943,040 || all params: 8,072,904,704
# → trainable%: 0.5195% — seulement 0.5% des poids sont entraînés !

QLoRA — Quantized LoRA

QLoRA combine LoRA avec la quantification 4-bit (NF4 — Normal Float 4) du modèle de base. Les poids originaux sont stockés en 4-bit (½ octet au lieu de 2 octets en fp16), réduisant la VRAM de ~70%. Un modèle de 7B paramètres tient dans 5-6 Go de VRAM avec QLoRA, contre 14 Go en fp16.

# QLoRA : charger le modèle quantifié en 4-bit avant d'appliquer LoRA
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
import torch

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",       # Normal Float 4 (mieux que fp4 pour LLMs)
    bnb_4bit_compute_dtype=torch.bfloat16,  # calculs en bfloat16 pour vitesse
    bnb_4bit_use_double_quant=True   # double quantification = -0.4 GB de plus
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B-Instruct",
    quantization_config=bnb_config,
    device_map="auto"  # répartit sur les GPU disponibles
)

# Les adapteurs LoRA sont en bfloat16 → gradient checkpointing obligatoire
model.gradient_checkpointing_enable()
model = prepare_model_for_kbit_training(model)

Fine-tuning via API OpenAI

Pour un usage en production sans infrastructure GPU, l'API OpenAI propose du fine-tuning sur GPT-4o-mini et GPT-3.5-turbo. Le format dataset est JSONL avec des conversations au format chat.

Format du dataset

# dataset.jsonl — chaque ligne est un JSON valide
# Format messages (chat completions) — recommandé pour GPT-4o-mini

{"messages": [
  {"role": "system", "content": "Tu es un assistant qui extrait des entités JSON depuis du texte."},
  {"role": "user", "content": "Commande: 3 pizzas margherita pour demain 19h livraison à Paris 75001"},
  {"role": "assistant", "content": "{\"quantity\": 3, \"item\": \"pizza margherita\", \"delivery_time\": \"demain 19h\", \"address\": \"Paris 75001\"}"}
]}
{"messages": [
  {"role": "system", "content": "Tu es un assistant qui extrait des entités JSON depuis du texte."},
  {"role": "user", "content": "Je voudrais 1 burger veggie et 2 frites pour ce soir 20h30 à Lyon"},
  {"role": "assistant", "content": "{\"quantity\": [1, 2], \"item\": [\"burger veggie\", \"frites\"], \"delivery_time\": \"ce soir 20h30\", \"address\": \"Lyon\"}"}
]}
# Minimum recommandé : 50 exemples (idéalement 200-500 pour des tâches complexes)

Lancement et suivi

from openai import OpenAI
import json

client = OpenAI()

# 1. Upload du dataset
with open("dataset.jsonl", "rb") as f:
    upload_response = client.files.create(file=f, purpose="fine-tune")

file_id = upload_response.id
print(f"File uploaded: {file_id}")

# 2. Création du job de fine-tuning
ft_job = client.fine_tuning.jobs.create(
    training_file=file_id,
    model="gpt-4o-mini-2024-07-18",
    hyperparameters={
        "n_epochs": 3,             # 3-5 époques pour éviter l'overfitting
        "batch_size": "auto",      # OpenAI détermine automatiquement
        "learning_rate_multiplier": "auto"
    },
    suffix="extraction-commandes"  # nom du modèle fine-tuné
)

print(f"Job created: {ft_job.id}")

# 3. Suivi (polling ou webhooks)
import time

while True:
    job = client.fine_tuning.jobs.retrieve(ft_job.id)
    print(f"Status: {job.status} | trained_tokens: {job.trained_tokens}")

    if job.status in ["succeeded", "failed"]:
        break
    time.sleep(60)  # vérifier chaque minute

# 4. Utilisation du modèle fine-tuné
fine_tuned_model = job.fine_tuned_model
# → "ft:gpt-4o-mini-2024-07-18:org:extraction-commandes:abc123"

response = client.chat.completions.create(
    model=fine_tuned_model,
    messages=[
        {"role": "system", "content": "Tu es un assistant qui extrait des entités JSON."},
        {"role": "user", "content": "2 salades césar pour midi demain Paris 8e"}
    ]
)
print(response.choices[0].message.content)
# → {"quantity": 2, "item": "salade césar", "delivery_time": "midi demain", "address": "Paris 8e"}
Coûts API OpenAI (2025) : GPT-4o-mini fine-tuning = $3/million tokens d'entraînement + $0.30/million tokens d'inférence (input) + $1.20/million tokens (output). Un dataset de 500 exemples de 200 tokens ≈ 300k tokens ≈ $0.90 d'entraînement.

Évaluation du modèle fine-tuné

# Validation automatique : comparer sur un test set réservé
import json

def evaluate_model(model_id, test_examples):
    correct = 0
    for example in test_examples:
        user_msg = example["messages"][1]["content"]
        expected = json.loads(example["messages"][2]["content"])

        response = client.chat.completions.create(
            model=model_id,
            messages=[
                {"role": "system", "content": "Tu extrais des entités JSON."},
                {"role": "user", "content": user_msg}
            ],
            temperature=0  # déterministe pour l'évaluation
        )

        try:
            predicted = json.loads(response.choices[0].message.content)
            # Comparer les clés et valeurs
            if predicted == expected:
                correct += 1
        except json.JSONDecodeError:
            pass  # le modèle n'a pas retourné un JSON valide

    accuracy = correct / len(test_examples)
    print(f"Accuracy: {accuracy:.1%} ({correct}/{len(test_examples)})")
    return accuracy

# Comparer modèle de base vs fine-tuné
base_acc = evaluate_model("gpt-4o-mini", test_set)
ft_acc = evaluate_model(fine_tuned_model, test_set)
print(f"Amélioration: +{(ft_acc - base_acc):.1%}")

QLoRA avec modèles open source

Pour fine-tuner des modèles open source (Llama 3, Mistral, Qwen) localement ou sur cloud GPU, Unsloth est devenu la bibliothèque de référence : 2× plus rapide qu'Axolotl, 70% moins de VRAM, compatible Colab gratuit (T4 16GB) pour les modèles jusqu'à 7B.

# Fine-tuning complet avec Unsloth sur Llama 3.1 8B Instruct
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset

# 1. Charger le modèle avec QLoRA activé automatiquement
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Meta-Llama-3.1-8B-Instruct",
    max_seq_length=2048,
    dtype=None,        # auto-détection (bfloat16 sur Ampere/Ada, float16 sinon)
    load_in_4bit=True  # QLoRA activé
)

# 2. Ajouter les adapteurs LoRA
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_alpha=16,
    lora_dropout=0,     # 0 est recommandé par Unsloth (perf optimisée)
    bias="none",
    use_gradient_checkpointing="unsloth",  # vrai GC = économise VRAM
    random_state=42
)

# 3. Formater le dataset en Alpaca ou ChatML
def format_prompt(example):
    # Format ChatML (recommandé pour les modèles Instruct)
    return f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{example['instruction']}<|eot_id|><|start_header_id|>user<|end_header_id|>
{example['input']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
{example['output']}<|eot_id|>"""

dataset = load_dataset("json", data_files="./dataset.jsonl", split="train")
dataset = dataset.map(lambda x: {"text": format_prompt(x)})

# 4. Entraînement
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    packing=True,  # compacte les séquences courtes → +20% de vitesse
    args=TrainingArguments(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,   # batch effectif = 2×4 = 8
        warmup_steps=10,
        num_train_epochs=3,
        learning_rate=2e-4,
        fp16=False,
        bf16=True,
        logging_steps=10,
        output_dir="./llama3-finetuned",
        optim="adamw_8bit",  # AdamW en 8-bit = économise ~4 GB de VRAM
    ),
)
trainer.train()

# 5. Exporter en GGUF pour inférence locale avec Ollama
model.save_pretrained_gguf(
    "llama3-finetuned",
    tokenizer,
    quantization_method="q4_k_m"  # bon compromis qualité/taille
)
# → crée llama3-finetuned-unsloth.Q4_K_M.gguf, utilisable avec ollama/llama.cpp
Ressources GPU requises : Llama 3.1 8B avec QLoRA + Unsloth sur T4 16 GB (Colab gratuit) : OK pour r=16, batch=2, 2048 tokens. Mistral 7B : identique. Llama 3.1 70B : nécessite 2× A100 80GB minimum (même avec QLoRA).

Comparatif technique approfondi

CritèreRAGFine-tuning (LoRA/QLoRA)Fine-tuning API
Temps de mise en place2-8 heures1-3 jours2-4 heures
Infrastructure requiseCPU/API seulementGPU (≥16 GB VRAM)Aucune
Données nécessairesDocuments bruts50-5000 paires annotées50-1000 paires annotées
Contrôle des données100% local possible100% local possibleDonnées envoyées à OpenAI
Mise à jour connaissanceImmédiate (réindexation)Réentraînement (heures)Réentraînement (heures)
Latence par requête+50-200ms (retrieval)Identique au modèle baseIdentique à gpt-4o-mini
Coût par requêteÉlevé (contexte long)Faible (GPU propre)Faible (modèle petit + fine-tuné)
Adaptation style/tonVia prompt uniquementExcellenteExcellente
Extraction format strictCorrecte avec bon promptParfaite (>99% valid JSON)Parfaite
Connaissances à jourOui (réindexation)Non (figé à l'entraînement)Non (figé à l'entraînement)
Citabilité des sourcesOui (métadonnées chunks)NonNon
Risque d'hallucinationFaible (<10% avec rerank)Moyen (si dataset biaisé)Moyen

Quand RAG atteint ses limites

  • Tâche de classification binaire ou extraction structurée très répétitive (fine-tuning > RAG pour la cohérence)
  • Style de réponse impossible à imposer par prompt (ton très spécifique, abréviations métier)
  • Latence critique : le retrieval ajoute 50-200ms, inacceptable pour certaines applis temps réel
  • Contexte limité : les vieux modèles (3.5-turbo, Llama 2) ont des fenêtres courtes et le RAG "mange" trop de tokens
  • Volume de requêtes très élevé : coût des tokens de contexte s'accumule vs modèle fine-tuné qui n'a pas besoin de contexte

Quand le fine-tuning ne suffit pas

  • Données qui changent tous les jours (actualités, prix, stock)
  • Besoin de citer explicitement les sources documentaires
  • Base documentaire massive (>10 000 documents) impossible à mémoriser via fine-tuning
  • Questions nécessitant de synthétiser plusieurs documents simultanément

Stratégie hybride RAG + fine-tuning

La combinaison des deux approches donne souvent les meilleurs résultats en production : fine-tuner pour le comportement (style, format, instructions système), RAG pour les connaissances actualisées.

# Architecture hybride : modèle fine-tuné + retrieval
# Le modèle fine-tuné maîtrise le format de sortie et le ton
# Le RAG lui fournit les connaissances spécifiques au domaine

# Exemple : assistant juridique
# Fine-tuning : apprendre le vocabulaire juridique, le format des conclusions,
#               les raisonnements par analogie de jurisprudence
# RAG         : accéder aux textes de loi récents, décisions de justice,
#               contrats du client

# dataset.jsonl pour fine-tuner le comportement (PAS les connaissances)
{"messages": [
  {"role": "system", "content": "Tu es un assistant juridique spécialisé en droit social français."},
  {"role": "user", "content": "Analyse ce contexte : [CONTEXTE]. Question : [QUESTION]"},
  {"role": "assistant", "content": "**Fondement juridique :** [référence légale]\n**Analyse :** [raisonnement]\n**Conclusion :** [réponse claire]\n**Points de vigilance :** [risques à mentionner]"}
]}
# → Le modèle apprend le FORMAT de réponse (les 4 sections)
# → Le contenu (CONTEXTE) sera injecté par RAG à chaque requête

# Pipeline complet
def legal_assistant(user_question: str) -> str:
    # 1. Retrieval des textes de loi pertinents
    legal_docs = law_vectorstore.similarity_search(user_question, k=3)
    legal_context = "\n\n".join([doc.page_content for doc in legal_docs])

    # 2. Appel au modèle fine-tuné avec le contexte RAG
    response = client.chat.completions.create(
        model=fine_tuned_legal_model,
        messages=[
            {"role": "system", "content": "Tu es un assistant juridique spécialisé."},
            {
                "role": "user",
                "content": f"Analyse ce contexte :\n{legal_context}\n\nQuestion : {user_question}"
            }
        ]
    )
    return response.choices[0].message.content
Règle pratique : Fine-tune le comportement (comment répondre), utilise RAG pour les connaissances (quoi répondre). Les deux dimensions sont orthogonales et se complètent sans conflit.

Coûts réels et calculateur

Coûts fine-tuning API OpenAI (2025)

ModèleEntraînementInférence inputInférence output
GPT-4o-mini fine-tuned$3/1M tokens$0.30/1M tokens$1.20/1M tokens
GPT-3.5-turbo fine-tuned$8/1M tokens$3/1M tokens$6/1M tokens
# Calculateur de coûts approximatifs
# Dataset 500 exemples × 300 tokens/exemple × 3 époques
training_cost = (500 * 300 * 3) / 1_000_000 * 3   # GPT-4o-mini
# = 450k tokens × $3/1M = $1.35 d'entraînement

# Inférence : 10 000 requêtes/mois × 200 tokens input × 300 tokens output
inference_monthly = (
    10_000 * 200 / 1_000_000 * 0.30 +  # input = $0.60
    10_000 * 300 / 1_000_000 * 1.20    # output = $3.60
)  # = $4.20/mois avec modèle fine-tuné

# vs gpt-4o-mini standard avec RAG (contexte 2000 tokens)
rag_monthly = (
    10_000 * 2000 / 1_000_000 * 0.15 +  # input incluant contexte = $3.00
    10_000 * 300  / 1_000_000 * 0.60    # output = $1.80
)  # = $4.80/mois avec RAG standard

# Différence faible à ce volume, mais le fine-tuning SANS contexte RAG
# devient beaucoup moins cher à grande échelle (100k+ requêtes/mois)

Coûts infrastructure GPU (cloud)

GPUFournisseurPrix/heureCas d'usage
T4 16GBGoogle Colab (gratuit)$0Llama 3 8B QLoRA, Mistral 7B QLoRA
L4 24GBRunPod, Lambda$0.43/hLlama 3 8B full fp16, Mistral 7B full
A100 40GBRunPod, Lambda$1.64/hLlama 3 70B QLoRA, Mixtral 8×7B QLoRA
A100 80GBCoreWeave, Lambda$2.21/hLlama 3 70B full fp16
H100 80GBCoreWeave, RunPod$3.89/hLlama 3 405B QLoRA, entraînements rapides
Durée estimée : Fine-tuning Llama 3.1 8B sur 1 000 exemples (3 époques, max 2048 tokens) sur L4 ≈ 45 minutes ≈ $0.32. Sur A100 40GB ≈ 15 minutes ≈ $0.41. Google Colab T4 : ≈ 2h30 à coût $0.

Coûts RAG

# Coûts RAG avec OpenAI
# Embeddings : text-embedding-3-small = $0.02/1M tokens
# 10 000 documents × 500 tokens = 5M tokens indexation = $0.10 (one-shot)
# Mise à jour 100 docs/semaine = 50k tokens/semaine = négligeable

# Vector store :
# Chroma : open source, gratuit, local ou cloud self-hosted
# Pinecone : gratuit jusqu'à 1 index/100k vecteurs, ~$70/mois au-delà
# Weaviate Cloud : gratuit tier, puis ~$25/mois
# pgvector (PostgreSQL) : coût infra seulement

# Inférence RAG vs standard :
# RAG ajoute ~2000 tokens de contexte par requête
# Surcoût : 2000 tokens × $0.15/1M = $0.00030 par requête
# Pour 100k requêtes/mois : +$30/mois de surcoût contexte

Guide de décision

La règle des 80% s'applique ici : dans la grande majorité des cas, commencer par RAG + prompt engineering est la bonne décision. Le fine-tuning n'est justifié que lorsque RAG a prouvé ses limites sur ton cas d'usage précis.

Commencer par RAG si...

  • Les données source changent ou s'enrichissent régulièrement
  • Besoin de citer explicitement les sources dans les réponses
  • Données confidentielles qui ne peuvent pas sortir de ton infrastructure
  • Budget limité ou délais serrés (RAG opérationnel en une journée)
  • Base documentaire large (>1 000 documents)
  • Questions nécessitant de croiser plusieurs documents

Passer au fine-tuning (ou l'ajouter) quand...

  • RAG donne déjà de bons résultats mais le format de réponse est incohérent
  • Tâche de classification ou extraction structurée avec 100+ exemples annotés disponibles
  • Volume de requêtes élevé où le coût du contexte RAG devient significatif
  • Latence critique et les 50-200ms du retrieval sont inacceptables
  • Style et vocabulaire très spécifiques impossibles à imposer par prompt

Utiliser la stratégie hybride quand...

  • Besoin d'un ton et format spécifiques (fine-tuning) + données actualisées (RAG)
  • Domaine métier avec terminologie propre (fine-tuning) + base documentaire (RAG)
  • Extraction structurée depuis des documents qui changent fréquemment
  • Assistant métier expert dans un domaine avec connaissances documentaires à jour
Workflow recommandé : Prototype RAG en 1 journée → mesure la qualité sur 100 questions réelles → si le retrieval est le problème (mauvais chunks retournés), améliore le chunking et le reranking → si le format/comportement est le problème avec un bon retrieval, ajoute du fine-tuning pour le comportement.
Ne pas négliger le prompt engineering : Avant tout fine-tuning, teste le chain-of-thought (CoT), les few-shot examples dans le system prompt, et les prompts structurés (XML, JSON schema). Un bon prompt peut résoudre 60-70% des problèmes de format sans fine-tuning.

Partager