Logging semantique des agents

Les logs semantiques structurent les evenements d'agents pour l'analyse.
Sur cette page
  1. Idee en 30 secondes
  2. Probleme principal
  3. Comment ca fonctionne
  4. Vocabulaire minimal des evenements
  5. Quand l'utiliser
  6. Exemple d'implementation
  7. Erreurs typiques
  8. Evenements nommes differemment selon les services
  9. Texte libre au lieu de champs normalises
  10. Absence de event_version
  11. Raw prompts ou raw args logges sans redaction
  12. Auto-verification
  13. FAQ
  14. Pages liees

Idee en 30 secondes

Le logging semantique (semantic logging) pour les agents signifie que les evenements ont non seulement un format JSON, mais aussi un sens stable.

Cela veut dire que des etapes equivalentes dans differents runs sont loggees de la meme facon : meme event, memes champs cles, memes statuts.

Cela rend les logs exploitables pour la recherche, les alertes, l'analytique et le debugging en production.

Probleme principal

Beaucoup d'equipes ecrivent deja des logs structures, mais cela ne suffit souvent pas.

Dans differents services et versions d'agent, un meme evenement peut avoir des noms et champs differents : tool_called, call_tool, tool.invoke. Resultat : les logs existent, mais comparer les runs est difficile.

Le logging semantique est une solution au niveau du design des evenements, pas seulement une couche technique sur les logs.

En production, cela ressemble souvent a :

  • une requete dans le systeme de logs renvoie trop de bruit ;
  • les alertes deviennent instables a cause de noms d'evenements differents ;
  • en incident, il faut faire une correspondance manuelle entre plusieurs formats d'evenements.

C'est pourquoi les systemes agentiques ont besoin d'un vocabulaire d'evenements partage et d'un schema de champs stable.

Comment ca fonctionne

Le logging semantique repose sur trois elements :

  • un vocabulaire d'evenements coherent (event taxonomy) ;
  • des champs stables pour chaque evenement ;
  • des valeurs normalisees (status, error_class, stop_reason).

event taxonomy est un contrat entre runtime, logs, dashboards et alertes. Si ce contrat est casse, l'observability se casse aussi. status utilise en general un ensemble limite de valeurs (par exemple : ok, error, timeout, cancelled; dans cet exemple on utilise la version simplifiee : ok / error).

Le logging semantique ne remplace pas le tracing, il le complete. Il rend les evenements non seulement visibles, mais aussi comparables entre services et releases. Le logging repond a "que s'est-il passe", le tracing repond a "comment cela s'est passe", et le semantic logging repond a "ce que cela signifie".

Vocabulaire minimal des evenements

EvenementSens semantiqueChamps cles
run_startedun nouveau run a demarrerun_id, trace_id, request_id, task_hash
agent_stepl'agent passe a l'etape suivantestep_index, step_type, actor
tool_calldebut d'un appel d'outiltool_name, args_hash
tool_resultresultat d'un appel d'outiltool_name, latency_ms, status, error_class
llm_resultresultat d'une etape modelemodel, token_usage, latency_ms, status
policy_decisiondecision policy/guardrailsrule_id, decision, reason_code
run_finishedrun terminestop_reason, total_steps, total_latency_ms

policy_decision permet de voir non seulement les erreurs, mais aussi les causes de blocage et decisions de guardrails.

event_version permet de faire evoluer le schema d'evenements sans casser dashboards et alertes existants.

Quand l'utiliser

Le logging semantique complet n'est pas toujours necessaire.

Pour un scenario single-shot simple sans tools ni boucle d'execution, des logs de base suffisent souvent.

Mais le logging semantique devient critique quand :

  • le systeme contient plusieurs agents ou services ;
  • il faut construire des alertes et dashboards stables ;
  • il est important de comparer le comportement entre releases ;
  • les incidents doivent etre analyses vite, sans mapping manuel d'evenements.

Exemple d'implementation

