在构建Agent应用时,一个用户请求通常会触发多个模型调用、工具调用和内存操作。这些步骤可能串行执行,也可能并行发生,形成一条隐含的执行链。若缺少链路追踪,故障定位只能不断加日志重新复现,效率很低。Jaeger与OpenTelemetry的组合可以为Agent系统提供完整的可观测性支撑:OpenTelemetry负责统一埋点与数据采集,Jaeger负责存储、检索与可视化。两者通过OTLP协议衔接,能够清晰还原每一次Agent任务的完整执行路径。

一、Agent链路追踪与传统微服务追踪的差异
传统微服务追踪通常基于HTTP或RPC调用,span之间的父子关系相对固定,一次请求在下游服务中基本是树形结构。Agent应用则不同,一次任务可能包含模型推理、工具调用、记忆读写、规划循环等多个步骤,而且这些步骤之间存在条件分支、重试和并行执行。例如,一个ReAct风格的Agent会反复执行“思考—行动—观察”循环,直到达到终止条件。这种动态性导致追踪结构不再是稳定的树,而可能是有环图或者多层嵌套的动态span。
另一个关键差异是业务属性的复杂度。普通微服务通常记录请求方法、状态码和路径即可满足排查需求,但Agent链路需要额外记录模型名称、提示词摘要、token消耗、工具名称、工具入参摘要以及返回结果长度等信息。这些属性如果设计不当,会导致span体积膨胀,增加存储和查询压力。因此,在Agent场景中做链路追踪,不能简单照搬微服务的埋点方式,而需要针对智能体的执行特点设计span结构。
此外,Agent内部的异步任务也会破坏传统父子span的时间连续性。一个工具调用可能在线程池中异步执行,父span早已结束,子span才被创建。OpenTelemetry提供了上下文传播机制,可以通过Context对象跨线程传递追踪信息,但开发者必须显式处理,否则span会散落成孤立的节点。理解了这些差异,才能更准确地规划埋点方案。
二、使用OpenTelemetry进行Agent埋点
OpenTelemetry的SDK提供了TracerProvider、Tracer和Span三层接口。初始化时先创建TracerProvider,并绑定OTLP exporter,将其设置为全局默认值。之后在Agent执行链的每个节点调用getTracer获取实例,通过startSpan开启一个span。span结束时必须调用end方法,否则数据不会上报。下面以Python为例展示一个基础埋点过程。
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# 创建资源,标识当前Agent服务
resource = Resource.create({"service.name": "agent-service"})
# 初始化TracerProvider,并配置OTLP exporter
provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)
# 获取tracer
tracer = trace.get_tracer("agent.tracer")
def run_agent(task: str):
# 创建根span,代表一次完整的Agent调用
with tracer.start_as_current_span("agent.run") as agent_span:
agent_span.set_attribute("agent.task", task[:100])
# 模拟规划步骤
with tracer.start_as_current_span("agent.plan") as plan_span:
plan_span.set_attribute("agent.plan.strategy", "react")
# 规划逻辑...
# 模拟工具调用步骤
with tracer.start_as_current_span("agent.tool_call") as tool_span:
tool_span.set_attribute("tool.name", "search")
tool_span.set_attribute("tool.query", task[:50])
# 工具执行逻辑...
# 模拟模型推理步骤
with tracer.start_as_current_span("agent.llm") as llm_span:
llm_span.set_attribute("llm.model", "gpt-4.1")
llm_span.set_attribute("llm.tokens", 1280)
# 模型调用逻辑...
上面的代码中,start_as_current_span会自动把新span设置为当前上下文,使嵌套的逻辑自动形成父子关系。当工具调用或模型推理发生在异步任务中时,需要使用context.attach和context.detach手动传递上下文。否则子span可能无法正确挂载到父span上。常见的做法是在创建异步任务前捕获当前Context,在任务执行时重新附着。
除了手动埋点,OpenTelemetry还提供了丰富的自动埋点方案。例如,对于基于HTTP的Agent服务,可以通过opentelemetry-instrument命令自动捕获请求和响应。对于常见的LLM客户端库,部分社区已经提供了对应的instrumentation包,能自动记录模型调用参数和返回信息。不过自动埋点无法覆盖Agent内部的规划、记忆读写等细粒度步骤,所以手动埋点仍然是保证追踪完整性的关键手段。
三、Jaeger部署与数据查看
Jaeger包含多个组件:Collector负责接收OTLP数据,Query提供Web UI和API查询,Storage负责持久化。小型环境中可以使用all-in-one镜像快速启动,它将所有组件集成在一个进程里。生产环境建议将Collector与Query分离,并使用Elasticsearch或Cassandra作为后端存储。以下是一个简单的docker-compose配置,用于本地开发验证。
version: "3.8"
services:
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
environment:
- COLLECTOR_OTLP_ENABLED=true
- LOG_LEVEL=info
启动后,Jaeger默认监听4317端口接收OTLP gRPC协议数据。前面Python示例中的OTLPSpanExporter正是将span批量发送到这个端口。访问http://localhost:16686即可打开Jaeger UI,在Service下拉框中选择agent-service,就可以看到近期上报的trace列表。点击某一条trace,能够展开完整的span树,每个span的属性、耗时和事件都会显示在详情面板中。
在实际排查中,可以利用Jaeger的标签过滤功能快速缩小范围。例如搜索tool.name=search可以查看所有调用搜索工具的span,结合耗时排序可以定位最慢的调用。Jaeger还支持基于trace ID和span ID的精确检索,方便与日志系统联动。将日志中的trace ID打印出来,就能从日志跳转到对应的链路视图,形成完整的可观测性闭环。
如果Agent服务运行在Kubernetes环境中,建议通过OpenTelemetry Collector作为中间层统一接收和转发数据。这样可以增加数据脱敏、批处理、多后端导出等能力,避免每个服务直接与Jaeger通信。Collector配置中定义OTLP receiver和Jaeger exporter,Agent服务只需要将数据发送到Collector即可。
四、Agent链路追踪的实践优化建议
Agent链路追踪不仅要保证数据完整,还要控制存储成本和查询性能。首先,span属性不宜过大。提示词和工具返回内容可能非常长,如果原样写入span属性,会迅速撑大存储。实践中通常只记录摘要或前N个字符,完整内容继续留在日志系统中。其次,合理设置采样策略。在开发环境可以使用全量采样,而在高并发生产环境中建议采用概率采样或基于规则的采样。OpenTelemetry支持ParentBased和TraceIDRatioBased采样器,可以在创建TracerProvider时设置。
另一个常见问题是如何处理Agent循环产生的海量span。一个长时间运行的Agent可能执行几十轮循环,每轮包含模型调用和多个工具调用,span数量会迅速累积到数百甚至上千个。此时可以考虑将每一轮循环作为独立的子span,并在span内部使用事件记录细节,而不是为每个微步骤创建独立span。这样既能保持调用结构清晰,又能避免span数量爆炸。
此外,把业务上下文与追踪信息关联也非常重要。Agent应用通常需要区分不同租户、会话和任务。建议在根span上设置user.id、session.id和agent.task_id等属性,并在后续子span中继承。这样在Jaeger中就可以按租户或会话快速过滤,提升问题定位效率。对于多Agent协作场景,还可以使用span_link把不同Agent之间的逻辑依赖关联起来,形成跨服务的完整视图。
最后要强调的是性能开销。虽然OTLP exporter采用批处理和异步发送,但过量的span创建和属性设置仍然会带来可观的CPU与内存消耗。建议对热点路径进行基准测试,必要时对低价值步骤关闭追踪,或者通过环境变量动态控制埋点开关。Agent链路追踪的目标是让系统更易于理解和维护,而不是反过来拖垮系统性能。
Agent链路追踪JaegerOpenTelemetry修改时间:2026-08-21 17:19:19