Distributed Tracing für Agenten: Tracing von Multi-Agenten-Systemen

Distributed Tracing verfolgt einen Run über mehrere Services, Queues, Tools und LLM-Provider hinweg und erhält den Trace Context end-to-end.
Auf dieser Seite
  1. Idee in 30 Sekunden
  2. Hauptproblem
  3. Wie es funktioniert
  4. Wie ein Distributed Trace aussieht
  5. Wann einsetzen
  6. Implementierungsbeispiel
  7. Typische Fehler
  8. Neue trace_id in jedem Service
  9. Nur trace_id, ohne Span-Beziehungen
  10. Kontextverlust in Async-Queues
  11. Fehlendes service_name und operation_name
  12. Selbstcheck
  13. FAQ
  14. Verwandte Seiten

Idee in 30 Sekunden

Distributed Tracing zeigt einen Run nicht nur innerhalb eines einzelnen Services, sondern über die gesamte Aufrufkette.

In Multi-Agenten-Systemen läuft eine Anfrage oft durch Gateway, Runtime, Tools, Queues und LLM-Provider.

Distributed Tracing verknüpft diese Schritte über trace_id, span_id und parent_span_id, sodass das Verhalten end-to-end sichtbar wird.

Hauptproblem

Wenn ein Agent über mehrere Services läuft, sind Logs meistens verteilt.

Gateway-Fehler sieht man separat, Tool-Service-Fehler separat, Agent-Runtime separat. Aber diese Events sind nicht als ein Run verbunden. Ohne gemeinsamen trace context ist schwer zu erkennen, dass es derselbe Run ist.

Dadurch wird selbst ein einfacher Incident zu einer langen Untersuchung:

  • unklar, in welchem Service die Verzögerung entstand;
  • unklar, wo der Kontext verloren ging;
  • schwer, Retries serviceübergreifend zu verbinden;
  • schwer, den vollständigen Pfad eines problematischen Runs zu rekonstruieren.

Deshalb brauchen Multi-Agenten-Systeme Distributed Tracing und nicht nur lokales Tracing innerhalb einer Runtime.

Wie es funktioniert

Distributed Tracing nutzt dasselbe Modell aus trace und span, aber über mehrere Services hinweg.

  • trace — der gesamte Pfad einer Anfrage über alle Services
  • span — eine konkrete Operation in einem Service

In realen Systemen basieren diese Felder meist auf OpenTelemetry (OTel):

  • trace_id — gemeinsame ID für den gesamten Pfad
  • span_id — ID des aktuellen Schritts
  • parent_span_id — Verbindung zum Eltern-Schritt
  • service_name — wo der Schritt ausgeführt wurde
  • operation_name — was der Service gemacht hat

Damit der Trace nicht abreißt, muss trace context bei jedem Aufruf zwischen Services weitergegeben werden. Üblicherweise geschieht das über Headers (traceparent) oder Metadata in Queue-Nachrichten.

Wie ein Distributed Trace aussieht

Distributed Tracing versteht man am einfachsten über ein Anfragebeispiel.

TEXT
trace_id: tr_7a31
user_query: "Find vendor invoices for March"

gateway         span g1   parent=-     18ms   status=ok
agent_runtime   span a1   parent=g1   240ms   status=ok
tool_service    span t1   parent=a1   410ms   status=ok
agent_runtime   span a2   parent=a1   130ms   status=ok
llm_provider    span l1   parent=a2   690ms   status=ok

stop_reason: completed

Ein solcher Trace zeigt:

  • den vollständigen Pfad zwischen Services;
  • welcher Service die größte Latenz verursacht;
  • wo genau der Kontext abgerissen ist (falls passiert);
  • welche Spans Eltern und welche Kinder sind.

Wann einsetzen

Distributed Tracing ist nicht immer erforderlich.

Wenn das System monolithisch ist und der gesamte Run in einem Prozess lebt, reicht lokales Tracing oft aus.

