Lección 6 — Exploiter

Coûts, rétention et passage en production

10 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Coûts, rétention et passage en production

Le contrôleur de gestion d'Arvea pose une question simple : combien coûte l'assistant par conseiller et par mois, et combien coûtera-t-il en janvier ? Le premier tableau de coûts produit pendant ce projet affichait 0,00 USD pour l'agent : aucune définition de prix ne correspondait aux modèles utilisés. Un chiffre faux est pire qu'une absence de chiffre, parce qu'on le croit. Cette leçon rend les coûts justes, les garde sous contrôle, et prépare la sortie du lab.

Le mécanisme : le coût est calculé une fois, à l'ingestion

Langfuse a deux façons d'obtenir un coût : vous l'envoyez (cost_details), ou il le déduit à partir de l'usage et d'une définition de modèle (expression régulière de correspondance + prix par clé d'usage). Le calcul a lieu dans le worker, au moment de l'ingestion, et n'est jamais refait : une définition ajoutée ou corrigée ne s'applique qu'aux nouvelles observations.

flowchart LR
  G["Génération<br/>model = gemini-3.5-flash-lite<br/>usage : input 196, output 47"] --> W["Worker"]
  D[("Définitions de modèles<br/>motif : gemini-3.5-flash-lite, avec ou sans models/<br/>input 0,30 / output 2,50 USD par M")] --> W
  W -->|"196 × 0,30e-6 + 47 × 2,50e-6"| C[("ClickHouse<br/>totalCost 0,0001763")]
  C --> M["API Metrics v2<br/>coût par modèle, par conseiller"]

La correspondance porte sur le nom de modèle enregistré et le prix sur chaque clé d'usage : une clé sans prix compte pour zéro. D'où deux pièges vus dans ce projet : un modèle servi différent du modèle demandé (leçon 2), et les tokens de réflexion, que Gemini facture au prix de la sortie mais que l'instrumentation range sous une clé distincte, output_reasoning_tokens.

La mise en œuvre

1. Déclarer les prix, toutes les clés

Les tarifs viennent de la page officielle de l'API Gemini (palier payant standard, relevés le 2026-10-15) :

files/src/assistant/observabilite.py (extrait)
PRIX: dict[str, dict[str, float]] = {
    "gemini-3.5-flash-lite": {"input": 0.30e-6, "input_cached_tokens": 0.03e-6,
                              "output": 2.50e-6, "output_reasoning_tokens": 2.50e-6},
    "gemini-3.8-flash": {"input": 0.75e-6, "input_cached_tokens": 0.075e-6,
                         "output": 3.75e-6, "output_reasoning_tokens": 3.75e-6},
}

def declarer_prix(langfuse) -> int:
    existants = {m.model_name for m in langfuse.api.models.list(limit=100).data if not m.is_langfuse_managed}
    for nom, prix in PRIX.items():
        if nom in existants:
            continue
        langfuse.api.models.create(
            model_name=nom, match_pattern=motif(nom), unit="TOKENS",
            pricing_tiers=[PricingTierInput(name="Standard", is_default=True, priority=0,
                                            conditions=[], prices=prix)])

Un test unitaire garantit que la réflexion est facturée au prix de la sortie pour chaque modèle. Les définitions vivent dans le code, versionnées, et make seed les applique : une instance reconstruite retrouve les mêmes prix.

2. Lire les coûts là où ils sont agrégés

L'API Metrics v2 agrège côté Langfuse ; inutile de rapatrier les observations pour les additionner :

files/src/assistant/__main__.py (extrait)
base = {"view": "observations", "metrics": [{"measure": "totalCost", "aggregation": "sum"},
                                             {"measure": "count", "aggregation": "count"}],
        "fromTimestamp": debut.isoformat(), "toTimestamp": fin.isoformat(),
        "orderBy": [{"field": "sum_totalCost", "direction": "desc"}], "config": {"row_limit": 10}}
for dimension in ("providedModelName", "userId"):
    rep = lf.api.metrics.metrics(query=json.dumps({**base, "dimensions": [{"field": dimension}]}))

userId a une forte cardinalité : Langfuse n'accepte de grouper dessus qu'en requête « top N » (limite explicite, tri décroissant sur une mesure).

3. Limiter ce qu'on garde

LANGFUSE_INIT_PROJECT_RETENTION=30 (leçon 1) supprime automatiquement les observations de plus de 30 jours. Pour un assistant qui manipule des données d'adhérents, c'est la durée de conservation qu'il faut pouvoir justifier au DPO. Les prompts, jeux de données et scores d'expérience, eux, ne contiennent pas de données de production et restent.

4. Arrêter proprement

make down arrête la pile et supprime ses volumes, puis vérifie qu'il ne reste aucun volume portant l'étiquette du projet Compose :

files/Makefile (extrait)
down:
	@$(COMPOSE) down -v --remove-orphans
	@left=$$(docker volume ls -q --filter label=com.docker.compose.project=lab-$(SLUG)); \
	  if [ -n "$$left" ]; then echo "Volumes restants : $$left"; exit 1; else echo "Pile et volumes lab-$(SLUG) supprimés."; fi

Résultat obtenu

make couts après make e2e, le 2026-10-15 :

Coût par providedModelName (7 j) :
  gemini-3.8-flash             0.025795 USD    16 observations
  gemini-3.5-flash-lite        0.003674 USD    23 observations
  None                         0.000000 USD   105 observations
Coût par userId (7 j) :
  None                         0.025795 USD    96 observations
  evaluation                   0.002221 USD    32 observations
  cons-0977                    0.001126 USD    12 observations
  cons-0412                    0.000327 USD     4 observations

Deux lectures : les 105 observations sans modèle sont des spans, agents et outils, sans coût par nature. Et 0,0258 USD n'est rattaché à aucun utilisateur : ce sont les appels du juge, exécutés par le SDK hors du bloc propagate_attributes de la tâche. L'exercice ci-dessous corrige cela.

Accueil du projet Langfuse : 20 traces, coût total 0,03 USD, gemini-3.8-flash 0,03 USD contre 0,00367 USD pour gemini-3.5-flash-lite, scores moyens

Capture réelle : le tableau d'accueil du projet après make e2e. Le tableau de bord « Langfuse Cost Dashboard » fourni par défaut affichait « Widget not found » sur cette instance 4.59.0 : l'accueil et l'API Metrics donnent les mêmes chiffres.

Le même make e2e tourne sur Vertex AI sans modifier le code : le client google-genai et ADK lisent GOOGLE_GENAI_USE_VERTEXAI. Rejoué ainsi le 2026-10-16 (location global, mêmes modèles) :

$ env -u GEMINI_API_KEY GOOGLE_GENAI_USE_VERTEXAI=TRUE GOOGLE_CLOUD_PROJECT=<projet> GOOGLE_CLOUD_LOCATION=global make e2e
production : {'moyenne_cite_reference': 0.125, 'moyenne_exactitude_juge': 0.8125}
staging    : {'moyenne_cite_reference': 0.875, 'moyenne_exactitude_juge': 0.78125}
staging promu en production (version 2)
OK : appli 2 générations (prompt conseiller-reponse v1), agent 5 générations, coût 0.001245 USD, 103 s

La dépense passe alors sur la facture Google Cloud du projet. Les prix déclarés dans Langfuse sont ceux de l'API Gemini : comparez-les à la grille Vertex AI avant de vous fier au coût affiché.

Puis l'arrêt, vérifié :

$ make down
 Volume lab-langfuse_langfuse_postgres_data  Removed
 Network lab-langfuse_default  Removed
Pile et volumes lab-langfuse supprimés.
$ docker ps -a --filter name=lab-langfuse -q | wc -l ; docker volume ls --filter name=lab-langfuse -q | wc -l
0
0

Ce qui casse en production

SymptômeCauseParade
Coût à 0 pour un modèlepas de définition, ou motif qui ne correspond pas au nom enregistré (models/…, alias, modèle servi)bouton Test Model Match dans les réglages ; motif tolérant au préfixe ; alerte « coût nul et usage non nul »
Coût sous-estimé d'un tiers sur un modèle qui réfléchitoutput_reasoning_tokens sans prixprix explicite pour chaque clé d'usage émise
Coût faux sur tout l'historique après correction d'un prixcalcul à l'ingestion, sans recalculcorriger vite ; pour l'historique, recalculer hors Langfuse à partir de l'usage exporté
Part importante du coût sans utilisateurappels hors contexte (juge, traitements par lot)propagate_attributes aussi autour des traitements internes, avec un user_id technique
Disque plein sur la VMClickHouse et MinIO sans rétentionrétention de projet, surveillance des volumes
Langfuse indisponible, l'application ralentitexport synchrone ou file pleinel'export du SDK est asynchrone et borné : le vérifier sous charge, ne jamais attendre Langfuse dans le chemin de réponse

Passer du lab à la production sur Google Cloud

Le fichier Compose convient à un pilote. Pour une instance d'équipe durable, l'éditeur fournit un module Terraform pour Google Cloud (langfuse/langfuse-terraform-gcp) : GKE pour les conteneurs web et worker, Cloud SQL pour PostgreSQL, Memorystore pour Redis, Cloud Storage pour le stockage objet, ClickHouse déployé dans le cluster. Ce projet ne crée aucune de ces ressources (coût d'un cluster), mais la correspondance est directe :

Lab (Compose)Production GCP
langfuse-web, langfuse-workerDeployments GKE, plusieurs réplicas, mises à jour progressives
PostgreSQL 17Cloud SQL, sauvegardes automatiques et récupération à un instant donné
Redis 7 noevictionMemorystore for Redis
MinIOCloud Storage (API compatible S3)
ClickHouseClickHouse dans GKE (ou service managé tiers), sauvegardes planifiées
.envSecret Manager
make down -vjamais : la suppression d'une instance de production est une décision, pas une commande de nettoyage

La règle de décision

Un coût n'est fiable que si chaque clé d'usage émise a un prix et si chaque modèle enregistré a une définition : vérifiez-le avec un test, et alertez sur toute génération dont l'usage est non nul et le coût nul. Attribuez chaque appel payant à un utilisateur, réel ou technique. Fixez la rétention le premier jour. Restez sur Compose tant qu'une interruption de quelques heures est acceptable ; au-delà, passez au module Terraform ou à Langfuse Cloud.

Pratique

Le 3 janvier 2027, le contrôleur de gestion compare la facture Google et le tableau Langfuse de décembre et de janvier. Décembre concorde, janvier est sous-estimé de moitié pour le juge. Que s'est-il passé ?

Le DPO demande de pouvoir garder un an les scores d'évaluation mais pas plus de 30 jours les conversations des conseillers. Comment l'organisez-vous ?

Exercice. Faites en sorte que les appels du juge soient attribués à un utilisateur technique juge-llm et portent le tag evaluation, puis vérifiez avec make couts que la ligne None ne porte plus de coût.

Voir la solution

Dans juge_exactitude, entourez la génération d'un bloc propagate_attributes :

from langfuse import propagate_attributes

with propagate_attributes(user_id="juge-llm", tags=["evaluation"]):
    with get_client().start_as_current_observation(as_type="generation", name="juge",
                                                   model=SETTINGS.judge_model, input=prompt) as gen:
        ...

Après make down && make up && make e2e && make couts, la ligne juge-llm porte l'essentiel du coût et None tombe à 0. Les spans sans modèle restent sans coût, ce qui est normal.

  • Je sais déclarer des prix de modèle couvrant toutes les clés d'usage, et le vérifier par un test.
  • Je sais pourquoi un changement de tarif doit être appliqué le jour même.
  • Je sais obtenir le coût par modèle et par conseiller sans exporter les observations.
  • Je sais fixer une rétention et arrêter le lab en supprimant tout, vérification comprise.
  • Je sais décrire la cible Google Cloud équivalente au fichier Compose.