微服务时代,Jaeger 是工程师理解调用链的利器;如今 AI Agent 大量涌入生产环境,调用链从"服务 A → 服务 B"变成了"Agent 规划 → 调 LLM → 调工具 → 再规划"。链路更长、分支更多、失败模式更隐蔽。Jaeger 正在围绕 OpenTelemetry 做一轮针对性演进,让 Agent 的每一步决策都可观测。
Agent 追踪和微服务追踪不是同一件事
微服务调用链的典型特征:路径相对固定、每个节点是确定性的服务、延迟主要来自网络和 IO。Agent 链路完全不同——
- 动态拓扑:Agent 每次运行可能选择不同的工具组合,链路形状随 prompt 和中间结果变化。
- 嵌套递归:Agent 可以在循环中反复调用 LLM 和外部工具,span 层级可能很深。
- 语义信息丢失:传统 trace 只记录耗时和状态码,但 Agent 追踪还需要记录 prompt 摘要、工具选择理由、token 消耗等业务语义。
如果只拿微服务那套 span 命名方式套上去,你会看到一串叫 agent_step 的扁平 span,既看不出 Agent 做了什么决策,也定位不了哪一步 token 爆了。Jaeger 的演进方向,正是要解决这些信息缺口。
OpenTelemetry 成为连接纽带
Jaeger 早期有自己的数据格式和客户端 SDK,现在全面转向 OpenTelemetry(OTel)作为数据摄入标准。这对 Agent 追踪是个关键利好——
- 统一语义约定:OTel 的 Semantic Conventions 机制允许社区为 AI/LLM 调用定义标准 attribute key(比如
gen_ai.request.model、gen_ai.response.finish_reason)。Jaeger 作为后端只需渲染这些 attribute,前端不用各自发明字段名。 - 跨语言 SDK:无论你的 Agent 用 Python(LangChain / CrewAI)还是 TypeScript(Vercel AI SDK)写,OTel 都有对应 SDK,trace 数据格式一致,Jaeger 统一收纳。
- Baggage 传递上下文:Agent 的任务 ID、session ID、用户意图标签可以通过 OTel Baggage 在跨进程调用中自动传播,不用在每个工具调用里手动塞 header。
实战:给一个 Python Agent 加上 Jaeger 追踪
下面用一个最小示例演示:启动 Jaeger + OTel Collector,给一个模拟 Agent 循环加上 trace,最终在 Jaeger UI 中看到带语义 attribute 的完整链路。
1. 用 Docker 启动 Jaeger 全部组件
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:1.60
4317/4318:OTLP gRPC / HTTP 端口,Python SDK 会往这里发数据。16686:Jaeger UI,浏览器打开http://localhost:16686查看链路。
版本号 1.60 是当前最新稳定版,Jaeger 1.57 起全面支持 OTLP 摄入,all-in-one 镜像内置了 Collector + Query + UI。
2. Python Agent 代码(可直接运行)
先安装依赖:
pip install opentelemetry-api opentelemetry-sdk \
opentelemetry-exporter-otlp-proto-grpc \
opentelemetry-instrumentation-requests
然后运行以下 Agent 脚本:
import time, random
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
# ── 初始化 OTel SDK,指向本地 Jaeger ──
resource = Resource.create({
"service.name": "ai-agent-demo",
"service.version": "0.1.0",
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
)
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("agent-loop")
# ── 模拟 Agent 循环 ──
def call_llm(prompt: str, parent_ctx) -> str:
"""模拟一次 LLM 调用,带 gen_ai 语义 attribute"""
with tracer.start_as_current_span("call_llm", parent_ctx) as span:
span.set_attribute("gen_ai.request.model", "gpt-4o-mini")
span.set_attribute("gen_ai.request.prompt_summary", prompt[:60])
span.set_attribute("gen_ai.system", "openai")
latency = random.uniform(0.3, 1.2)
time.sleep(latency)
tokens = random.randint(80, 300)
span.set_attribute("gen_ai.response.finish_reason", "stop")
span.set_attribute("gen_ai.response.token_count", tokens)
span.set_attribute("gen_ai.response.latency_ms", int(latency * 1000))
return f"LLM结果: {prompt[:20]}... (tokens={tokens})"
def call_tool(tool_name: str, parent_ctx) -> str:
"""模拟一次外部工具调用"""
with tracer.start_as_current_span(f"call_tool.{tool_name}", parent_ctx) as span:
span.set_attribute("tool.name", tool_name)
span.set_attribute("tool.type", "api")
time.sleep(random.uniform(0.1, 0.5))
if tool_name == "search_web" and random.random() < 0.15:
span.set_attribute("tool.error", "timeout")
span.set_status(trace.StatusCode.ERROR, "tool timeout")
return "ERROR: timeout"
return f"{tool_name} 返回数据"
def agent_run(task: str, max_steps: int = 4):
"""Agent 主循环:规划 → 调 LLM → 调工具 → 再规划"""
with tracer.start_as_current_span("agent_run") as root:
root.set_attribute("agent.task", task)
root.set_attribute("agent.max_steps", max_steps)
ctx = trace.get_current_span().get_span_context()
parent_ctx = trace.SpanContext(
trace_id=ctx.trace_id, span_id=ctx.span_id,
trace_flags=ctx.trace_flags, is_remote=False
)
for step in range(1, max_steps + 1):
with tracer.start_as_current_span(f"agent_step.{step}", parent_ctx) as step_span:
step_span.set_attribute("agent.step_number", step)
# 先调 LLM 做规划
llm_result = call_llm(f"步骤{step}: 处理任务'{task}'", parent_ctx)
# 根据步骤选不同工具
tool = random.choice(["search_web", "read_db", "send_email"])
tool_result = call_tool(tool, parent_ctx)
step_span.set_attribute("agent.chosen_tool", tool)
step_span.set_attribute("agent.llm_output_summary", llm_result[:40])
if "ERROR" in tool_result:
step_span.set_attribute("agent.step_outcome", "tool_failed")
break
step_span.set_attribute("agent.step_outcome", "success")
if __name__ == "__main__":
agent_run("分析用户投诉并自动回复")
time.sleep(2) # 等待 BatchSpanProcessor 刷出数据
print("Trace 已发送,打开 http://localhost:16686 查看")
3. 在 Jaeger UI 中你会看到什么
运行后打开 Jaeger UI,选择 ai-agent-demo 服务,你会看到:
- 一个
agent_run根 span,带agent.taskattribute。 - 嵌套的
agent_step.1→agent_step.2… 子 span,每个记录了选了哪个工具。 - 再下一层是
call_llm和call_tool.search_web,LLM span 里能直接看到模型名、token 数、延迟毫秒数。 - 如果工具超时,对应 span 标红,状态为 ERROR。
这比一串扁平的 agent_step 信息密度高得多——你一眼就能定位"第 3 步调 search_web 超时了,那一步 LLM 花了 900ms、返回了 200 token"。
追踪 Agent 的几个实操要点
给 span 取有结构的名字。不要全部叫 agent_step,用 agent_step.{n} 或 agent.plan / agent.execute / agent.reflect 这样的层级命名,Jaeger 的瀑布图才能展示决策分支。
把业务语义写进 attribute 而不是 span name。模型名、工具名、token 数、finish_reason 这些用 OTel Semantic Conventions 定义的标准 key,Jaeger UI 会自动在 span 详情里展示,搜索过滤也靠这些字段。
控制 span 数量。Agent 循环 20 步就会产生 60+ span(每步一个 step span + 一个 LLM span + 一个 tool span)。如果你的 Agent 是长跑型,考虑用 BatchSpanProcessor 并设置合理的 max_queue_size 和 schedule_delay,避免内存撑爆或 Collector 压力过大。
Baggage 传 session ID。如果 Agent 调了外部服务(比如通过 HTTP 调搜索 API),用 OTel Baggage 把 session.id 自动注入请求 header,下游服务也能挂到同一条 trace 上:
from opentelemetry.baggage import set_baggage, get_baggage
from opentelemetry.context import attach, detach
ctx = set_baggage("session.id", "sess-abc123")
token = attach(ctx)
# 后续 HTTP 调用自动携带 baggage
# ...
detach(token)
还不完美,但方向明确
Jaeger 对 Agent 追踪的适配仍在进行中。目前几个现实限制——
- LLM Semantic Conventions 还在草案阶段:OTel GenAI Working Group 的约定尚未最终发布,字段名可能微调,代码里用的 key 要跟进变更。
- Jaeger UI 对长链路的渲染:超过 50 层嵌套的瀑布图会变得拥挤,需要配合筛选和折叠功能。
- Agent 内部状态追踪:比如 Agent 的记忆、规划树,目前只能靠手动 attribute,还没有标准化的可视化方案。
尽管如此,用 OTel SDK + Jaeger 给 Agent 加追踪已经是当前最务实的路径。标准在成型,工具在跟进,越早把 attribute 写规范,未来迁移成本越低。
上手清单:
- 用 all-in-one 镜像本地跑起 Jaeger,确认 OTLP 端口可用。
- 在 Agent 入口初始化 OTel TracerProvider,指向 Jaeger Collector。
- 给 Agent 循环、LLM 调用、工具调用各加一层 span,写进
gen_ai.*和tool.*attribute。 - 跑一次完整任务,在 Jaeger UI 验证链路形状和语义字段是否可读。
- 上生产前换成 Jaeger Operator + OTel Collector 的正式部署,加上采样策略控制数据量。