跨 Agent 服务边界的分布式追踪:上下文传播的断裂
大多数分布式追踪方案在引入 Agent 之前都运作良好。一旦系统中出现 Agent A 跨微服务边界调用 Agent B——Agent B 调用工具服务器、工具服务器再查询向量数据库——原本连贯的端到端视图就会碎片化为互不相连的片段。追踪后端展示的是一个个孤立的操作,而你失去的是因果链:为什么某件事发生了,哪个用户请求触发了它,以及那 800 毫秒究竟消耗在了哪里。
这不是监控配置问题,而是上下文传播架构问题。它有着特定的技术形态,大多数团队都是在付出代价后才意识到这一点。
为何 W3C TraceContext 在 Agent 边界处失效
W3C Trace Context 标准解决的是一个范围很窄的问题:跨单次 HTTP 请求边界传播追踪身份。每个请求携带格式为 version-trace-id-parent-id-trace-flags 的 traceparent 头。下游服务读取该头,在父 Span ID 下创建子 Span,然后返回。简单、可靠、支持广泛。
这一模型内置的假设是同步的、请求范围内的通信:一个服务调用另一个服务,得到响应,追踪结束。而 Agent 以三种不同方式打破了这一假设。
第一,Agent 采用异步通信。 当编排 Agent 通过消息队列向工作 Agent 排队任务时,没有 HTTP 请求可以携带 traceparent 头。工作 Agent 开始处理消息时创建自己的根 Span,因果链就此断裂。追踪后端会将原本属于同一逻辑操作的内容显示为两条独立的追踪。
第二,Agent 跨信任边界调用 Agent。 模型上下文协议(MCP)服务器是最典型的例子。当 Agent 调用 MCP 服务器执行工具时,MCP 协议不会自动接收并传播调用方 Agent 的 traceparent 头。每次 MCP 服务器操作都以孤立的根 Span 出现,除非你在调用点手动注入该头。
第三,Agent 框架具有不透明的内部循环。 AutoGen 等框架在内部运行工具循环,在没有暴露仪表钩子的情况下进行 LLM 调用和工具调用。从外部看,整个 Agent 执行只有一个顶层 Span。内部发生了什么——哪次 LLM 调用花了 2 秒、哪个工具返回了格式错误的响应、哪次重试最终成功——都是不可见的。
实际结果是:对多 Agent 系统的一次查询本应产生一条连贯的追踪,却在 Jaeger 或 Zipkin 中产生了三到十个孤儿根 Span,在仪表板上无法将它们重新关联在一起。
孤儿 Span 的实际表现
这种故障有明确的诊断特征。在追踪后端中,查找以下情况:
- 序列中途出现没有父节点的根 Span。 如果你看到一个
parent_span_id: null的 Span 在用户请求开始 300ms 后才出现,说明某个边界处的上下文传播失败了。 - Trace ID 不连续。 用户请求进入网关时的 Trace ID 是
4bf92f3577b34da6a3ce929d0e0e4736,到达 Agent B 时 Trace ID 已变。这意味着 Agent B 创建了新的追踪根,而不是延续原有追踪。 - 工具调用 Span 作为根节点出现。 工具调用 Span 应该是 LLM Span 的子节点,而 LLM Span 是 Agent Span 的子节点。当工具调用 Span 以根节点出现时,框架的内部上下文没有传播到工具层。
- 没有对应 Span 解释的时间空白。 编排器 Span 在 T+500ms 结束,工作器 Span 在 T+550ms 开始,中间什么都没有。那 50ms 是消息队列的传输时间,现在对你来说是不可见的。
每种故障模式都需要不同的修复方式。知道你面对的是哪种,是重新连接追踪的第一步。
核心修复:显式上下文提取与注入
重新连接孤儿 Span 的标准模式是:在跨越任何异步或服务边界之前提取当前追踪上下文,将其序列化到消息或请求载荷中,并在另一端重新附加。
在 Python 和 OpenTelemetry 中,结构大致如下:在排队任务之前捕获活跃 Span 上下文,使用传播器将其序列化为载体字典,将该字典与消息载荷一起存储,在消 费者端于创建任何新 Span 之前从载体中提取上下文。
该模式适用于消息队列、任务队列以及任何其他 HTTP 头不自动可用的异步交接。关键认识在于:traceparent 只是一个字符串,它可以通过任何媒介传输——消息体、数据库行、Redis 键——只要你在发送方写入它,在接收方读取它。
对于 MCP 服务器,修复方式是将 traceparent 和 tracestate 注入 MCP 传输支持的任何头或元数据机制中。Red Hat 的实现模式使用装饰器包装每次 MCP 服务器调用,在调用触发前注入当前 Span 上下文。
Baggage:无需修改每个函数即可传播业务上下文
W3C Baggage 是 TraceContext 中被低估的兄弟机制。TraceContext 传播追踪身份,Baggage 传播任意键值对,这些键值对会自动跟随追踪内的所有 Span,而无需通过函数参数传递。
实际使用场景:你希望每个 Span——LLM 调用、工具调用、向量数据库查询——都携带发起原始请求的用户 ID 和会话 ID。没有 Baggage,你需要将这些值穿透每一个函数调用。有了 Baggage,你只需在请求边界设置一次,它们就会自动出现在所有后代 Span 中。
这在多 Agent 系统中尤为重要:编排器知道用户是谁,但专项 Agent 不知道——也不应该知道。在入口点将会话 ID 放入 Baggage,意味着可观测性后端可以筛选给定用户会话的所有 Span,而任何 Agent 都不需要明确感知这一关联需求。
注意:Baggage 值与 traceparent 一起在 HTTP 头中传输,这意味着每个下游服务都可以看到它们。不要在 Baggage 中放置敏感数据,只用于关联标识符,而非内容。
Python 中的异步上下文丢失:具体故障形态
Python 的 asyncio 使用上下文变量(contextvars.Context)存储 OpenTelemetry 的活跃 Span。当你使用 asyncio.create_task() 创建 asyncio 任务时,在 Python 3.7+ 中任务会自动继承当前上下文。这听起来应该有效——在简单场景下确实如此。
以下情况会失败:
- 从没有活跃 Span 上下文的后台线程创建任务
- 使用线程池执行器且工作线程未继承事件循环的上下文
- 在从不同上下文注册的回调内部创建任务
- 使用独立于主应用上下文创建的 asyncio 事件循环
诊断方式:在这些边缘情况下创建的任务会产生没有父节点的 Span,即使它们在逻辑上是创建它们的操作的子节点,也会在追踪后端显示为根 Span。
修复模式是在任务创建点之前显式捕获上下文,然后在任务内部使用 token = context.attach(captured_context),并在 finally 块中调用 context.detach(token) 显式附加上下文。这很繁琐,但可靠。
对于使用抽象了 asyncio 的框架(FastAPI、aiohttp、带异步工作器的 Celery)的团队,请检查框架的 OTel 集成是否正确处理了这个问题。大多数现代版本都能处理,但值得通过创建一个具有明确异步边界的 测试追踪并检查后端中子 Span 是否正确嵌套来加以验证。
OpenTelemetry GenAI 规范的空白
OpenTelemetry 的 GenAI 语义规范为 LLM Span 定义了标准属性名称:gen_ai.request.model、gen_ai.usage.input_tokens、gen_ai.usage.output_tokens 等。Datadog 在 2025 年采用了这些规范,表明该规范正获得业界认可。
空白在于多 Agent 层次结构。该规范定义了单次 LLM 调用和工具调用的 Span,但尚未标准化编排器与工作器关系的属性。没有标准的 gen_ai.agent.role(编排器 vs 专项),没有标准方式表示 Agent 委派深度,也没有跨完整 Agent DAG 的标准成本归因。
这意味着每个团队都在为编排层构建自定义 Span。LLM 调用 Span 在工具间可互操作,Agent 协调 Span 是私有的——而这恰恰是在调试多 Agent 流水线生产故障时最需要的那一层。
OTel GenAI SIG(于 2024 年 4 月启动)正在积极研究多 Agent Span 语义。该规范在 2026 年进入时仍处于实验阶段。如果你现在正在构建可观测性基础设施,建议为 LLM 调用实现现有的 GenAI 规范,同时以不会与最终标准冲突的方式为 Agent 协调 Span 添加自己的结构化属性。将自定义属性命名在 app.agent.* 而非 gen_ai.* 命名空间下,便于后续分离。
仪表方案对比
工具生态已分化为三种 架构方式,各自在 Agent 服务边界上有不同的权衡取舍。
基于 SDK 的仪表(Langfuse、LangSmith、Datadog with GenAI 支持)需要在每个 Agent 服务中添加 SDK 并显式调用仪表方法。这能获得最丰富的数据——可以捕获中间推理步骤、置信度分数、自定义业务指标——但每个新 Agent 服务都需要仪表化,跨边界的上下文传播仍然是你的责任。
基于代理的仪表(Helicone、Portkey)在 LLM API 边界进行拦截。你将 LLM 客户端的基础 URL 指向代理,代理自动记录输入和输出。零代码修改,增加 50-80ms 延迟,即可获得 LLM 调用可见性。但你无法看到:Agent 内部逻辑、工具调用 Span、Agent 间通信,以及 LLM 调用之间发生的任何事情。对于有趣的故障模式存在于编排层的多 Agent 系统,基于代理的可观测性捕获的是错误的层。
OpenTelemetry 优先的工具(Arize 的 Phoenix、Traceloop 的 OpenLLMetry、带 GenAI 支持的 SigNoz)发出标准 OTel 格式并导出到任何兼容后端。关键优势:Agent 服务发出 OTel Span,这些 Span 流入 Jaeger、Tempo、Datadog、Honeycomb 或任何其他 OTel 兼容后端,无需供应商锁定。上下文传播仍需正确实现,但至少你在传播 W3C TraceContext——每个 OTel 兼容后端都能理解的标准。
对于多 Agent 系统,OTel 优先是风险最低的选择。你获得供应商灵活性,仪表层是标准化的,当 GenAI 语义规范稳定后,你可以逐步采用而无需更换后端。
可观测性后端中的敏感内容
多 Agent 系统通常处理不应传 输到可观测性后端的用户内容:PII、财务数据、健康信息、机密商业文件。这与为调试故障而捕获提示词和补全内容的可观测性目标形成了张力。
解决这一张力的模式:将敏感内容存储在 Span 事件中,而非 Span 属性中。事件是附加到 Span 的结构化日志条目,可独立于 Span 本身进行过滤。收集管道可以在转发到后端之前剥离带有 sensitive: true 标签的事件,保留 Span 结构和时序,同时丢弃敏感载荷。
这只有在收集管道实际实现了过滤时才有效。在收集阶段定义过滤规则,而非在 Agent 中定义,这样无论哪个 Agent 服务发出数据,策略都会统一执行。
实践起点
如果你从零开始为多 Agent 系统添加仪表,投入产出比最高的顺序是:
首先为 LLM 调用启用 OpenTelemetry 自动仪表,这能以最少的代码获得模型层的可见性。然后在每个消息队列边界添加显式上下文提取和注入——这是孤儿 Span 最容易出现且最难调试的地方。用标准 HTTP 客户端仪表为 Agent 间 HTTP 调用加上仪表,这会自动处理 TraceContext 传播。为编排逻辑添加自定义 Span——任务分解、Agent 选择、结果聚合——使用你自己的属性命名空间。最后,显式为 MCP 服务器调用添加头注入仪表。
不要一次性为所有东西添加仪表。上下文静默丢失的地方是异步和服务边界。先把这些地方处理好,追踪的其余部分自然会连接起来。
目标不是完美的可观测性,而是拥有足够的因果上下文——当多 Agent 工作流产生错误答案时,能在几分钟内判断故障是出在检索、特定 LLM 调用、Agent 交接逻辑,还是返回 格式错误数据的工具中。一条能回答这个问题的连贯追踪,远比一个展示各服务指标却看不出服务间关系的精美仪表板有价值得多。
- https://opentelemetry.io/blog/2024/llm-observability/
- https://opentelemetry.io/docs/specs/semconv/gen-ai/
- https://opentelemetry.io/docs/concepts/context-propagation/
- https://developers.redhat.com/articles/2026/04/06/distributed-tracing-agentic-workflows-opentelemetry
- https://uptrace.dev/blog/opentelemetry-ai-systems
- https://www.w3.org/TR/trace-context/
- https://arize.com/blog-course/traces-spans-large-language-model-orchestration/
- https://oneuptime.com/blog/post/2026-02-06-trace-ai-agent-execution-flows-opentelemetry/view
- https://oneuptime.com/blog/post/2026-02-06-propagate-trace-context-async-boundaries/view
- https://fast.io/resources/ai-agent-distributed-tracing/
- https://signoz.io/blog/otel-baggage/