Ci-dessous un exemple simplifie de logging semantique dans la runtime. L'idee est simple : logger uniquement des evenements du vocabulaire valide et normaliser les valeurs de champs.

PYTHON
import hashlib
import json
import logging
import time
import uuid
from enum import StrEnum

logger = logging.getLogger("agent")


class EventName(StrEnum):
    RUN_STARTED = "run_started"
    AGENT_STEP = "agent_step"
    TOOL_CALL = "tool_call"
    TOOL_RESULT = "tool_result"
    LLM_RESULT = "llm_result"
    POLICY_DECISION = "policy_decision"
    RUN_FINISHED = "run_finished"


def stable_hash(value):
    # default=str donne une compatibilite de base
    # dans les systemes critiques, preferer une serialisation explicite (par ex. ISO 8601)
    payload = json.dumps(
        value,
        sort_keys=True,
        ensure_ascii=False,
        default=str,
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()


def normalize_status(ok):
    return "ok" if ok else "error"


def normalize_error(error):
    if error is None:
        return None
    return type(error).__name__


def log_semantic(event_name: EventName, **fields):
    logger.info(
        event_name.value,
        extra={
            "event": event_name.value,
            "event_version": 1,
            "timestamp_ms": int(time.time() * 1000),
            **fields,
        },
    )


def run_agent(agent, task, request_id=None):
    run_id = str(uuid.uuid4())
    trace_id = str(uuid.uuid4())
    started_at = time.time()
    step_index = 0
    stop_reason = "max_steps"
    run_status = "ok"

    log_semantic(
        EventName.RUN_STARTED,
        run_id=run_id,
        trace_id=trace_id,
        request_id=request_id,
        task_hash=stable_hash(task),
    )

    try:
        for step in agent.iter(task):  # step: reasoning ou tool execution
            step_index += 1
            step_started_at = time.time()
            step_type = step.type
            tool_name = getattr(step, "tool_name", None)

            log_semantic(
                EventName.AGENT_STEP,
                run_id=run_id,
                trace_id=trace_id,
                step_index=step_index,
                step_type=step_type,
                actor=getattr(step, "actor", "agent_runtime"),
            )

            if step_type == "tool_call":
                args = getattr(step, "args", {})
                log_semantic(
                    EventName.TOOL_CALL,
                    run_id=run_id,
                    trace_id=trace_id,
                    step_index=step_index,
                    tool_name=tool_name,
                    args_hash=stable_hash(args),
                )

            try:
                result = step.execute()
                latency_ms = int((time.time() - step_started_at) * 1000)

                if step_type == "tool_call":
                    log_semantic(
                        EventName.TOOL_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        tool_name=tool_name,
                        latency_ms=latency_ms,
                        status=normalize_status(True),
                        error_class=None,
                    )
                else:
                    log_semantic(
                        EventName.LLM_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        model=getattr(step, "model", None),
                        token_usage=getattr(result, "token_usage", None),
                        latency_ms=latency_ms,
                        status=normalize_status(True),
                    )

                # policy_decision est loggee apres l'execution du step
                # (quand resultat ou erreur est connu)
                if getattr(step, "policy_decision", None) is not None:
                    decision = step.policy_decision
                    log_semantic(
                        EventName.POLICY_DECISION,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        rule_id=decision.rule_id,
                        decision=decision.value,
                        reason_code=decision.reason_code,
                    )

            except Exception as error:
                latency_ms = int((time.time() - step_started_at) * 1000)
                run_status = "error"

                if step_type == "tool_call":
                    stop_reason = "tool_error"
                    log_semantic(
                        EventName.TOOL_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        tool_name=tool_name,
                        latency_ms=latency_ms,
                        status=normalize_status(False),
                        error_class=normalize_error(error),
                    )
                else:
                    stop_reason = "step_error"
                    log_semantic(
                        EventName.LLM_RESULT,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        model=getattr(step, "model", None),
                        latency_ms=latency_ms,
                        status=normalize_status(False),
                        error_class=normalize_error(error),
                    )

                if getattr(step, "policy_decision", None) is not None:
                    decision = step.policy_decision
                    log_semantic(
                        EventName.POLICY_DECISION,
                        run_id=run_id,
                        trace_id=trace_id,
                        step_index=step_index,
                        rule_id=decision.rule_id,
                        decision=decision.value,
                        reason_code=decision.reason_code,
                    )

                raise

            if result.is_final:
                stop_reason = "completed"
                break

    finally:
        log_semantic(
            EventName.RUN_FINISHED,
            run_id=run_id,
            trace_id=trace_id,
            status=run_status,
            stop_reason=stop_reason,
            total_steps=step_index,
            total_latency_ms=int((time.time() - started_at) * 1000),
        )

En production, ces evenements sont generalement envoyes vers un systeme de logs centralise (par exemple ELK, Datadog ou ClickHouse), ou ils servent a construire requetes, dashboards et alertes.

Par exemple, un semantic event en JSON peut ressembler a :

JSON
{
  "timestamp_ms": 1774106220000,
  "event": "policy_decision",
  "event_version": 1,
  "run_id": "run_9fd2",
  "trace_id": "tr_9fd2",
  "step_index": 3,
  "rule_id": "email_external_domain",
  "decision": "deny",
  "reason_code": "missing_user_confirmation"
}

Erreurs typiques

Meme avec des logs structures, le logging semantique casse souvent a cause des erreurs ci-dessous.

Evenements nommes differemment selon les services

Quand la meme action a des noms event differents, les requetes deviennent instables. Cela rend plus difficile la detection d'un echec d'outil ou d'une phase precoce de spam d'outils.

Texte libre au lieu de champs normalises

Des champs comme "error": "something failed" sont peu exploitables en analytique. Mieux vaut des champs separes status, error_class, reason_code avec des valeurs fixes.

Absence de event_version

Sans version d'evenement, les changements de schema cassent silencieusement dashboards, saved queries et alertes. L'evolution du schema doit donc etre explicite.

Raw prompts ou raw args logges sans redaction

C'est un risque de securite et de conformite. Plus sur : stocker un hash ou une version anonymisee des champs.

Auto-verification

Ci-dessous, une checklist courte de base pour le logging semantique avant release.

Progression: 0/9

⚠ L'observability de base manque

Le système sera difficile à déboguer en production. Commencez par run_id, structured logs et tracing des tool calls.

FAQ

Q : Quelle difference entre semantic logging et JSON-logging classique ?
R : JSON-logging definit seulement le format. Semantic logging definit le sens : noms d'evenements stables, champs stables, valeurs normalisees.

Q : Est-ce que semantic logging remplace le tracing ?
R : Non. Le tracing montre le chemin d'execution, tandis que semantic logging rend les evenements de ce chemin exploitables pour recherche, alertes et analytique.

Q : Quel minimum de semantic logging faut-il pour un premier release production ?
R : Vocabulaire de base (run_started, tool_call, tool_result, run_finished), run_id/trace_id stables, status, error_class et stop_reason.

Q : Faut-il migrer tous les anciens logs immediatement ?
R : Non. Mieux vaut commencer par les nouveaux evenements et les chemins critiques, puis migrer progressivement les anciens formats.

Pages liees

Suite du sujet :

⏱️ 7 min de lectureMis à jour 20 mars 2026Difficulté: ★★★
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Permissions outils (allowlist / blocklist)
  • Kill switch & arrêt incident
  • Idempotence & déduplication
  • Audit logs & traçabilité
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.

Auteur

Nick — ingénieur qui construit une infrastructure pour des agents IA en production.

Focus : patterns d’agents, modes de défaillance, contrôle du runtime et fiabilité des systèmes.

🔗 GitHub: https://github.com/mykolademyanov


Note éditoriale

Cette documentation est assistée par l’IA, avec une responsabilité éditoriale humaine pour l’exactitude, la clarté et la pertinence en production.

Le contenu s’appuie sur des défaillances réelles, des post-mortems et des incidents opérationnels dans des systèmes d’agents IA déployés.