首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent 可观测性:Trace / Span 语义约定与任务回放

Agent 可观测性:Trace / Span 语义约定与任务回放

原创
作者头像
Archive
发布于 2026-10-09 09:21:25
发布于 2026-10-09 09:21:25
200
举报
文章被收录于专栏:Agent 架构Agent 架构

为什么 Agent 的观测不能照搬 APM

先看一个判断上的差异。

普通微服务里,"出问题"的信号是明确的:异常率涨了、P99 抖了、某个下游返回 5xx。观测系统的任务是把这些信号关联起来,回答"哪个依赖挂了"。

Agent 不是这样。它的典型故障长这样:任务跑完了,没有报错,耗时和 token 都在正常区间,但结果是错的。或者:任务没跑完,卡在第 6 步等一个永远不返回的工具调用,而监控上一切正常。

所以照搬 APM 会碰到三个结构性差异。

边界不同。 一个用户请求对应的是"一次任务",任务里包含若干步决策,每一步又可能触发一次模型调用和若干次工具调用。层级不是干净的 RPC 树,而是"任务 → 步骤 → 调用"三层,且步骤的数量在运行前是未知的。

失败形态不同。 服务会抛异常,模型不会。模型会给一个语法完全合法、语义完全错误的答案。超时也可能是"慢"而不是"挂",或者干脆是输出被截断。

内容即数据。 服务调用的关键信息是状态码和耗时;Agent 调用的关键信息是它看到了什么、决定了什么。只记耗时和状态码,等于什么都没记。

数据模型:任务是一条 Trace,四类 Span

我会把观测模型定成:一次任务 = 一条 Trace,Span 按角色分四类。

  • task span:整个任务。承载 task_id、发起方、最终状态(成功/失败/放弃/待人工)、总步数。
  • step span:一步决策。承载 step_seq、当前意图、被判定的结果。
  • model span:一次模型调用。承载模型名、采样参数、输入输出 token 数、首 token 延迟、总时长、是否命中缓存。
  • tool span:一次工具调用。承载工具名、参数摘要、返回摘要、耗时,以及副作用等级(读 / 幂等写 / 非幂等写)。

最后这一项是 Agent 特有的,也是最有用的:线上出问题时,你要能一眼看出"这次任务动了几次非幂等写"。

如果你们已经在用 OpenTelemetry,可以直接沿用它的 GenAI 语义约定方向——它已经覆盖了模型名、token 用量、请求参数、响应标识这几类属性。Agent 特有的(任务、步骤、副作用等级)再自己加命名空间,不要硬挤进模型那套属性里。

首 token 延迟和总时长必须分开

这一条值得单独拎出来,因为它经常被合并成一个"耗时"。

两个指标指向完全不同的问题:

  • 首 token 延迟高 → 输入太长、排队、或者上游限流。用户的感受是"点了没反应"。
  • 总时长高但首 token 正常 → 输出太长,或者工具调用占了大头。用户的感受是"一直在输出"。

合并成一个耗时之后,这两种情况在监控上长得一模一样,而优化方向完全相反:前者要压上下文和排队,后者要压输出长度和工具耗时。

还有一个 Agent 特有的信号:一次任务里的重试次数。重试率突然上升,往往不是模型的问题,而是某个工具开始变慢或开始返回格式错的东西。

内容怎么存:引用加摘要,不存全文

Span 里塞原始输入输出会让存储迅速失控。一个检索工具返回几万字是常事。

稳妥的做法是:Span 里存引用 + 摘要 + 哈希。

代码语言:python
复制
span.set_attribute("input_ref", "oss://agent-trace/{trace_id}/{span_id}/in")
span.set_attribute("input_len", len(payload))
span.set_attribute("input_hash", sha256(canonical(payload)))
span.set_attribute("input_preview", payload[:200])

三个字段各有用途:

input_ref 让你在需要时能取回全文,用于回放。

input_hash 有两个额外好处。一是去重:同一段输入在两个任务里出现,可以直接看出是重复调用。二是调试缓存:前缀缓存的命中判定本质上是哈希比对,这个字段能直接告诉你"缓存为什么没命中"。

input_preview 是给人看的。九成的排查只需要看这两百个字符。

顺带说一句:内容里必然带敏感信息。所以 Span 落库前要有脱敏环节,并且给引用设置保留期限——观测数据的保存周期通常应该短于业务数据。

采样:不能按固定比例

微服务里按 1% 采样是可以的,Agent 不行。原因是"这次执行有没有价值"要等 Trace 结束才知道。

所以要用尾部采样(tail-based sampling),规则大致是:

  • 无条件全量保留:失败、放弃、转人工、走过补偿、重试次数超阈值、被判定为不确定态的
  • 按比例采样:成功且步数正常的
  • 完全不采样:重复调用且输入哈希相同的(保留计数即可)

一个容易被忽略的点:慢不等于有价值。很多团队按耗时排序看 Trace,结果只看到了一堆慢但成功的请求。真正该被看的是"结果不对"的那批,而它们往往又快又安静。

回放:三种能力,按难度递进

回放是观测系统的终点,也是它区别于日志的地方。我把它分三级。

一级:日志回放。 把一条 Trace 渲染成可读的时间线——第几步、模型看到了什么、决定了什么、调了什么工具、返回了什么。这一级最容易做,也最能立刻解决问题。大多数团队做到这一级,就能处理八成以上的线上排查。

二级:决策回放。 把当时喂给模型的完整上下文逐字重现,然后用同样的参数重新请求一次。这要求上下文装配层输出结构(前缀、尾部、被丢弃的内容),而且要求装配是确定性的——这正好是上下文装配那一篇里强调过的性质。做不到确定性,这一级就没法做。

三级:副作用回放。 在沙箱里用打桩的外部依赖重跑同类任务,看行为是否一致。这一级依赖前面两件事:步骤要标副作用等级,外部调用要能打桩。它更接近"回归测试"而不是"排查",所以优先级可以往后放。

我的建议是明确只做一级,把它做扎实,别一上来就规划三级。

一个反对意见

我不建议第一版就接全套 OTel collector + 后端。链路长、组件多,而你现在真正需要的只是"能按 task_id 查出一条时间线"。

先把 Span 写成一张普通的关系表(或者一个支持 JSON 字段的列存表),用 SQL 能查。等数据量和查询复杂度真的顶不住了,再往标准栈上迁。

观测系统的第一版,目标是让自己愿意打开它,而不是架构正确。

落地顺序

  1. 定 Span 四类角色和字段,先把 task 和 step 两级跑通。
  2. 加 input_ref / input_hash / input_preview 三件套,脱敏规则同时定。
  3. 做日志回放的时间线页面——这是投入产出比最高的一步。
  4. 补成本与延迟维度(首 token / 总时长 / 重试次数分开)。
  5. 最后做尾部采样和决策回放。

收尾

Agent 的观测不是"给模型调用加个埋点",而是给一个多步、跨进程、失败不报错的过程建立可追问的记录。它的验收标准很朴素:线上有人说"这次结果不对",你能在几分钟内打开一条时间线,指着某一步说——问题在这儿。


同系列:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么 Agent 的观测不能照搬 APM
  • 数据模型:任务是一条 Trace,四类 Span
  • 首 token 延迟和总时长必须分开
  • 内容怎么存:引用加摘要,不存全文
  • 采样:不能按固定比例
  • 回放:三种能力,按难度递进
  • 一个反对意见
  • 落地顺序
  • 收尾
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档