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 sous-étapes :
| Sous-étape | Code | Rôle |
|---|---|---|
create-vector-db |
coicop-rag/scripts/0_create_vector_db.py |
encode la nomenclature prunée dans Qdrant |
run-rag |
coicop-rag/scripts/2_run_rag.py |
recherche vectorielle + génération LLM |
- Code :
coicop-rag/· config :coicop-rag/config/config.yaml
create-vector-db reste skippable via skip-vector-db=true (la collection Qdrant est réutilisée d’un run à l’autre). Les artefacts prunés qu’elle consomme (nomenclature, jeu à coder, mapping) sont produits par l’étape prune, qui n’est pas skippable et qui applique la troncature niveau 4 + l’élagage des hiérarchies linéaires pour tout le pipeline. Voir 3. Prune — troncature & élagage.
create-vector-db — encoder dans Qdrant
Chaque notice COICOP prunée (lue directement depuis prune/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, collection
coicop_lineage(distance cosinus), recréée à chaque exécution.
run-rag — recherche + génération
Pour chaque libellé à coder (coicop-rag/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 : métriques dans MLflow (expérience
test), traces dans Langfuse.
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}/run-rag/predictions.parquet (rag_code, confidence, codable) |
predictions.s3_path_retrieved_codes |
…/{run}/run-rag/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