type
status
date
slug
summary
tags
category
icon
password
起因:玄学的日志
2026 年,WPS 的某个 AI 产品在内部开发一套 Agent 系统。它能帮你排版文档、做 PPT、整理竞品报告。开发初期,团队需要排查一个性能问题:「某些任务执行特别慢,但不知道慢在哪。」
我们尝试通过密密麻麻的日志进行问题定位,发现了一个很尴尬的事实:看不懂啊,哥们、、、
当多个 Agent 并发执行时,日志混在一起,根本分不清哪个操作是哪个 Agent 产的。
用户确认弹窗等了 30 秒,也被算进了工具执行时间,指标全部失真。
一个多步任务串了 5 个工具调用,瓶颈在哪一步?不知道。真不知道…
这不是日志"有没有"的问题,本质上是Agent的可观测性问题。
于是我们开始搭建一套 Agent 执行链路的观测平台。捣鼓了小半个月。
过程中踩了不少坑,有些来自对 OpenTelemetry(OTel)的误用,有些来自 Agent 架构本身的特殊性。这些经验值得记录下来。
先聊聊 OTel 是什么
如果你接触过微服务开发,大概率听过 OTel。如果没听过也没关系,三句话讲清楚:
OpenTelemetry 是一个开源的可观测性框架,专门用来采集软件运行时的数据。它采集三类东西:Trace(链路追踪)、Metrics(指标)、Logs(日志)。
我们这篇文章主要聊 Trace,也就是链路追踪。
核心概念只有三个:
- Trace:一次完整请求的全链路记录。从用户发消息到收到结果,所有操作串成一条链。
- Span:链路中的一次操作记录。有明确的开始和结束时间,是 Trace 的最小单位。一次 Trace 由多个 Span 组成,形成父子树。
- Propagator:负责在组件之间传递链路标识(trace ID)的搬运工,让不同组件产生的 Span 能串成同一条 Trace。
微服务时代,这套体系非常成熟。HTTP 请求进来、处理、返回,每个服务产生一个 Span,trace ID 通过 HTTP header 在服务间传递,一切天然是对的。
但吊诡的是: Agent 并不是微服务。
OTel 在 Agent 场景下哪里不对劲
接 OTel 的时候,我们很快发现微服务的经验套上来会出问题(莫名其妙的第六感)。
逐个来说吧:
1. Span 的边界并不不好画
微服务里的 Span 有天然的闭合边界:一个 HTTP 请求进来,路由到处理函数,返回响应,Span 开始到结束,清清楚楚。
Agent 的执行逻辑不是这样。它的运行模式更像一个不断循环的决策过程:模型推理 → 调工具 → 再推理 → 可能再调另一个工具 → 等用户确认 → 继续。这个循环没有一个明确的"任务完成"信号,模型不知道啥时候结束,而且中间还可能因为用户中断、权限不足、工具报错而随时改变方向。
以 WPS 的AI产品为例,用户说「把帮我写一个商务风格文档」,Agent 的执行过程大致是:
哪些算一个 Span?如果把整条链路压成一个 Span,你只能看到「这个任务一共花了 40 秒」,但不知道 40 秒花在哪。如果拆成六个 Span,你又面临一个问题:「等用户确认」那段时间,Span 应该保持开启状态还是关闭?
反正从最大化的还原每个步骤,我是建议用户等待时间单独作为一个 Span,不计入工具执行时间。
这个决定看起来简单,但背后的原则是:Agent是一个耦合人的行为,但是本质脱离人的系统。
Span 记录的是应该是纯粹的:系统在做事的时间,不是人在做事的时间。把人的时间混进去,所有性能指标都会失真。
2. Propagator 得自己造
微服务之间传 trace ID 靠 HTTP header,天然有通道,OTel 默认的 W3C Trace Context Propagator 直接能用。
Agent 系统不是这样。
所有Agentic产品,主 Agent 和执行层之间是进程内的函数调用,不是 HTTP 请求。没有 HTTP header,OTel 默认的 Propagator 就废了。
给没有太多技术背景的产品单独唠唠:为什么不是HTTP请求就废了。 因为函数调用和 HTTP 请求是两种完全不同的通信方式。函数调用:同一个程序内部,A 直接喊 B 干活,数据通过参数传过去,没有网络参与。就像你坐在工位上,转头跟旁边的同事说一句话。HTTP 请求:A 和 B 是两个独立的服务,跑在不同的机器(或不同的进程)上,通过网络发请求来通信。就像你给另一个城市的同事发一封邮件。关键就在他们的区别:HTTP 请求有"header"这个现成的位置可以塞 trace ID。OTel 的默认 Propagator 就是利用这一点,把 trace ID 编码成一个叫做traceparent的 header 字段,跟着请求一起传过去。函数调用没有 header。你调用一个函数,传的是参数——排版工具("把这段文字改成宋体")。参数里没有一个标准位置给你塞 trace ID。OTel 的默认 Propagator 不知道往哪放。所以就得自己造 Propagator ,而且造起来也不难,本质就是:在函数调用的参数里,硬加一个 trace context 字段,让它模拟 HTTP header 的效果。
如果不管它,子 Agent 产生的 Span 就会变成一条独立的 Trace,跟主 Agent 断开。你在 Jaeger 里看到的是一堆互不相关的链路,看不出「执行排版」是「文档改造任务」的子步骤。
我们做的事说白了就一件:自己实现了一个 Propagator,把 trace ID 塞进函数调用的参数里,子 Agent 收到后从参数里取出来,再创建属于同一条链路的 Span。
打个比方:OTel 的 Propagator 像是快递员,负责把包裹(trace ID)从 A 送到 B。微服务之间有现成的快递通道(HTTP),Agent 之间没有,你得自己修一条。
另外值得注意的是,Span 的创建其实依赖 Propagator 先把上下文传过来。如果 extract 没执行,startSpan 创建出来的就是一个孤链,跟父 Agent 断开了,因此Propagator 不是"可选的增强",是整个链路能串起来的前提。所以也必须造一个。
3. 指标维度爆炸
这是让我们吃了一记闷棍的问题。
OTel 的指标(Metrics)通过维度组合来记录数据。微服务的维度是 HTTP 方法 × 路由 × 状态码,组合数量有限,Prometheus 存得舒舒服服。再多还能有多少呢?都是小case
Agent 的维度是什么呢?工具名 × Skill 名 × 模型名。
WPS 的这个产品有内置工具(文档操作、表格操作、PPT 生成),也有尝试放开让用户自定义Skill。一开始我们对所有工具都做细粒度维度统计,很快 Prometheus 的内存就飙上去了。
问题的本质是维度组合的基数(Cardinality)太高:维度的取值种类越多,产生的数据序列就越多。一个用户自定义了一个工具,Prometheus 就多一条时间序列。几十个用户各定义了十几个工具,序列数量就直接爆炸。说白了,这个问题就是要降低内存压力。
我们的做法是分层处理:
- 内置工具(文档读取、排版、PPT 生成等):细粒度统计,每个工具单独一个维度值,能看到具体哪个工具的调用量和耗时。
- 用户自定义工具:统一归入
custom_tool类别,不区分具体是哪个。
用户自定义工具的调用量总量可以看,但具体是哪个被调了、耗时多少,看不到。这是精度和成本的权衡。内置的十几个工具组合数量可控,用户自建的工具数量不可预测,必须封口。
4. 多 Agent 的日志怎么区分
单个 Agent 跑的时候,日志是线性的,顺着看就行。
但 WPS 的复杂任务会派出多个子 Agent 同时工作。比如「帮我做一份竞品分析 PPT」,一个子 Agent 负责调研,一个负责写内容,一个负责生成 PPT。三个并行跑,日志混在一起,你根本分不清哪条是哪个 Agent 的。
而且不只是"分不清"的问题——子 Agent 的执行结果会反过来影响主 Agent 的决策。比如调研子 Agent 返回了两份报告,主 Agent 看完决定要补充一个方向,于是又创建了一个新的子 Agent。这个因果链如果不记录下来,事后排查的时候你根本还原不了"为什么主 Agent 突然多做了一步"。
解决方案分两层:
第一层:给每个 Agent 实例一个唯一 ID,所有日志都带上这个 ID。 查日志的时候按 ID 过滤,单个 Agent 的完整链路就清晰了。这是基本功,没啥好说的。
第二层:记录跨 Agent 的因果关系,这也是比较重要的。OTel 有一个机制叫 Span Link,可以给 Span 打上因果关联标记——这个 Span 的存在,是受哪个 Span 的结果影响的。理论上能解决因果追踪的问题。不过没有什么好用的可视化工具对 Span Link 的展示进行支持,所有记录的因果关系,都要去看一行又一行的纯文本标注。
结果就是:可以看,但是费时费力。
目前我们的做法是,在 UI 里给子 Agent 做独立卡片展示,通过 Agent ID 字段手动关联因果关系。也能用反正。
一个完整的 Trace 长什么样
用一个实际场景来展示。假设用户说:「帮我做一份 AI Agent 市场的竞品分析 PPT,明天下午给客户看。」
下面是这个任务在 OTel 里的完整 Trace 结构:
这个图里有几个关键点值得标注:
Propagator 发生的位置:只在主 Agent 向子 Agent 传递任务时出现(Span 3、5、6 开头的
Propagator.inject)。主 Agent 内部推理(Span 1 到 Span 2、Span 4 到 Span 5)不需要 Propagator,同组件内的上下文是自动传递的。用户确认等待的时间:Span 2 和 Span 3 之间,用户在思考该选什么风格。这段时间没有任何 Span 在计时,不会污染工具执行指标。
错误和返工的记录:Span 3.2.2 的
web_fetch 失败(HTTP 403),Span 6.3 的视觉校验发现问题触发返工。Trace 记录的是实际发生的路径,不是预设路径,失败和重试都会留下痕迹。并行分支:Span 3.1(国内竞品)和 Span 3.2(海外竞品)在同一时刻并行执行,各自独立产生子 Span,最终都汇聚回 Span 3。
写在最后
回过头看,Agent 可观测性不是「接上 OTel 就完事」的事。微服务时代积累的工具链和方法论,在 Agent 场景下需要重新适配。
Span 的生命周期假设不适用,得重新定义操作边界。上下文传播没有现成通道,得自己造 Propagator。指标维度会爆炸,得做分层治理。多 Agent 的因果关系追踪,工具链还没跟上。
OTel 本身的设计是好的,扩展接口是留了的,但用的人得根据自己的场景去填。Agent 不是微服务,Agent 的可观测性也不能照搬微服务的做法。
这不是 OTel 的问题,是范式变了。
- 作者:阿旭
- 链接:https://tangly1024.com/example-7
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。




