6. Codification RAG
Rôle de l’étape
La branche RAG (Retrieval-Augmented Generation) code les libellés par recherche sémantique : on encode la nomenclature COICOP dans une base vectorielle, on récupère pour chaque produit les codes les plus proches, et on demande à un LLM de choisir parmi cette courte liste de candidats.
Elle se compose de deux traitements, désormais répartis dans deux workflows distincts :
| Traitement | Workflow | Code | Rôle |
|---|---|---|---|
index-notices |
argo/index-notices-pipeline.yaml |
rag-notices/scripts/0_create_vector_db.py |
encode la nomenclature prunée dans Qdrant |
classify-rag-notices |
argo/codif-pipeline.yaml |
rag-notices/scripts/2_run_rag.py |
recherche vectorielle + génération LLM |
- Code :
rag-notices/· config :rag-notices/config/config.yaml
index-notices — un workflow autonome
argo/index-notices-pipeline.yaml enchaîne deux étapes seulement :
prune-codes (--only nomenclature) ──→ index-notices
Il est autonome au sens strict : la nomenclature dérive d’un CSV statique, aucune donnée issue d’un run de classification n’y entre. D’où le drapeau --only nomenclature, qui arrête prune-codes après l’étape 1 — les étapes suivantes liraient des sorties de classify-regex qui n’existent pas ici.
| Paramètre | Défaut | Rôle |
|---|---|---|
git-branch |
main |
branche clonée par les deux étapes |
run_id / run_date |
calculés par Argo | identité du run d’indexation, reprise dans le nom de la collection |
collection-name |
(vide) | échappatoire : forcer un nom exact au lieu de le composer |
Chaque notice COICOP prunée (lue depuis prune-codes/nomenclature_pruned.parquet, sans re-traitement) est mise en forme en texte (code, libellé FR, notes générales, contenu central, exclusions, lignage des parents) puis encodée en vecteur :
- Modèle d’embedding :
qwen3-embedding-8b(dimension 4096), via l’API OpenAI-compatible de llm.lab (LLMLAB_URL/LLMLAB_API_KEY). - Base vectorielle : Qdrant, distance cosinus. La collection porte un nom unique composé depuis
qdrant.collection_base(coicop_notices) et l’identité du run d’indexation :coicop_notices__{run_date}__{run_id}, p. ex.coicop_notices__2026-09-02__index-notices-a7k2p.
L’ancienne collection s’appelait coicop_lineage, un nom fixe partagé par tous les runs : réindexer détruisait la base que lisait un run concurrent, et rien ne permettait de savoir quelle version de la nomenclature était embarquée. La clé de config a été renommée collection_base (et non collection_name) exprès : le consommateur reçoit le nom complet en argument, et une clé homonyme dans la config l’inciterait à retomber en silence sur l’ancienne collection partagée.
À côté de la collection, l’étape écrit un manifeste JSON sous s3://projet-budget-famille/data/vector_db_manifests/{collection_name}.json : modèle d’embedding, dimension, stratégie de découpage (with_hierarchy), CSV source, nombre de points effectivement comptés dans Qdrant, SHA du commit. Le nom ne peut pas porter tout cela, et deux collections de même dimension bâties avec des réglages différents sont sinon indistinguables.
Le job se termine en imprimant la ligne à recopier :
classify-rag-notices-collection: coicop_notices__2026-09-02__index-notices-a7k2p
classify-rag-notices — recherche + génération
Cette étape appartient au pipeline de codification. Le nom de la collection à interroger lui est passé par le paramètre classify-rag-notices-collection, obligatoire et sans valeur par défaut : un nom vide fait échouer l’étape en quelques secondes (garde dans le conteneur, avant même le git clone, et required=True en argparse).
Validation de l’index avant tout travail coûteux. Le script relit le manifeste et vérifie que la collection existe, n’est pas vide, n’est pas en statut RED, et qu’elle a bien la dimension, le modèle d’embedding et la stratégie que ce run attend. Deux détails de conception : la validation a lieu avant mlflow.set_experiment, pour ne pas laisser un run FAILED polluant l’expérience ; et chaque message d’erreur nomme le paramètre fautif et le workflow à relancer. Le SHA git et le run_id de l’index sont ensuite loggués comme tags MLflow — sans quoi deux runs aux métriques différentes mais bâtis sur des index différents seraient indistinguables.
Pour chaque libellé à coder (rag-notices/scripts/2_run_rag.py) :
Embedding du libellé
l_pr_product(même modèle qu’à l’indexation).Recherche dans Qdrant : les
retrieval.size = 10codes COICOP les plus proches.Construction du prompt par
prepare_prompts(): le libellé, un bloc enseigne, un bloc prix, et la liste des codes candidats récupérés sont injectés dans un gabarit. Détail important : le gabarit de prompt n’est pas dans le dépôt — il est géré dans Langfuse (prompt-multi-level, version 12, cf.config.yaml).prompt_template.compile( product=searched_product["l_pr_product"], enseigne_bloc=enseigne_bloc, # "acheté dans cette enseigne : Action…" price_bloc=price_bloc, # "a coûté : 4.0 euros." proposed_codes="\n\n## ".join(qdrant_results_texts[i]), list_proposed_codes=qdrant_results_codes[i], )Génération par le LLM (
gemma4-26b-moe,temperature 0.1,max_tokens 2048), via la même API llm.lab (LLMLAB_URL/LLMLAB_API_KEY), avec 32 appels parallèles (llm.concurrencydansconfig.yaml).Parsing de la réponse JSON (
extract_json_from_response) →code_predict,codable,confidence, puis re-pruning du code prédit (troncature niveau 4 + mappingmapping_lvl4.parquet) : même si le LLM répond au niveau 5 ou sur un code replié, la prédiction retombe dans l’espace de codes pruné.Traçage : paramètres et compteurs dans MLflow (expérience
test), traces dans Langfuse. Aucune accuracy n’est calculée ici — c’est l’étapeevaluatequi mesure. Ce module évaluait autrefois sans condition, et loguait donc une accuracy ≈ 0 sur chaque run de production.
Exemple sur le fil rouge
Pour Max garden flowers balle surprise (Action, 3,99 €) :
- la recherche Qdrant renvoie 10 candidats COICOP (jouets, articles de jardin, articles de fête…) ;
- le prompt y ajoute l’enseigne (Action, magasin discount) et le prix (3,99 €) ;
- le LLM choisit le code le plus plausible et fournit une
confidence. Comme le libellé est ambigu, la confiance peut être moyenne — ce que l’arbitrage final prendra en compte.
Sorties
| Variable config | Chemin S3 |
|---|---|
predictions.s3_path |
…/{run}/classify-rag-notices/predictions.parquet (rag_code, confidence, codable) |
predictions.s3_path_retrieved_codes |
…/{run}/classify-rag-notices/retrieved_codes.parquet (top-10 candidats par produit) |
retrieved_codes.parquet permet d’auditer le retrieval indépendamment de la génération : si le bon code n’est pas dans les 10 candidats, le problème vient de l’embedding ou de la nomenclature indexée — pas du LLM. C’est le premier fichier à inspecter quand rag_code semble aberrant.
➡️ Étape suivante : 7. RAG sur annotations