如何将上下文管理器应用于 Agent 链路追踪,包括 LLM 调用耗时、异常捕获,以及与 OpenTelemetry 的结合?
Agent 调用链往往涉及工具调用、LLM 推理、检索增强等多个环节,排查慢调用和异常全靠日志是地狱模式。
上下文管理器天生为“进入-退出”模式设计,做链路追踪是一等公民。
🎯 核心思路:用 with 块包裹“追踪单元”¶
每个关键操作(LLM 调用、工具执行、RAG 检索)都作为一个 Span,用上下文管理器自动记录:
-
⏱️ 耗时
-
❌ 异常
-
📝 元数据(模型名、prompt token 数、工具名等)
跟 OpenTelemetry 集成后,这些 Span 自然串成 Trace,直接推到 Jaeger / Zipkin。
🔧 实战:定制 @traceable 上下文管理器¶
from contextlib import contextmanager
from time import perf_counter
import traceback
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@contextmanager
def traced_span(name: str, attributes: dict = None):
span = tracer.start_span(name, attributes=attributes or {})
start = perf_counter()
try:
yield span # 可在 with 体内添加事件
except Exception as e:
span.record_exception(e)
span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
raise
finally:
duration = perf_counter() - start
span.set_attribute("duration_ms", duration * 1000)
span.end()
🧠 Agent 场景应用:LLM 调用追踪¶
def call_llm(prompt: str, model="gpt-4"):
with traced_span("LLM Call", {"model": model, "prompt_len": len(prompt)}) as span:
try:
response = openai.chat.completions.create(...)
span.set_attribute("usage.total_tokens", response.usage.total_tokens)
span.set_attribute("status", "ok")
return response
except Exception as e:
span.set_attribute("status", "fail")
raise
🔗 多层嵌套自动形成父子关系(OpenTelemetry 的 context 传播):
⚡ 升级:装饰器一步到位¶
def trace(name=None, attrs=None):
def decorator(func):
def wrapper(*args, **kwargs):
span_name = name or func.__name__
with traced_span(span_name, attrs) as span:
return func(*args, **kwargs)
return wrapper
return decorator
@trace("LLM Reasoning", {"type": "agent"})
def reason(state: AgentState):
...
📊 与 OpenTelemetry 结合的关键点¶
| 关键能力 | 实现方式 |
|---|---|
| 自动注入 TraceID | 无需手动传递,OpenTelemetry 自动管理 context |
| 关联 Agent 事件 | 在 Span 内 span.add_event("token_used", {"count": n}) |
| 全链路异常关联 | record_exception 自动带上堆栈,错误率立刻可见 |
| 零侵入接入框架 | 重写 BaseTool 的 run 方法,套一层 traced_span |
🧨 面试加分点¶
-
异步支持:用
@asynccontextmanager做 async with,适配异步 Agent。 -
开销控制:生产环境建议用采样 (Sampling),而非每个请求都全量 Trace。
-
Token 用量归因:在 LLM Span 上打上
llm.usage.total_tokens,成本追踪利器。 -
兼容 Dify/LangChain:直接覆盖它们的
on_llm_start回调,注入自定义 span。
最后考你:当 Agent 内部有大量短小工具调用时,如何在不丢失可见性的前提下避免 Span 爆炸?想想 SpanKind.INTERNAL 与自定义采样逻辑的结合。 😏