Apogée Consult
Retour au blog
Jules Ginhac
Jules GinhacCo-Founder & ingénieur IA

Je suis Jules Ginhac, Co-Founder & ingénieur IA chez Apogée Consult à Lyon. Je conçois et déploie des architectures IA génératives (RAG, agents, LLMOps) pour des PME, startups et organisations publiques.

Pourquoi on logue absolument tout côté LLM, jusqu'à la dernière chaîne de pensée

  • ia-produit

Sans logs LLM détaillés, déboguer une réponse inattendue revient à enquêter sans preuves. Voici ce qu'on logue, pourquoi, et comment on structure ça avec Langfuse.

Pourquoi on logue absolument tout côté LLM, jusqu'à la dernière chaîne de pensée

Les premières semaines après le déploiement d'un produit LLM, vous recevez des retours du type "l'assistant a dit quelque chose d'étrange". Sans logs, votre enquête commence et finit au même endroit : "je ne sais pas ce qu'il s'est passé".

Nous avons appris à ne jamais déployer un appel LLM sans traçabilité complète. Voici pourquoi, et comment nous structurons ça.

Ce que le logging LLM couvre que votre APM ne couvre pas

Un APM classique (Datadog, New Relic) capture la latence de la requête, le code de statut HTTP, et les erreurs. C'est nécessaire mais insuffisant pour déboguer un comportement LLM.

Ce que l'APM ne capture pas :

  • Le prompt exact envoyé au modèle (avec toutes les variables interpolées).
  • La réponse complète du modèle (pas seulement le résumé).
  • Le nombre de tokens utilisés par segment (prompt, completion, cache hits).
  • L'état de chaque étape dans un pipeline multi-appels.
  • La chaîne de pensée (extended thinking) quand elle est activée.

Ce sont précisément ces données qui permettent de répondre à "pourquoi l'assistant a-t-il dit ça".

La structure de trace qu'on utilise

Nous utilisons Langfuse comme backend de traçabilité. Le concept central est la trace : une unité logique d'interaction, qui peut contenir plusieurs générations (appels LLM), observations (étapes intermédiaires), et events (actions sans appel LLM).

from langfuse import Langfuse
from langfuse.decorators import observe, langfuse_context

langfuse = Langfuse()

@observe()
async def process_user_request(user_id: str, message: str) -> str:
    # La trace est créée automatiquement par le décorateur
    langfuse_context.update_current_trace(
        user_id=user_id,
        tags=["production", "chat"],
        metadata={"message_length": len(message)}
    )

    # Chaque appel LLM imbriqué est une génération dans la trace
    response = await call_llm_with_tracing(message)
    return response

@observe(as_type="generation")
async def call_llm_with_tracing(message: str) -> str:
    prompt = build_prompt(message)

    langfuse_context.update_current_observation(
        input=prompt,
        model="claude-sonnet-4-5",
    )

    result = await anthropic_client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    )

    response_text = result.content[0].text
    langfuse_context.update_current_observation(
        output=response_text,
        usage={
            "input": result.usage.input_tokens,
            "output": result.usage.output_tokens,
            "cache_read_input_tokens": getattr(result.usage, "cache_read_input_tokens", 0)
        }
    )

    return response_text

Ce pattern capture automatiquement le prompt final, la réponse, et l'usage de tokens pour chaque appel. La trace parent lie toutes les générations à une session utilisateur identifiable.

Ce qu'on logue que la plupart des équipes ne loguent pas

Le prompt après interpolation, pas avant

La plupart des équipes loguent le template de prompt. Ce n'est pas suffisant. Si votre template contient {documents} et que les documents varient selon le contexte, la réponse étrange peut venir d'un document mal formaté, d'une troncature, ou d'un caractère spécial non échappé.

Nous loguons le prompt complet après interpolation, à chaque appel. La taille de stockage est le compromis accepté.

Les tokens de cache

Anthropic facture différemment les cache hits et les cache misses. Si votre prompt system est censé être mis en cache mais ne l'est pas (à cause d'un changement de version ou d'un bug de construction), vous payez deux fois plus sans le savoir.