Distributed Tracing wird aber kritisch, wenn:

  • eine Anfrage mehrere Services durchläuft;
  • der Workflow Queues oder Async-Worker enthält;
  • mehrere Agenten Events austauschen;
  • präzise Analyse von Latenz und Retries zwischen Services nötig ist.

Implementierungsbeispiel

Unten ist ein vereinfachtes Beispiel, wie trace context zwischen Gateway und Worker-Service weitergegeben wird. Im Beispiel werden vereinfachte Header (x-trace-id, x-parent-span-id) genutzt, um das Prinzip zu zeigen. In Production werden meist standardisierte W3C-Header traceparent (über OpenTelemetry) verwendet, die den Trace Context automatisch zwischen Services weitergeben.

PYTHON
import contextvars
import logging
import time
import uuid

logger = logging.getLogger("distributed-tracing")
trace_id_ctx = contextvars.ContextVar("trace_id", default=None)
span_id_ctx = contextvars.ContextVar("span_id", default=None)


def start_span(service_name, operation_name, parent_span_id=None):
    span_id = str(uuid.uuid4())
    started_at = time.time()
    logger.info(
        "span_started",
        extra={
            "trace_id": trace_id_ctx.get(),
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "service_name": service_name,
            "operation_name": operation_name,
        },
    )
    return span_id, started_at


def finish_span(service_name, operation_name, span_id, started_at, status, parent_span_id=None, error=None):
    logger.info(
        "span_finished",
        extra={
            "trace_id": trace_id_ctx.get(),
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "service_name": service_name,
            "operation_name": operation_name,
            "status": status,
            "latency_ms": int((time.time() - started_at) * 1000),
            "error": error,
        },
    )


def inject_context(headers):
    headers["x-trace-id"] = trace_id_ctx.get() or ""
    headers["x-parent-span-id"] = span_id_ctx.get() or ""


def extract_context(headers):
    incoming_trace_id = headers.get("x-trace-id") or str(uuid.uuid4())
    incoming_parent_span_id = headers.get("x-parent-span-id")
    trace_token = trace_id_ctx.set(incoming_trace_id)
    return incoming_parent_span_id, trace_token


def gateway_handle_request():
    trace_id = str(uuid.uuid4())
    trace_token = trace_id_ctx.set(trace_id)

    root_span_id, root_started_at = start_span("gateway", "handle_request", parent_span_id=None)
    span_token = span_id_ctx.set(root_span_id)

    try:
        headers = {}
        inject_context(headers)
        call_worker_service(headers)  # beispielhafter HTTP/gRPC-Aufruf zu worker_handle_request in einem anderen Service
        finish_span(
            "gateway",
            "handle_request",
            root_span_id,
            root_started_at,
            status="ok",
            parent_span_id=None,
        )
    except Exception as error:
        finish_span(
            "gateway",
            "handle_request",
            root_span_id,
            root_started_at,
            status="error",
            parent_span_id=None,
            error=str(error),
        )
        raise
    finally:
        span_id_ctx.reset(span_token)
        trace_id_ctx.reset(trace_token)


def worker_handle_request(headers):
    parent_span_id, trace_token = extract_context(headers)
    span_id, started_at = start_span("worker", "process_task", parent_span_id=parent_span_id)
    span_token = span_id_ctx.set(span_id)

    try:
        # ... Agent-Arbeit, Tool-Calls, LLM-Schritte ...
        finish_span("worker", "process_task", span_id, started_at, status="ok", parent_span_id=parent_span_id)
    except Exception as error:
        finish_span(
            "worker",
            "process_task",
            span_id,
            started_at,
            status="error",
            parent_span_id=parent_span_id,
            error=str(error),
        )
        raise
    finally:
        span_id_ctx.reset(span_token)
        trace_id_ctx.reset(trace_token)

Selbst der manuelle Ansatz oben hilft, die Grundmechanik von Distributed Tracing zu verstehen.

Im realen Workflow erstellt jeder Service meist seinen eigenen Span und gibt dessen span_id als parent_span_id für den nächsten Hop weiter. Wenn dieser Schritt fehlt, startet der nächste Service einen neuen Trace und das end-to-end Bild bricht.

