OpenTelemetry GenAI:端到端追踪工具调用


文档摘要

OpenTelemetry GenAI:端到端追踪工具调用 本节摘要:一个 Agent 调了五个工具、三个 MCP 服务端、两个子 Agent,你需要横跨这一切的一条 trace。OpenTelemetry GenAI 语义约定(v1.37 起稳定属性)是 2026 年的标准,被 Datadog、Langfuse、Arize Phoenix、OpenLLMetry、AgentOps 原生支持。本节命名必需属性、走通 span 层次(agent → LLM → tool)、并造一个可接任何 OTel 导出器的 stdlib span 发射器。

OpenTelemetry GenAI:端到端追踪工具调用

本节摘要:一个 Agent 调了五个工具、三个 MCP 服务端、两个子 Agent,你需要横跨这一切的一条 trace。OpenTelemetry GenAI 语义约定(v1.37 起稳定属性)是 2026 年的标准,被 Datadog、Langfuse、Arize Phoenix、OpenLLMetry、AgentOps 原生支持。本节命名必需属性、走通 span 层次(agent → LLM → tool)、并造一个可接任何 OTel 导出器的 stdlib span 发射器。读完本节,你能说出 LLM span 与工具 span 的必需属性、构建覆盖 Agent 循环/LLM 调用/工具调用/MCP 派发的 trace 层次、并决定哪些内容可选采集、哪些默认脱敏。

学习目标

阅读完本节,你应当能够:

  1. 说出 LLM span 与工具执行 span 的必需 OTel GenAI 属性
  2. 构建覆盖 Agent 循环、LLM 调用、工具调用、MCP 客户端派发的 trace 层次
  3. 决定哪些内容可选采集(opt-in)、哪些默认脱敏
  4. 把 span 发射到本地采集器(Jaeger、Langfuse),而无需改写工具代码

一、问题与直觉

一个 2026 年 2 月的真实 debug:用户报告「我的 Agent 有时要 30 秒才响应,有时 3 秒」。没 trace。日志能看到 LLM 调用,但看不到工具派发、看不到 MCP 服务端往返、看不到子 Agent。你只能猜。最后发现:一个 MCP 服务端在冷启动时偶尔卡住。

没有端到端追踪,你找不到这种问题。OTel GenAI 修了它。

这套约定在 2025-2026 年由 OpenTelemetry semantic-conventions 工作组定稿。它定义稳定的属性名,让 Datadog、Langfuse、Phoenix、OpenLLMetry、AgentOps 都能解析同样的 span。埋点一次,发到任意后端

二、从零实现

Span 层次

agent.invoke_agent (顶,INTERNAL span) ├── llm.chat (CLIENT span) ├── tool.execute (INTERNAL) │ └── mcp.call (CLIENT span) ├── llm.chat (CLIENT span) └── subagent.invoke (INTERNAL)

整体嵌在同一个 trace id 下,span id 编码父子关系。

必需属性

按 2025-2026 semconv:

LLM span:

  • gen_ai.operation.name——"chat""text_completion""embeddings""execute_tool""invoke_agent"
  • gen_ai.provider.name——"openai""anthropic""google""azure_openai"
  • gen_ai.request.model——请求的模型串(如 "gpt-4o-2024-08-06")。
  • gen_ai.response.model——实际服务的模型。
  • gen_ai.usage.input_tokens / gen_ai.usage.output_tokens
  • gen_ai.response.id——用于关联的厂商响应 id。

工具 span:

  • gen_ai.tool.name——工具标识。
  • gen_ai.tool.call.id——具体那次调用 id。
  • gen_ai.tool.description——工具描述(可选)。

Agent span:

  • gen_ai.agent.name / gen_ai.agent.id / gen_ai.agent.description

Span 类型

  • SpanKind.CLIENT 给跨进程边界的调用(LLM 厂商、MCP 服务端)。
  • SpanKind.INTERNAL 给 Agent 自己的循环步骤与工具执行。

可选内容采集

默认 span 只带指标与时序——不带提示或补全。大负载与 PII 默认关。设 OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental 与具体的内容采集环境变量才纳入内容。生产启用前要谨慎评审。

span 上的事件

token 级事件可作为 span event 加:

  • gen_ai.content.prompt——输入消息。
  • gen_ai.content.completion——输出消息。
  • gen_ai.content.tool_call——记录到的工具调用。

事件按时间排在 span 内,便于详细回放。

导出器

OTel span 可导出到:

  • Jaeger / Tempo:开源,本地部署。
  • Langfuse:LLM 可观测专用品,可视化 token 用量。
  • Arize Phoenix:评测 + 追踪合一。
  • Datadog:商业,原生解析 gen_ai.* 属性。
  • Honeycomb:列式存储,查询友好。

都讲 OTLP——线缆格式。你的代码不关心后端是谁。

跨 MCP 传播

MCP 客户端调服务端时,把 W3C traceparent 头注入请求。Streamable HTTP 支持标准头;Stdio 原生不带 HTTP 头,规范 2026 路线图讨论在 JSON-RPC 调用上加一个 _meta.traceparent 字段。

在那之前:手动把 traceparent 放进每个请求的 _meta,服务端记录 trace id。

