引言
一次Agent对话可能涉及路由决策、向量检索、工具调用、LLM推理等十几个步骤,跨越多个微服务。当用户反馈"Agent回复变慢了"时,如果没有链路追踪,定位瓶颈就像大海捞针。OpenTelemetry + Jaeger的组合为Agent系统提供了端到端的请求追踪能力,让每一次对话的完整路径都清晰可见。
OpenTelemetry架构
┌──────────────────────────────────────────────────────────┐
│ OpenTelemetry Architecture │
│ │
│ Agent Services │
│ ┌─────────────────────────────────────────────┐ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │Router│ │ Tool │ │ LLM │ │Memory│ │ │
│ │ │Service│ │Service│ │Service│ │Service│ │ │
│ │ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌──▼─────────▼─────────▼─────────▼───┐ │ │
│ │ │ OpenTelemetry SDK (Instrument) │ │ │
│ │ └──────────────┬─────────────────────┘ │ │
│ └─────────────────┼───────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ OTLP Exporter │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ OTel Collector │ │
│ │ (接收+处理+导出) │ │
│ └────┬───────┬────┘ │
│ │ │ │
│ ┌──────▼┐ ┌──▼──────┐ │
│ │Jaeger │ │Prometheus│ │
│ │(Traces)│ │(Metrics) │ │
│ └───────┘ └─────────┘ │
└──────────────────────────────────────────────────────────┘
SDK初始化
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.grpc import GrpcInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
def setup_tracing(service_name: str, otlp_endpoint: str = "otel-collector:4317"):
"""初始化OpenTelemetry追踪"""
resource = Resource.create({
"service.name": service_name,
"service.version": "2.0.0",
"deployment.environment": "production",
})
provider = TracerProvider(resource=resource)
# OTLP导出器
exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True)
processor = BatchSpanProcessor(
exporter,
max_queue_size=2048,
max_export_batch_size=512,
export_timeout_millis=30000,
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
# 自动注入HTTP/gRPC调用
GrpcInstrumentor().instrument()
HTTPXClientInstrumentor().instrument()
return trace.get_tracer(service_name)
Agent Span设计
class AgentTracer:
"""Agent专用追踪器"""
def __init__(self, tracer):
self.tracer = tracer
async def trace_request(
self,
session_id: str,
user_input: str,
handler: callable
):
"""追踪完整请求链路"""
with self.tracer.start_as_current_span(
"agent.request",
attributes={
"session.id": session_id,
"input.length": len(user_input),
"input.language": self._detect_language(user_input),
}
) as request_span:
try:
result = await handler()
request_span.set_attribute(
"response.length", len(result.get("response", ""))
)
request_span.set_attribute(
"response.quality_score",
result.get("quality_score", 0)
)
request_span.set_status(trace.StatusCode.OK)
return result
except Exception as e:
request_span.record_exception(e)
request_span.set_status(
trace.Status(trace.StatusCode.ERROR, str(e))
)
raise
async def trace_routing(
self,
user_input: str,
routing_fn: callable
):
"""追踪路由决策"""
with self.tracer.start_as_current_span(
"agent.route",
attributes={"input.preview": user_input[:100]}
) as span:
decision = await routing_fn()
span.set_attributes({
"route.model": decision.get("model", ""),
"route.tools": ",".join(decision.get("tools", [])),
"route.confidence": decision.get("confidence", 0),
"route.reason": decision.get("reason", ""),
})
return decision
async def trace_tool_call(
self,
tool_name: str,
params: dict,
executor: callable
):
"""追踪工具调用"""
with self.tracer.start_as_current_span(
f"tool.{tool_name}",
attributes={
"tool.name": tool_name,
"tool.params_hash": hashlib.md5(
json.dumps(params, sort_keys=True).encode()
).hexdigest()[:8],
}
) as span:
start = time.monotonic()
try:
result = await executor()
latency_ms = (time.monotonic() - start) * 1000
span.set_attributes({
"tool.latency_ms": latency_ms,
"tool.success": True,
"tool.result_size": len(str(result)),
})
return result
except Exception as e:
span.set_attributes({
"tool.success": False,
"tool.error": str(e)[:200],
})
span.record_exception(e)
raise
async def trace_llm_call(
self,
model: str,
prompt: str,
generator: callable
):
"""追踪LLM推理"""
with self.tracer.start_as_current_span(
f"llm.{model}",
attributes={
"llm.model": model,
"llm.prompt_length": len(prompt),
}
) as span:
start = time.monotonic()
result = await generator()
latency_ms = (time.monotonic() - start) * 1000
span.set_attributes({
"llm.latency_ms": latency_ms,
"llm.prompt_tokens": result.get("usage", {}).get("prompt_tokens", 0),
"llm.completion_tokens": result.get("usage", {}).get("completion_tokens", 0),
"llm.total_tokens": result.get("usage", {}).get("total_tokens", 0),
"llm.finish_reason": result.get("finish_reason", ""),
})
return result
Span层级示例
agent.request (session=abc123) [2000ms]
├── agent.route [15ms]
│ ├── embedding.generate [8ms]
│ └── similarity.match [5ms]
├── memory.retrieve [50ms]
│ ├── vector.search [30ms]
│ └── context.assemble [15ms]
├── tool.search [800ms]
│ ├── http.get [600ms]
│ └── result.parse [50ms]
├── tool.calculator [20ms]
├── llm.gpt-4o [1100ms]
│ ├── prompt.build [5ms]
│ ├── api.call [1050ms]
│ └── response.parse [30ms]
└── response.format [15ms]
上下文传播
from opentelemetry.propagate import inject, extract
from opentelemetry.trace import get_current_span
class TraceContextPropagator:
"""跨服务Trace上下文传播"""
@staticmethod
def inject_to_headers(headers: dict = None) -> dict:
"""注入trace context到HTTP头"""
headers = headers or {}
inject(headers)
return headers
@staticmethod
def extract_from_headers(headers: dict):
"""从HTTP头提取trace context"""
return extract(headers)
@staticmethod
def get_current_trace_id() -> str:
"""获取当前trace ID"""
span = get_current_span()
if span and span.is_recording():
return format(span.get_span_context().trace_id, "032x")
return ""
@staticmethod
def get_current_span_id() -> str:
"""获取当前span ID"""
span = get_current_span()
if span and span.is_recording():
return format(span.get_span_context().span_id, "016x")
return ""
# 在gRPC metadata中传播
class GrpcTraceInterceptor:
"""gRPC trace拦截器"""
async def intercept(self, method, request, context):
# 从metadata提取trace context
metadata = dict(context.invocation_metadata())
trace_context = TraceContextPropagator.extract_from_headers(metadata)
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(
f"grpc.{method}",
context=trace_context,
) as span:
# 注入trace context到响应metadata
response_metadata = TraceContextPropagator.inject_to_headers()
context.set_trailing_metadata(
[(k, v) for k, v in response_metadata.items()]
)
return await method(request, context)
Jaeger查询与分析
class JaegerAnalyzer:
"""Jaeger数据分析器"""
def __init__(self, jaeger_url: str):
self.url = jaeger_url
async def find_slow_traces(
self,
service: str,
min_duration_ms: int = 2000,
limit: int = 20
) -> list:
"""查找慢trace"""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.url}/api/traces",
params={
"service": service,
"limit": limit,
"minDuration": f"{min_duration_ms}ms",
"lookback": "1h",
}
)
return response.json()["data"]
async def analyze_bottleneck(
self,
trace_id: str
) -> dict:
"""分析trace瓶颈"""
trace = await self._get_trace(trace_id)
spans = self._flatten_spans(trace)
# 找到最耗时的span
slowest = max(spans, key=lambda s: s["duration"])
# 分析Span层级
tree = self._build_span_tree(spans)
# 找到关键路径
critical_path = self._find_critical_path(tree)
return {
"trace_id": trace_id,
"total_duration_ms": tree["duration"],
"slowest_span": {
"name": slowest["operationName"],
"duration_ms": slowest["duration"],
"service": slowest["process"]["serviceName"],
},
"critical_path": [
{
"span": s["operationName"],
"duration_ms": s["duration"],
"service": s["process"]["serviceName"],
}
for s in critical_path
],
"span_count": len(spans),
}
性能影响控制
class TracingPerformanceGuard:
"""追踪性能守护——控制追踪开销"""
def __init__(self):
self.sampling_rates = {
"fast_path": 0.05, # <500ms的请求5%采样
"normal": 0.2, # 500ms-2s的请求20%采样
"slow": 1.0, # >2s的请求100%采样
"error": 1.0, # 错误请求100%采样
}
def should_trace(self, estimated_duration_ms: int = 0) -> bool:
"""决定是否追踪"""
if estimated_duration_ms > 2000:
rate = self.sampling_rates["slow"]
elif estimated_duration_ms > 500:
rate = self.sampling_rates["normal"]
else:
rate = self.sampling_rates["fast_path"]
return random.random() < rate
@contextmanager
def conditional_span(self, tracer, name: str, should_trace: bool):
"""条件性创建span"""
if should_trace:
with tracer.start_as_current_span(name) as span:
yield span
else:
yield None
总结
OpenTelemetry为Agent系统提供了统一的、标准化的链路追踪能力。精心设计的Span层级让每一次Agent对话的完整路径都清晰可见——从路由决策到工具调用,从记忆检索到LLM推理。Jaeger的可视化让性能瓶颈一目了然,基于trace的分析能够将排障效率提升数倍。
核心原则:链路追踪的投入产出比极高——它不仅帮你定位问题,更帮你理解系统。在生产环境中,即使只有1%的采样率,也能捕获足够的信息用于性能分析和故障排查。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。