OpenTelemetry GenAI:端到端追踪工具调用 本节摘要:一个 Agent 调了五个工具、三个 MCP 服务端、两个子 Agent,你需要横跨这一切的一条 trace。OpenTelemetry GenAI 语义约定(v1.37 起稳定属性)是 2026 年的标准,被 Datadog、Langfuse、Arize Phoenix、OpenLLMetry、AgentOps 原生支持。本节命名必需属性、走通 span 层次(agent → LLM → tool)、并造一个可接任何 OTel 导出器的 stdlib span 发射器。
本节摘要:一个 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 层次、并决定哪些内容可选采集、哪些默认脱敏。
阅读完本节,你应当能够:
一个 2026 年 2 月的真实 debug:用户报告「我的 Agent 有时要 30 秒才响应,有时 3 秒」。没 trace。日志能看到 LLM 调用,但看不到工具派发、看不到 MCP 服务端往返、看不到子 Agent。你只能猜。最后发现:一个 MCP 服务端在冷启动时偶尔卡住。
没有端到端追踪,你找不到这种问题。OTel GenAI 修了它。
这套约定在 2025-2026 年由 OpenTelemetry semantic-conventions 工作组定稿。它定义稳定的属性名,让 Datadog、Langfuse、Phoenix、OpenLLMetry、AgentOps 都能解析同样的 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。SpanKind.CLIENT 给跨进程边界的调用(LLM 厂商、MCP 服务端)。SpanKind.INTERNAL 给 Agent 自己的循环步骤与工具执行。默认 span 只带指标与时序——不带提示或补全。大负载与 PII 默认关。设 OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental 与具体的内容采集环境变量才纳入内容。生产启用前要谨慎评审。
token 级事件可作为 span event 加:
gen_ai.content.prompt——输入消息。gen_ai.content.completion——输出消息。gen_ai.content.tool_call——记录到的工具调用。事件按时间排在 span 内,便于详细回放。
OTel span 可导出到:
gen_ai.* 属性。都讲 OTLP——线缆格式。你的代码不关心后端是谁。
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(2024 年成立)专攻 GenAI 可观测。它包裹流行框架(LangGraph、Pydantic AI、CrewAI)自动发 OTel 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.* 属性已填、内容采集默认关(一种场景用环境变量打开)。
数 span:运行 code/main.py,数 span 个数,标出哪个是 CLIENT、哪个是 INTERNAL。
开内容采集:用环境变量打开,确认 gen_ai.content.prompt 与 gen_ai.content.completion 事件出现,留意 PII 影响。
加工具指标:加 gen_ai.tool.execution.duration 指标,每次调用发一个直方图样本。
跨 MCP 传播:把 traceparent 从父 Agent span 注入 MCP 请求的 _meta.traceparent,验证 MCP 服务端能看到同一 trace id。
补属性:读 OTel GenAI semconv 规范,找出一个本节代码没发的属性,补上。
gen_ai.* 属性:operation/provider/request.model/response.model/usage/response.id,工具与 Agent 各有子集。_meta。下一节,我们退一步看 LLM 这一层的路由网关——LiteLLM、OpenRouter、Portkey 如何用单一 API 表面、回退链、成本追踪与护栏,把多家厂商统一管理。