Fonctionnement, sorties et pistes de consolidation — pour les mainteneurs
2026-06-10
Le problème en bref
L’enquête Budget de Famille collecte les tickets de caisse des ménages ; chaque ligne est un libellé court et bruité à rattacher à la nomenclature COICOP :
Bagu. Tradition U Blé Bretagne · Nescafé DLC Gust ESP 30 cap 165g · Ticket CB · Illisible
Codes hiérarchiques 01 → 01.1.1.1 (le pipeline s’arrête au niveau 4)
Codes techniques 98.x / 99.x pour les lignes non-produit
Codage manuel coûteux → le pipeline automatise et trace chaque décision
Architecture
flowchart LR
input[/"CSV d'entrée"/] --> PRE[preprocessing]
PRE -- raw_test.parquet --> REG[codif-regex]
PRE -.->|"suggester.parquet"| LCS
PRE -.-> PC[prune-coicop]
PC -.-> VDB[create-vector-db]
VDB -.->|"collection Qdrant"| RAG
REG -- raw_test_without_regex --> LCS[codif-lcs]
REG -- raw_test_without_regex --> PA[prune-annotations]
REG -- raw_test_without_regex --> TTC[run-ttc]
PA --> RAG[run-rag]
LCS -- raw_test_LCS --> DEC[decide-coicop]
RAG -- predictions --> DEC
TTC -- predictions --> DEC
DEC -- predictions --> FIN[final-output]
REG -- REGEX_pred --> FIN
DEC --> REP[report]
FIN --> out[/"CSV livré"/]
REP --> html[/report.html/]
style PC stroke-dasharray: 5 5
style VDB stroke-dasharray: 5 5
Argo Workflows ; chaque étape = un conteneur qui git clone (branche codif-pipeline) + uv sync
Un dossier S3 par run : …/workflow_runs/{run_date}/{run_id}/<étape>/ ; run_id/run_date passés en CLI partout
Deux workflows : codif-pipeline.yaml (prédiction, avec final-output) et pipeline.yaml (évaluation, entrée annotée)
prune-coicop / create-vector-db : sorties partagées entre runs, sautées par défaut (skip-vector-db=true)
La logique de conception
Aucune méthode ne couvre tout — le pipeline en croise quatre, puis arbitre :
Brique
Forte sur…
Aveugle sur…
Règles regex
libellés standardisés
tout le reste (voulu)
LCS
quasi-doublons du stock manuel
produits jamais vus
RAG
sens, libellés inédits
coût LLM, retrieval raté
TTC
volume grande distribution
hors supermarché
L’arbitre LLM-as-judge ne travaille que sur les désaccords : le consensus court-circuite l’appel LLM. C’est le compromis coût / qualité central du pipeline.
preprocessing + codif-regex
preprocessing (preprocessing/main.py) :
mappage de colonnes configurable (NAT_DEP → raw_product…) ; id UUID par ligne
deux normalisations : l_pr_product (légère → RAG/TTC) et s_pr_product (forte → regex/LCS)
enseigne → shop_type_name (jointure liste_magasins.csv) ; retrait des non-codables
écrit raw_test.parquet + le pool suggester.parquet (COPAIN, codé main)
codif-regex (regex-codif/, 43 règles dans config/rules.yaml) :
s’applique sur s_pr_product ; règles ancrées (^…$), périmètre volontairement étroit
match → REGEX_pred.parquet, la ligne sort du circuit ; sinon → raw_test_without_regex.parquet
2 règles renvoient le littéral Reprise manuelle — il traverse tout le pipeline jusqu’au livrable
codif-lcs (R + C++)
Plus longue sous-chaîne commune entre le libellé et chaque entrée du pool suggester :
tokenisation n-grammes de caractères (type fastText) — robuste aux abréviations
entraîné sur des données de caisses (grande distribution), fine-tuné sur le test BDF 2024 (sous-commandes train-basic / fine-tune-basic, workflow argo/ttc-pipeline.yaml)
prédiction sur l_pr_product, renvoie ttc_code_1..10 + ttc_conf_1..10
modèle chargé depuis MLflow : paramètre de workflow ttc-model-uri (URI d’artefact, propagé au rapport pour traçabilité)
⚠ Biais connu : très fiable en supermarché, moins ailleurs — compensé par l’arbitrage.
decide-coicop — 1. le consensus
try_consensus_decision() (coicop-bdf-classifier/src/decide_coicop.py) tranche sans appel LLM quand :
ttc_conf_1 ≥ 0,90 (seuil threshold codé en dur), et
tous les codes disponibles (lcs_code, rag_code) égalentttc_code_1
Tout est inspectable en place (DuckDB read_parquet('s3://…')) — premier réflexe de debug.
Le livrable et le rapport
final-output : fichier d’entrée d’origine + 3 colonnes — la décision LLM prime, sinon le code regex, sinon vide.
prediction_source
Lecture
consensus
les 3 modèles d’accord — le plus sûr
regex
règle déterministe (inclut Reprise manuelle)
llm
arbitrage — croiser avec llm_confiance et llm_comment
(vide)
non codé : échec LLM, raison dans llm_error (fichier decide-coicop)
report : 2 gabarits choisis automatiquement (code tout NULL → prédiction) — distribution par niveau, sources, meilleur prédicteur par code, désaccords totaux ; en mode évaluation : accuracy par niveau/groupe, confusion, calibration.
Traçabilité : MLflow codif-coicop-eval (paramètres dont ttc_model_uri, métriques, report.html en artefact) + traces Langfuse (run-rag).
Opérations courantes
# Pipeline complet (test rapide : -p sample_size=100)argo submit argo/codif-pipeline.yaml -p input_file=s3://…/mon_fichier.csv \-p text_column=NAT_DEP -p shop_column=MAG_DEP -p budget_column=MONT_DEP# Relancer UNE étape : --entrypoint + run_id/run_date du run d'origine (sinon dossier S3 vide !)argo submit argo/codif-pipeline.yaml --entrypoint decide-coicop \-p run_id=codif-abc12 -p run_date=2026-03-20 -p model=gemma4-26b-moe -p concurrency=5# Reprendre le DAG là où il a échouéargo retry codif-abc12
Pièges connus :
decide-coicop attend les inputs model/concurrency (pas decide-model/decide-concurrency) ; report attend experiment-name + step-timings — valider avec --dry-run
chaque étape est aussi exécutable en local (uv / Rscript) avec les bonnes variables d’env
Constats vérifiés dans le code, à garder en tête :
llm_code n’est jamais validé contre la nomenclature : la validation Pydantic est purement formelle → hallucination (code inexistant) possible dans le livrable
Pas de retry/backoff sur les erreurs LLM transitoires : un RateLimitError laisse la ligne en échec ; la « relance manuelle + reprise checkpoint » fait office de retry
Prompt RAG hors dépôt (Langfuse v12) : une modif de prompt n’est ni versionnée ni revue avec le code
Branches git incohérentes : run-ttc clone main, les autres étapes codif-pipeline (déjà fusionnée dans main) — risque de drift silencieux
MLflow éclaté : run-rag loggue dans l’expérience test, report dans codif-coicop-eval
Collection Qdrant unique, écrasée par create-vector-db : pas de versionnage de la nomenclature embarquée ; un run en cours pendant une recréation lirait un index incohérent
README en retard sur le code (image pré-construite run-ttc, DAG sans final-output)
Pistes — fiabilité
Valider predicted_code contre la nomenclature en sortie de decide-coicop : flag code_invalide (a minima) ou re-prompt automatique du LLM avec l’erreur
Retry avec backoff exponentiel sur RateLimitError / APIConnectionError dans call_llm_async — le checkpoint existant rend ça peu risqué
Boucle vertueuse : réinjecter les codifications validées par les gestionnaires dans le pool suggester (LCS s’améliore) et dans le fine-tuning TTC périodique
Suivi des non-codés : seuil d’alerte sur la part de llm_error / lignes vides par run (métrique MLflow déjà disponible)
Pistes — industrialisation
CI GitHub Actions : smoke tests uv run … --help par module, argo lint, rendu du site docs (et publication GitHub Pages)
Factoriser les manifests Argo : envFrom: secretRef au lieu de ~20 secretKeyRef copiés-collés par étape ; ou un WorkflowTemplate partagé
MLflow Model Registry pour le TTC : alias (@champion) au lieu de l’URI d’artefact brut mlflow-artifacts:/10/cacf26… codée dans le YAML
Unifier les branches clonées (tout sur main, ou tag/commit épinglé par release) et mettre le README à jour (ou pointer vers ce site)
Harmoniser MLflow : une seule expérience (ou convention claire run-rag vs report)
Pistes — méthode
Jeu d’évaluation permanent : un échantillon annoté stable + un run pipeline.yaml (mode évaluation) à chaque changement de modèle, prompt ou règle — comparaison directe dans MLflow (l’infra existe déjà)
Versionner la collection Qdrant : nom suffixé du hash de la nomenclature (coicop_lineage_<hash>), bascule atomique au lieu d’écrasement
Vendoriser une copie du prompt Langfuse dans le dépôt (fallback + revue de code)
Remplacer le littéral Reprise manuelle par un code technique dédié (ex. 97.x) pour garder predicted_code typé « code »
Reprendre les éléments out of scope de harmonize-io-plan.md : purge/cycle de vie des dossiers workflow_runs/, paramètre source_run_id pour ttc-pipeline.yaml
Pour contribuer
Site de documentation : une page par étape (Rôle · Entrées · Traitement · Fil rouge · Sorties), construit depuis docs/ (quarto render docs/)