def log_cache_efficiency(usage):
    cache_ratio = (
        usage.cache_read_input_tokens / usage.input_tokens
        if usage.input_tokens > 0 else 0
    )
    if cache_ratio < 0.3 and usage.input_tokens > 2000:
        logger.warning(
            "Cache hit rate faible sur un prompt long",
            extra={
                "cache_read_tokens": usage.cache_read_input_tokens,
                "total_input_tokens": usage.input_tokens,
                "cache_ratio": cache_ratio
            }
        )

La chaîne de pensée (extended thinking)

Quand nous utilisons le mode extended thinking de Claude, la chaîne de pensée est visible dans la réponse. Nous la loguons séparément, avec un flag pour distinguer les runs avec et sans thinking activé. C'est la donnée la plus utile pour comprendre pourquoi le modèle a pris une décision inattendue.

def extract_thinking(response) -> tuple[str, str]:
    thinking_blocks = []
    text_blocks = []

    for block in response.content:
        if block.type == "thinking":
            thinking_blocks.append(block.thinking)
        elif block.type == "text":
            text_blocks.append(block.text)

    return "\n\n".join(thinking_blocks), "\n\n".join(text_blocks)

thinking, answer = extract_thinking(response)
langfuse_context.update_current_observation(
    metadata={
        "thinking": thinking,  # Stocké dans metadata, pas dans output
        "thinking_length": len(thinking)
    },
    output=answer
)

Les alertes qu'on a construites sur les logs

Les traces ne servent pas qu'au débogage réactif. Nous avons quatre alertes actives sur Langfuse :

  1. Latence p95 > 8s sur les 5 dernières minutes : signal d'un problème de rate limiting ou de dégradation du provider.
  2. Taux d'erreur LLM > 2 % sur une fenêtre de 10 minutes : rate limit, timeout, ou bug de prompt.
  3. Coût journalier > seuil : contrôle budgétaire, détecte les boucles infinies d'agents.
  4. Score d'évaluation < 3/5 sur les feedbacks utilisateurs liés à une trace : permet de croiser le feedback avec le prompt exact qui l'a produit.

Ce dernier point est le plus précieux. Quand un utilisateur clique "mauvaise réponse", nous récupérons automatiquement la trace associée dans Langfuse et nous pouvons voir exactement ce que le modèle a reçu comme contexte.

Ce qu'on ne logue pas

Deux catégories de données sont exclues des traces :

Les données personnelles identifiables. Avant l'interpolation dans le prompt, nous remplaçons les PII (noms, emails, numéros de téléphone) par des placeholders dans les traces. La réponse complète est loguée, mais anonymisée si nécessaire.

Les secrets et credentials. Si votre pipeline passe des clés API ou des tokens dans le contexte LLM (ce que vous devriez éviter), ils ne doivent jamais apparaître dans les traces.

import re

PII_PATTERNS = [
    (r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]'),
    (r'\b\d{10}\b', '[PHONE]'),
    (r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b', '[CARD]'),
]

def anonymize_for_logging(text: str) -> str:
    result = text
    for pattern, replacement in PII_PATTERNS:
        result = re.sub(pattern, replacement, result)
    return result

La question du coût de stockage

Stocker tous les prompts complets coûte. Sur notre produit, avec des prompts moyens de 2 000 tokens et 500 requêtes par jour, ça représente environ 1 Mo de texte brut par jour, soit 365 Mo par an. Avec une rétention de 90 jours, le volume est gérable.

Le coût de ne pas avoir ces logs est plus difficile à chiffrer, mais nous l'avons mesuré indirectement : les incidents qui auraient nécessité une heure de débogage avec logs se résolvent en 5 minutes. Sur un an, nous estimons que les traces ont économisé environ 40 heures de debugging collectif.

La question que nous n'avons pas encore résolue : comment réduire le volume de logs sans perdre la capacité de débogage ? Échantillonner à 10 % serait insuffisant pour les cas rares, ce sont précisément les cas rares qui posent problème. Le sampling intelligent (100 % sur les erreurs, 10 % sur les succès) est notre piste actuelle, mais nous ne l'avons pas encore mis en production.

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
Logging LLM : pourquoi tout logger jusqu'aux traces | Apogée Consult