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
Note

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) :

  1. Embedding du libellé l_pr_product (même modèle qu’à l’indexation).

  2. Recherche dans Qdrant : les retrieval.size = 10 codes COICOP les plus proches.

  3. 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],
    )
  4. 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.concurrency dans config.yaml).

  5. Parsing de la réponse JSON (extract_json_from_response) → code_predict, codable, confidence, puis re-pruning du code prédit (troncature niveau 4 + mapping mapping_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é.

  6. 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)
Astuce

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