Zum Beispiel kann ein Span-Event im JSON-Log so aussehen:

JSON
{
  "timestamp": "2026-03-21T15:17:00Z",
  "event": "span_finished",
  "trace_id": "tr_7a31",
  "span_id": "sp_worker_02",
  "parent_span_id": "sp_gateway_01",
  "service_name": "worker",
  "operation_name": "process_task",
  "latency_ms": 410,
  "status": "ok"
}

Typische Fehler

Auch wenn Distributed Tracing bereits vorhanden ist, bleiben in Production oft typische Probleme.

Neue trace_id in jedem Service

Wenn jeder Service eine eigene trace_id erzeugt, zerfällt der end-to-end Trace in Fragmente. In diesem Modus ist es schwer, die Ursache eines serviceübergreifenden Incidents zu lokalisieren.

Nur trace_id, ohne Span-Beziehungen

trace_id ohne span_id und parent_span_id ergibt nur eine flache Eventliste. Ohne Span-Baum ist schwer zu verstehen, welche Schritte verschachtelt waren.

Kontextverlust in Async-Queues

Wenn Queue-Metadata keinen trace context enthält, fallen Teile des Workflows aus dem Trace. Solche Lücken maskieren oft eine frühe Phase von teilweisem Ausfall oder kaskadierenden Ausfällen.

Fehlendes service_name und operation_name

Ohne diese Felder sieht man zwar einen Fehler, aber nicht in welchem Service und welcher Operation. Dadurch dauert Debugging deutlich länger.

Selbstcheck

Unten ist eine kurze Checkliste für grundlegendes Distributed Tracing vor dem Release.

Fortschritt: 0/9

⚠ Grundlegende Observability fehlt

Das System wird in production schwer zu debuggen sein. Starten Sie mit run_id, structured logs und tracing von tool calls.

FAQ

Q: Worin unterscheidet sich Distributed Tracing von normalem Agent Tracing?
A: Agent Tracing zeigt Schritte innerhalb einer Runtime. Distributed Tracing verbindet Schritte über mehrere Services zu einem end-to-end Trace.

Q: Was geht kaputt, wenn zwischen Services nur trace_id, aber nicht span_id und parent_span_id propagiert wird?
A: Dann wird der Trace flach: Man sieht, dass Events zu einem Pfad gehören, aber es ist schwer zu erkennen, welche Schritte verschachtelt waren, welcher Service wen aufgerufen hat und wo genau Verzögerung oder Fehler entstanden. trace_id verbindet den Gesamtpfad, span_id und parent_span_id bauen den Schrittbaum.

Q: Wie überträgt man Trace Context über Queues?
A: trace_id, span_id und parent_span_id in Message-Metadata aufnehmen. Sonst fallen Async-Schritte aus dem Trace.

Q: Muss man OpenTelemetry sofort einführen?
A: Nein. Man kann mit manueller Propagation und strukturierten Logs starten und später auf OTel SDK wechseln, wenn das System wächst.

Verwandte Seiten

Weiter zum Thema:

⏱️ 6 Min. LesezeitAktualisiert 21. März 2026Schwierigkeit: ★★★
Integriert: Production ControlOnceOnly
Guardrails für Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Tool-Permissions (Allowlist / Blocklist)
  • Kill switch & Incident Stop
  • Idempotenz & Dedupe
  • Audit logs & Nachvollziehbarkeit
Integrierter Hinweis: OnceOnly ist eine Control-Layer für Production-Agent-Systeme.

Autor

Nick — Engineer, der Infrastruktur für KI-Agenten in Produktion aufbaut.

Fokus: Agent-Patterns, Failure-Modes, Runtime-Steuerung und Systemzuverlässigkeit.

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


Redaktioneller Hinweis

Diese Dokumentation ist KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Der Inhalt basiert auf realen Ausfällen, Post-Mortems und operativen Vorfällen in produktiv eingesetzten KI-Agenten-Systemen.