def mcp_call(server, tool, args, ctx): span = tracer.start_span("mcp.call", kind=SpanKind.CLIENT, attributes={"gen_ai.tool.name": tool}) args["_meta"] = {"traceparent": format_traceparent(ctx, span)} # 手动传播 return server.call(tool, args)

指标

除 span 外,GenAI semconv 还定义指标:

  • gen_ai.client.token.usage——直方图。
  • gen_ai.client.operation.duration——直方图。
  • gen_ai.tool.execution.duration——直方图。

用于不需要逐调用细节的仪表盘。

AgentOps 层

AgentOps(2024 年成立)专攻 GenAI 可观测。它包裹流行框架(LangGraph、Pydantic AI、CrewAI)自动发 OTel span。栈用支持框架时有用,否则手工埋点。

最小 span 发射器骨架

import uuid, time, json def new_trace(): return uuid.uuid4().hex[:16] def new_span(): return uuid.uuid4().hex[:16] def emit_span(trace_id, span_id, parent_id, name, kind, attrs, start, end): print(json.dumps({"traceId": trace_id, "spanId": span_id, "parentSpanId": parent_id, "name": name, "kind": kind, "attributes": attrs, "startTime": start, "endTime": end})) class Span: def __init__(self, trace_id, name, kind, parent_id=None, attrs=None): self.tid, self.sid, self.pid = trace_id, new_span(), parent_id self.name, self.kind, self.attrs = name, kind, attrs or {} def __enter__(self): self.start = time.time(); return self def __exit__(self, *e): emit_span(self.tid, self.sid, self.pid, self.name, self.kind, self.attrs, self.start, time.time()) def child(self, name, kind, attrs=None): return Span(self.tid, name, kind, self.sid, attrs)

设计要点:trace id 跨所有 span 共享,父子关系靠 parentSpanId 串起来。把埋点做成可嵌套的上下文管理器(Span),让你像写普通代码那样自然产出层次——而不用手工维护一张父子表。

三、框架对比

后端 类型 GenAI 原生支持 强项
Jaeger / Tempo 开源 解析 gen_ai.* 本地部署、无锁定
Langfuse 开源/托管 token 可视化、评测
Arize Phoenix 开源/商业 评测 + 追踪合一
Datadog 商业 一站式、原生解析
Honeycomb 商业 列式、查询快

💡 心法:都讲 OTLP,埋点与后端解耦。埋点一次,切换后端零代码改动——这正是 OTel 相比厂商专有 SDK 的核心价值。内容采集默认关,生产开之前先过 PII 评审。

四、可复用产物

本节产出 outputs/skill-otel-genai-instrumentation.md——给定一份 Agent 代码库,这个 skill 产出埋点计划:在哪加 span、填哪些属性、对接哪些导出器。

code/main.py 为一个「调 LLM、派两个工具、做一次 MCP 往返」的 Agent 发 OTel 形状的 span 到 stdout(OTLP-JSON 风格)。无真实导出器——本节聚焦 span 形状与属性集。把输出贴进任意 OTLP 兼容查看器,或直接读。可重点看:trace id 跨所有 span 共享、父子关系靠 parentSpanId 编码、必需 gen_ai.* 属性已填、内容采集默认关(一种场景用环境变量打开)。

五、练习

  1. 数 span:运行 code/main.py,数 span 个数,标出哪个是 CLIENT、哪个是 INTERNAL。

  2. 开内容采集:用环境变量打开,确认 gen_ai.content.promptgen_ai.content.completion 事件出现,留意 PII 影响。

  3. 加工具指标:加 gen_ai.tool.execution.duration 指标,每次调用发一个直方图样本。

  4. 跨 MCP 传播:把 traceparent 从父 Agent span 注入 MCP 请求的 _meta.traceparent,验证 MCP 服务端能看到同一 trace id。

  5. 补属性:读 OTel GenAI semconv 规范,找出一个本节代码没发的属性,补上。

本节要点回顾

  1. 端到端 trace 是必需:跨 LLM/工具/MCP/子 Agent 的单一 trace,定位「哪一跳慢」靠它。
  2. GenAI semconv 是 2026 标准:v1.37 起稳定属性,Datadog/Langfuse/Phoenix 等都解析。
  3. span 层次:agent.invoke_agent → llm.chat → tool.execute → mcp.call,父子靠 trace id 与 span id。
  4. 必需 gen_ai.* 属性:operation/provider/request.model/response.model/usage/response.id,工具与 Agent 各有子集。
  5. SpanKind:CLIENT 给跨进程调用,INTERNAL 给 Agent 自身循环。
  6. 内容采集默认关:大负载与 PII 默认不带,用环境变量 opt-in,生产开前过 PII 评审。
  7. 跨 MCP 传播 traceparent:HTTP 走标准头,Stdio 暂时手动塞进 _meta
  8. 埋点一次,后端任意:都讲 OTLP,代码不关心后端是谁。

下一节,我们退一步看 LLM 这一层的路由网关——LiteLLM、OpenRouter、Portkey 如何用单一 API 表面、回退链、成本追踪与护栏,把多家厂商统一管理。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U