LLM Engineer's Handbook
Chapitre 2 / 11 · 15 min de lecture

La boîte à outils du LLM Engineer

L'outillage MLOps et cloud qui rend un système LLM reproductible : orchestration, suivi d'expériences, registres de modèles et bases vectorielles.

Construire un modèle de langage qui fonctionne dans un notebook est une chose ; le transformer en un système qu'on peut réentraîner, déployer et déboguer six mois plus tard en est une autre. C'est précisément le fossé que comble l'outillage MLOps. Iusztin et Labonne consacrent un chapitre entier à présenter leur pile technique avant même d'écrire la première ligne du projet fil rouge, le LLM Twin (un assistant entraîné à imiter votre style d'écriture). Leur message implicite est clair : on ne choisit pas des outils par goût, on les choisit parce qu'ils incarnent des principes d'ingénierie.

Ce chapitre n'est pas un guide d'installation pas à pas — les auteurs renvoient le lecteur au README du dépôt pour cela. Il s'agit plutôt de comprendre, catégorie par catégorie, quel problème chaque outil résout et pourquoi il a sa place dans un système de production. La règle d'or à garder en tête : ces outils ne sont jamais une fin en soi, ils sont la matérialisation concrète de trois exigences MLOps fondamentales — la reproductibilité (reproducibility), l'automatisation et le versionnage du code, des données et des modèles.

La philosophie MLOps : pourquoi avant comment

Le développement d'un modèle d'apprentissage automatique diffère radicalement du développement logiciel classique sur un point : c'est un processus itératif et expérimental. On lance des dizaines d'essais en parallèle, on les compare sur des métriques, et on décide lequel mérite la production. Sans discipline, ce chaos devient ingérable et, surtout, irreproductible : impossible de réexpliquer pourquoi le modèle déployé se comporte comme il le fait.

Trois piliers structurent toute la pile présentée dans le livre.

  • Reproductibilité : pouvoir relancer un pipeline et obtenir le même résultat. Cela suppose de figer non seulement le code, mais aussi la version de Python, les dépendances exactes, les données d'entrée et les hyperparamètres.
  • Automatisation : enchaîner les étapes (collecte, prétraitement, entraînement, déploiement) sans intervention manuelle, ce qui réduit les erreurs et accélère les itérations.
  • Versionnage : suivre l'historique du code (Git), mais aussi des données et des modèles, qui changent indépendamment du code. Un même script produit des modèles différents selon le jeu de données ; il faut donc versionner les trois.

Note

Le livre repousse délibérément la théorie approfondie de MLOps et LLMOps au chapitre 11. L'idée pédagogique est qu'on comprend bien mieux ces concepts après avoir vu les outils à l'œuvre dans un cas concret. Ce chapitre-ci se contente donc de poser le vocabulaire et le rôle de chaque brique.

Avant d'entrer dans le détail, voici la cartographie d'ensemble, qui sert de fil conducteur au reste du chapitre.

Catégorie d'outilProblème résoluExemple retenu dans le livre
Gestion des versions de PythonIsoler la version d'interpréteur par projetpyenv
Gestion des dépendances et environnementsFiger les paquets exacts, éviter les conflitsPoetry
Exécuteur de tâchesCentraliser les commandes CLI du projetPoe the Poet
Orchestrateur de pipelinesAutomatiser et tracer les enchaînements MLZenML
Suivi d'expériencesComparer les essais, métriques, hyperparamètresComet ML
Registre de modèlesStocker et partager les modèles versionnésHugging Face
Surveillance de promptsTracer les chaînes de prompts d'un LLMOpik
Base NoSQLStocker les données brutes non structuréesMongoDB
Base de données vectorielleRecherche sémantique pour le RAGQdrant
Cloud et calcul GPUEntraîner et servir les modèles à l'échelleAWS / SageMaker
ConteneurisationReproduire l'infra localement et en prodDocker

L'écosystème Python : reproductibilité dès la racine

Tout projet Python repose sur trois fondations : l'interpréteur, la gestion des dépendances et un exécuteur de tâches (task runner). Le livre fixe Python 3.11.8 comme version de référence, et c'est là que la reproductibilité commence vraiment.

pyenv : une version d'interpréteur par projet

Plutôt que d'installer plusieurs Python globaux qui s'écrasent mutuellement, pyenv permet de gérer plusieurs versions et d'en épingler une par dossier. Un simple fichier .python-version à la racine du dépôt suffit : pyenv détecte automatiquement la bonne version dès qu'on travaille dans ce répertoire.

pyenv install 3.11.8       # installe la version exacte
pyenv local 3.11.8         # cree le fichier .python-version
python --version           # -> Python 3.11.8 (dans ce dossier)

Poetry : figer l'arbre des dépendances

Poetry est le gestionnaire de dépendances et d'environnements virtuels retenu. Un gestionnaire de dépendances (dependency manager) permet de spécifier, installer et mettre à jour les bibliothèques externes dont un projet dépend. Poetry apporte deux garanties décisives.

D'abord, il épingle (pin) les versions. Le fichier pyproject.toml peut indiquer des plages (requests = "^2.25.1"), mais le fichier poetry.lock enregistre la version exacte réellement installée (requests = "2.25.1"), y compris les sous-dépendances non listées explicitement. C'est ce verrouillage qui élimine le fameux « ça marche sur ma machine ».

[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.25.1"
numpy = "^1.19.5"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Ensuite, Poetry crée un environnement virtuel (virtual environment) isolé. Si le projet A exige numpy == 1.19.5 et le projet B numpy == 1.26.0, les garder dans l'environnement global est impossible : l'un écraserait l'autre. L'isolation par projet supprime ce conflit.

Astuce

Les auteurs citent les alternatives et leurs compromis. Venv et Conda créent des environnements mais gèrent mal les dépendances (on retombe sur des requirements.txt moins puissants que les fichiers de verrouillage). Pipenv ressemble à Poetry mais est plus lent. Surtout, ils signalent uv, un remplaçant de Poetry écrit en Rust et bien plus rapide, jugé prometteur. Le principe reste le même quel que soit l'outil : épingler pour reproduire.

Poe the Poet : la façade des commandes

Dernier maillon, Poe the Poet est un greffon de Poetry qui centralise toutes les commandes CLI du projet directement dans pyproject.toml, plutôt que de les éparpiller dans des scripts shell ou un Makefile.

[tool.poe.tasks]
test = "pytest"
format = "black ."
start = "python main.py"

L'intérêt dépasse le confort : une façade unique sur les commandes agit comme une documentation vivante et simplifie la collaboration. Tout le monde lance poetry poe test ou poetry poe run-digital-data-etl sans avoir à connaître les détails sous-jacents.

ZenML : l'orchestrateur, le pont entre ML et MLOps

C'est le cœur de la pile. ZenML se présente comme la passerelle entre la recherche exploratoire (les notebooks) et un environnement de production. Il s'attaque frontalement aux problèmes de réplication en production : difficultés de versionnage, reproduction des expériences, organisation de flux complexes, et fossé entre entraînement et déploiement.

Pipelines et steps : le motif universel des orchestrateurs

Un orchestrateur (orchestrator) automatise, planifie et coordonne les pipelines ML pour qu'ils s'exécutent dans le bon ordre, gèrent leurs dépendances, optimisent les ressources et tolèrent les pannes. ZenML applique un motif que l'on retrouve dans tous les orchestrateurs : une fonction de haut niveau, le pipeline, appelle plusieurs unités de travail, les steps (étapes). On bascule de l'un à l'autre par un simple décorateur.

from zenml import pipeline
from steps.etl import crawl_links, get_or_create_user


@pipeline
def digital_data_etl(user_full_name: str, links: list[str]) -> None:
    user = get_or_create_user(user_full_name)
    crawl_links(user=user, links=links)

Chaque step est une fonction Python ordinaire, décorée par @step. Le point essentiel : pour bien s'intégrer à ZenML, il faut écrire du code modulaire, où chaque fonction fait une seule chose.

from loguru import logger
from typing_extensions import Annotated
from zenml import get_step_context, step

from llm_engineering.application import utils
from llm_engineering.domain.documents import UserDocument


@step
def get_or_create_user(
    user_full_name: str,
) -> Annotated[UserDocument, "user"]:
    logger.info(f"Getting or creating user: {user_full_name}")
    first_name, last_name = utils.split_user_full_name(user_full_name)
    user = UserDocument.get_or_create(
        first_name=first_name, last_name=last_name
    )
    return user

Une fois exécuté, l'enchaînement des steps forme un graphe orienté acyclique (directed acyclic graph, DAG), visualisable dans le tableau de bord ZenML, avec le statut de chaque run, ses logs agrégés et la pile (stack) utilisée.

À retenir

Une décision d'architecture lourde de conséquences : la granularité des steps. Chaque step s'exécutera comme une unité distincte, sur une machine distincte, une fois déployé dans le cloud. Découper trop fin multiplie les transferts ; trop gros, on perd en parallélisme et en observabilité. C'est un compromis à arbitrer projet par projet.

Découpler la logique de l'orchestrateur

Les auteurs insistent sur un choix de conception réutilisable : toute la logique métier vit dans un module llm_engineering, tandis que les dossiers steps et pipelines ne contiennent que le « collage » ZenML. Les steps importent ce dont ils ont besoin depuis llm_engineering comme on consomme un paquet ; les pipelines se contentent d'assembler les steps. Conséquence directe : on peut remplacer ZenML par un autre orchestrateur — ou réutiliser la même logique dans une API REST — sans toucher au code métier.

La feature « stack » : pas de verrouillage fournisseur

Plutôt qu'une énième plateforme ML, ZenML introduit la notion de stack (pile). Une stack relie ZenML à des infrastructures variées sans que le code Python ne change : un orchestrateur et moteur de calcul (SageMaker ou Vertex AI), un stockage distant (S3 ou Google Cloud Storage), un registre de conteneurs (Docker Registry ou ECR).

   Code Python (steps + pipelines)
              |
              v
        +-----------+
        |  ZenML    |   <- abstraction
        +-----------+
         /    |     
        v     v      v
   SageMaker  S3    ECR     (stack AWS du LLM Twin)
   Vertex AI  GCS   ...     (stack alternative, code inchange)

C'est précisément ce qui évite le verrouillage fournisseur (vendor lock-in) : passer d'AWS à GCP revient à changer la stack, pas le code. Le livre note qu'aucun des concurrents — Airflow, Prefect, Metaflow, Dagster, ou Kubeflow et Argo Workflows pour les adeptes de Kubernetes — n'offre cette abstraction, ce qui fait de ZenML, pour les auteurs, le meilleur compromis entre simplicité, fonctionnalités et coûts.

Artefacts et métadonnées : versionner les fichiers, pas que le code

Un artefact (artifact) est tout fichier produit pendant le cycle de vie ML : jeux de données, modèles, checkpoints, logs. ZenML transforme automatiquement chaque sortie de step en artefact versionné, partageable et enrichi de métadonnées. C'est ici que se concrétise le versionnage des données.

L'intérêt des métadonnées est de comprendre le contenu d'un artefact sans le télécharger : pour un jeu de données, sa taille, le ratio train/test, le nombre d'échantillons par catégorie. On les attache manuellement, ce qui permet de précalculer tout ce qui aide à la découverte des données dans l'entreprise.

from typing_extensions import Annotated
from zenml import ArtifactConfig, get_step_context, step


@step
def generate_instruction_dataset(
    prompts: Annotated[dict, "prompts"],
) -> Annotated[
    InstructTrainTestSplit,
    ArtifactConfig(
        name="instruct_datasets",
        tags=["dataset", "instruct", "cleaned"],
    ),
]:
    datasets = ...  # generation du jeu de donnees

    step_context = get_step_context()
    step_context.add_output_metadata(
        output_name="instruct_datasets",
        metadata=_get_metadata_instruct_dataset(datasets),
    )
    return datasets

Chaque artefact possède un identifiant unique (UUID) qui permet de retrouver et charger une version précise des données — la clé de la reproductibilité.

from zenml.client import Client

artifact = Client().get_artifact_version(
    "8bba35c4-8ff9-4d8f-a039-08046efc9fdc"
)
loaded_artifact = artifact.load()

Note

Une sortie de step doit être sérialisable pour devenir un artefact. ZenML sérialise la plupart des objets réductibles à des types primitifs, mais pas tout : les auteurs racontent avoir dû étendre le « materializer » de ZenML pour gérer les UUID utilisés comme identifiants. Anecdote instructive sur le fait qu'un orchestrateur transforme réellement vos sorties de fonctions en fichiers tracés.

Configurer un pipeline sans toucher au code

ZenML permet d'injecter une configuration au moment de l'exécution via un fichier YAML. Pour le LLM Twin, changer la personne dont on collecte les écrits revient à changer un fichier de config, jamais le code.

parameters:
  user_full_name: Maxime Labonne
  links:
    - https://mlabonne.github.io/blog/posts/2024-07-29_Finetune_Llama31.html
    - https://maximelabonne.substack.com/p/uncensor-any-llm-with-abliteration
    # ... autres liens

Cette approche découple les paramètres du code et trace les entrées de chaque pipeline, garantissant la reproductibilité de chaque run.

Suivre les expériences et stocker les modèles

L'orchestration assure le « comment » de l'exécution. Reste à savoir quel essai a gagné et où ranger le modèle final.

Comet ML : le suivi d'expériences

Un outil de suivi d'expériences (experiment tracking) journalise tout ce qui permet de comparer des essais : métriques d'entraînement et d'évaluation (loss, norme du gradient), hyperparamètres, mais aussi des métriques système prêtes à l'emploi (utilisation GPU, CPU, mémoire) pour repérer les goulots d'étranglement. Le livre utilise Comet ML dans sa version en ligne gratuite.

from comet_ml import Experiment

experiment = Experiment(project_name="llm-twin-training")
experiment.log_parameters({
    "learning_rate": 2e-4,
    "lora_rank": 16,
    "epochs": 3,
})

for step, (train_loss, eval_loss) in enumerate(history):
    experiment.log_metric("train_loss", train_loss, step=step)
    experiment.log_metric("eval_loss", eval_loss, step=step)

Les concurrents (W&B, MLflow, Neptune) offrent globalement les mêmes fonctionnalités ; les auteurs retiennent Comet pour sa simplicité et son interface intuitive.

Hugging Face : le registre de modèles

Un registre de modèles (model registry) est un dépôt centralisé qui gère les modèles tout au long de leur cycle de vie : il stocke chaque modèle avec ses métadonnées, son historique de versions et ses métriques, servant de source unique de vérité. Il s'intègre aux pipelines CI/CD pour le déploiement.

Le livre choisit Hugging Face non pour ses fonctionnalités brutes — ZenML, Comet et SageMaker proposent aussi un registre — mais pour son écosystème : partage immédiat des modèles affinés (les TwinLlama 3.1 8B et sa variante alignée par DPO) et intégration native avec Unsloth pour le fine-tuning et SageMaker pour l'inférence.

from transformers import AutoModelForCausalLM, AutoTokenizer

# Pousser un modele affine vers le registre Hugging Face
model.push_to_hub("mlabonne/TwinLlama-3.1-8B")
tokenizer.push_to_hub("mlabonne/TwinLlama-3.1-8B")

# Plus tard : recharger une version precise du registre
model = AutoModelForCausalLM.from_pretrained(
    "mlabonne/TwinLlama-3.1-8B"
)

La leçon généralisable : on choisit le registre qui s'intègre le mieux au reste de sa pile et à ses besoins de partage, pas le plus riche dans l'absolu.

Opik : surveiller les prompts

Les outils de logs classiques ne conviennent pas aux LLM. Pourquoi ? Parce qu'une interaction avec un LLM enchaîne plusieurs prompts d'entrée et sorties générées en une trace (trace), où chaque prompt dépend des précédents. Un log en texte plat est illisible ; il faut un tableau de bord spécialisé pour grouper et déboguer ces traces. Le livre utilise Opik, outil open source signé Comet, pour la surveillance de prompts (prompt monitoring), préféré à Langfuse, Galileo ou LangSmith, jugés plus lourds à mettre en œuvre, pour sa simplicité.

Les bases de données : NoSQL et vectorielle

Le LLM Twin manipule deux types de données, donc deux bases, toutes deux lancées localement via Docker.

MongoDB sert de base NoSQL pour stocker les données brutes collectées sur internet avant traitement. Comme on travaille avec du texte non structuré, la souplesse du schéma NoSQL convient parfaitement.

Qdrant est la base de données vectorielle (vector database) qui stocke les données une fois traitées et transformées en vecteurs (embeddings), pour la recherche sémantique au cœur du RAG (génération augmentée par récupération). Les auteurs auraient pu prendre n'importe quelle base vectorielle pour un MVP, mais ont retenu Qdrant pour son meilleur compromis entre débit (RPS), latence et temps d'indexation, et sa robustesse industrielle.

BaseTypeRôle dans le pipelineDonnées stockées
MongoDBNoSQL documentCollecte amontTexte brut non structuré
QdrantVectorielleRécupération RAG avalEmbeddings + payload

Astuce

Pour comparer en détail les bases vectorielles (Milvus, Redis, Weaviate, Pinecone, Chroma, pgvector...), le livre renvoie au comparatif de Superlinked, qui couvre licence, fonctionnalités, modèles d'embedding et frameworks supportés. Comparer ces bases en profondeur pourrait remplir un chapitre entier — d'où le renvoi.

Le cloud : AWS et SageMaker

L'entraînement et le service d'un LLM exigent des GPU coûteux : c'est là qu'intervient le cloud. Le livre choisit AWS, non par supériorité technique absolue (GCP et Azure offrent des services équivalents) mais parce que c'est le plus répandu et celui où les auteurs ont le plus d'expérience — un compromis assumé entre temps de développement, fonctionnalités et coûts.

SageMaker : le calcul d'entraînement et d'inférence

SageMaker est un service ML entièrement managé pour construire, entraîner et déployer des modèles à l'échelle, en masquant l'infrastructure sous-jacente. Le LLM Twin s'en sert pour affiner le modèle sur des clusters de GPU et le déployer comme API REST accessible en temps réel.

Le choix le plus instructif est celui de SageMaker plutôt que Bedrock. Voici le compromis fondamental que le livre met en lumière.

CritèreAmazon BedrockAWS SageMaker
Modèle d'exécutionServerless, aucune infra à gérerCalcul provisionné à gérer
PersonnalisationLimitée aux modèles pré-entraînésContrôle complet du processus ML
Public viséDéveloppeurs sans expertise MLData scientists, experts ML
TarificationPar appel d'API, prévisiblePay-as-you-go, payé même au repos
Cas d'usagePrototypage rapideIngénierie ML personnalisée

Bedrock est un excellent choix pour prototyper vite, mais il masque justement les aspects d'ingénierie que ce livre veut enseigner. SageMaker offre la personnalisation nécessaire pour montrer tout le travail de déploiement.

Attention

SageMaker est une lame à double tranchant côté coûts : un endpoint déployé continue de facturer même inutilisé, contrairement au serverless de Bedrock. Il faut donc concevoir des systèmes d'autoscaling qui suppriment les ressources non utilisées. Les auteurs estiment le coût AWS du suivi pratique du livre entre 50 et 100 dollars, essentiellement liés aux tests SageMaker — d'où le conseil de configurer des alarmes de facturation.

Les auteurs notent enfin que SageMaker lui-même n'est pas totalement personnalisable : pour un contrôle complet à moindre coût, on descendrait sur EKS ou ECS (les services Kubernetes / conteneurs d'AWS), au prix d'une complexité bien supérieure. SageMaker représente donc l'équilibre entre contrôle et confort d'un service managé.

Docker et le compromis « tout maison » vs managé

Toute cette infrastructure — ZenML, MongoDB, Qdrant — se lance localement grâce à Docker (conteneurisation), en une commande : poetry poe local-infrastructure-up. La conteneurisation garantit que l'environnement reproduit localement est le même qu'en production, fermant la boucle de reproductibilité jusqu'au système d'exploitation.

En filigrane de tous ces choix se joue un arbitrage récurrent : faut-il héberger soi-même (open source, contrôle total, coût d'exploitation) ou recourir à des services managés (rapidité, fiabilité, facture)? Le livre adopte une voie pragmatique : services managés et options serverless gratuites quand c'est possible (Comet, Qdrant Cloud, ZenML serverless), Docker en local pour itérer sans frais, et calcul GPU payant uniquement quand l'entraînement l'exige. Le principe à retenir dépasse chaque outil : la pile doit servir les principes MLOps, pas l'inverse.

À retenir

  • Les outils ne sont jamais une fin en soi : ils matérialisent trois principes MLOps — reproductibilité, automatisation et versionnage du code, des données et des modèles.
  • L'écosystème Python pose les fondations : pyenv fige l'interpréteur, Poetry verrouille l'arbre des dépendances (poetry.lock) pour tuer le « ça marche sur ma machine », et Poe the Poet centralise les commandes comme documentation vivante.
  • ZenML orchestre les pipelines via le motif @pipeline / @step, transforme chaque sortie en artefact versionné doté de métadonnées, et son système de stack évite le verrouillage fournisseur en découplant le code de l'infrastructure.
  • Le suivi d'expériences (Comet ML) compare les essais, le registre de modèles (Hugging Face) sert de source de vérité partagée, et la surveillance de prompts (Opik) trace les chaînes spécifiques aux LLM.
  • Deux bases pour deux besoins : MongoDB (NoSQL) pour le texte brut, Qdrant (vectorielle) pour la recherche sémantique du RAG.
  • Côté cloud, le choix SageMaker plutôt que Bedrock illustre le compromis maître du livre : on sacrifie la simplicité serverless pour le contrôle d'ingénierie, en surveillant des coûts qui courent même au repos ; Docker reproduit toute la pile en local.