As Large Language Model (LLM) applications evolve from simple question-answering bots to complex Multi-Agent Retrieval-Augmented Generation (RAG) systems, observability has become a critical bottleneck. In a single-agent RAG setup, a request flows linearly: query, retrieve, generate. However, in multi-agent architectures, requests fork into parallel sub-agents, each performing distinct tasks like document parsing, semantic search, or fact verification. This complexity leads to trace fragmentation, where a single user request is scattered across dozens of unrelated spans, making debugging nearly impossible.
Traditional logging falls short because it lacks context. This is where OpenTelemetry (OTel) shines. By implementing custom instrumentation patterns, you can stitch these fragmented traces back together, providing a unified view of your AI infrastructure. In this post, we explore how to structure these patterns using Python.
The Challenge of Distributed AI Workflows
Imagine a customer support system with three agents: a Triage Agent, a Knowledge Retrieval Agent, and a Response Generator. When a user submits a query, the Triage Agent determines the intent and delegates tasks. If the intent is technical, the Knowledge Agent searches a vector database; if it’s billing-related, it queries a SQL database.
Without proper correlation, the spans generated by these agents appear as isolated islands in your APM (Application Performance Monitoring) dashboard. You cannot see that the high latency in the Response Generator was caused by a timeout in the Knowledge Agent’s vector search. To solve this, we need to propagate context explicitly across agent boundaries.
Implementing Custom Instrumentation with OpenTelemetry
The core strategy involves creating a custom wrapper that captures the execution context of each agent. We use the opentelemetry-api to manually manage spans, ensuring that nested calls within an agent’s internal logic are grouped under a single parent span. This is particularly useful when your agents use multiple libraries (e.g., LangChain, LlamaIndex, or custom HTTP clients) that may not automatically instrument well.
Below is a practical example of a custom decorator that wraps an agent’s execution method, ensuring a consistent trace structure:
import functools
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
tracer = trace.get_tracer(__name__)
def agent_span(agent_name: str):
"""
A decorator that wraps an agent's method in a dedicated OpenTelemetry span.
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
with tracer.start_as_current_span(f"agent.{agent_name}") as span:
try:
# Set attributes for better filtering in APM tools
span.set_attribute("agent.name", agent_name)
span.set_attribute("args", str(args)[:100]) # Sanitize inputs
# Execute the agent's logic
result = func(*args, **kwargs)
# Mark success
span.set_status(Status(StatusCode.OK))
return result
except Exception as e:
# Mark failure and record the error
span.set_status(Status(StatusCode.ERROR, str(e)))
span.record_exception(e)
raise
return wrapper
return decorator
# Usage Example
class KnowledgeRetrievalAgent:
@agent_span("knowledge_retrieval")
def search_docs(self, query: str):
# Simulate expensive vector DB lookup
return {"doc_id": "123", "content": "The answer is 42."}
In this pattern, the @agent_span decorator acts as a container for all internal operations performed by that specific agent. When you chain multiple agents together, the child spans (from the internal logic) are automatically nested within the agent’s span, creating a hierarchical view rather than a flat list of events.
Correlating Spans Across Service Boundaries
For multi-agent systems deployed as microservices, context propagation is vital. You must ensure that the TraceId and SpanId are passed via headers (e.g., HTTP headers in REST APIs or message headers in Kafka). OpenTelemetry’s context propagation utilities handle this automatically if your HTTP client or server instrumentation is enabled. However, for custom message brokers or internal event buses, you may need to manually inject the context carrier:
from opentelemetry.propagate import inject
def send_message_to_agent(queue, message, agent_name):
# Inject trace context into the message metadata
headers = {}
inject(headers)
queue.publish({
"agent": agent_name,
"payload": message,
"trace_headers": headers
})
Conclusion
Solving trace fragmentation in Multi-Agent RAG systems is not just about collecting more data; it’s about structuring that data intelligently. By adopting custom instrumentation patterns with OpenTelemetry, developers can gain granular visibility into agent interactions, identify bottlenecks across parallel workflows, and ultimately build more reliable AI applications. As these systems grow in complexity, observability will transition from a nice-to-have feature to the backbone of operational excellence.