跳转至

第 11 章:追踪、跨度与结构化事件

1. 不要先从日志开始,要先从一次事故调查开始

继续沿用同一个支持场景。

用户写道:

我已经等了三天还没有开通访问权限。请检查状态,如果申请卡住了,就创建一个紧急工单。

智能体回复说工单已经创建。十分钟后,值班人员在 helpdesk 里看到了 两张 一模一样的工单。

这时团队面对的是一个非常具体的问题:

  • 是模型自己重复发起了调用;
  • 是超时之后触发了重试;
  • 是工具返回了含糊不清的结果;
  • 是副作用发生在运行时看到错误之前;
  • 还是两个不同的 runs 各自创建了一张工单。

如果你手里只有应用日志和几条指标,这个答案通常既慢又难找。

这就是为什么智能体系统的可观测性不该围绕“日志总量”来设计,而要围绕“能否还原一次运行的历史”来设计。

而在本章里,这个说法有一个刻意收窄的含义:追踪是单次运行的捕获层,还不是那个跨越许多运行的全域级基底,用来做检测、关联与运营可见性。

在本书里,追踪的角色比整个可观测性层更窄。追踪负责捕获执行的原始历史。后面的章节会分别说明可观测性如何把这段历史转成证据基底,评估如何把它转成判断,以及保证或治理如何消费这些结果。

如果你想看到追踪如何与策略、审批、评估、事故和 rollout 判断串成同一条运行记录,可以把单独的 Evidence Spine 页面当作这条桥接层。

2. 为什么普通日志几乎总是不够

当系统简单时,扁平日志和少量指标也许够用。但智能体系统几乎总是更复杂:

  • 一个用户请求会变成多步骤运行;
  • 运行内部有规划、检索、提示组装、工具调用和策略门;
  • 某些步骤会被放到后台;
  • 错误出现的位置可能并不是它真正开始的地方。

如果你只用扁平日志看这些东西,很快就会失去因果关系。你能看到噪音,却看不到执行历史。

对于这个支持事故,这意味着一件很简单的事:没有好的追踪,团队就无法知道是谁创建了重复工单,以及为什么会发生。

3. 追踪是一次运行的故事,span 是一个有意义的步骤

这里最好先固定一个简单模型:

  • trace 描述请求或运行的完整路径;
  • span 描述这条路径中的一个重要步骤;
  • 结构化事件补充那些不该埋在自由文本里的精确信息。

对于同一个支持场景,一次运行可能包含:

  • 策略评估;
  • 检索;
  • 模型推理;
  • 工具执行;
  • 审批等待;
  • 后台记忆更新。

当这种结构存在后,团队就不再把系统看成杂乱调用流,而是看成一串可观察的决策。

这个区分很重要,因为本章并不在讨论如何在整个全域上聚合、关联或告警。它讨论的是,哪些原始执行历史必须被保留下来,后面的那些功能才有可能成立。

4. 在这个支持场景里,追踪应该长什么样

下面这张图的重要性不在“好看”,而在于它能告诉你故障到底可能发生在哪一层。

成熟追踪不该只展示模型,还应展示关键控制点

flowchart LR
    A["用户请求"] --> B["运行追踪"]
    B --> C["策略 span"]
    B --> D["检索 span"]
    B --> E["模型 span"]
    B --> F["工具 span:检查状态"]
    B --> G["工具 span:创建工单"]
    B --> H["审批 span"]
    B --> I["记忆更新 span"]

如果这条追踪被正确采集,团队应该能很快看清:

  • 第二次工具调用是否发生在同一次运行里;
  • 是否出现了重试;
  • idempotency_key 是什么;
  • side_effect_unknown 出现在哪一步;
  • 是否存在审批;
  • 哪个策略门放行了这个动作。

贯穿案例:用 trace 回答争议

在支持分诊事故里,trace 不应该只写“工单已创建”。它应该展示关联的 trace_idsession_ididempotency_key、策略决策、审批状态,以及最终 create_support_ticket 结果。这样,“是模型重复调用,还是重试制造了重复工单?”就不再是猜测,而是对同一条事件链的检查。

5. 哪些东西适合做成独立 span

没必要给每个小细节都建 span,但整个运行只有一个巨大 span 也几乎没用。

一个实用规则是:

  • 编排步骤单独一个 span;
  • 检索单独一个 span;
  • 模型调用单独一个 span;
  • 每次工具调用单独一个 span;
  • 如果策略决策会改变行为,就给它单独一个 span;
  • 如果存在人工审批等待,也单独建一个 span。

这样追踪既保持可读,又能真正告诉你时间、成本和可靠性到底花在了哪里。

6. 结构化事件在纯文本只会碍事的地方最有价值

一个常见错误是:有价值的运营事实被写进了给人看的日志里,结果以后既无法分析,也无法调查。

结构化事件尤其适合这些地方:

  • 策略决策;
  • 工具结果;
  • 提示组装元数据;
  • token 用量;
  • 成本归因;
  • 幂等键;
  • 租户和主体上下文;
  • 记忆写入;
  • 关于运行之后如何被评分的验证器证据。

也就是说,事件应该回答的不是“这条日志怎么写”,而是“以后哪些信息需要被机器当作证据来分析”。

这个区分很重要。追踪还不是裁决,不是策略决策,也不是事故响应动作。它是最底层的原始捕获层,没有它,后续这些功能都无法可靠成立。

7. 好的追踪模型展示的是控制平面,而不只是 LLM 延迟

如果可观测性最终只剩模型响应时间,团队看到的图景会非常扭曲。

现实里,同一个支持运行往往坏在别处:

  • 检索开始返回噪音;
  • 策略引擎过度阻断;
  • 审批等待被拉长;
  • 工具适配器退化;
  • 后台更新堵住队列;
  • 提示组装膨胀了上下文;
  • 写工具返回了模糊结果。

所以追踪模型应覆盖整个控制流,而不只是推理步骤。

但它仍然应该保持为追踪模型。它负责为后续调查保留执行历史。更靠后的可观测性层才负责把许多追踪连接成全域级证据、检测逻辑与运营可见性。

因此,本章会停留在捕获边界上:哪些东西必须被记录、应该如何结构化,以及哪些内容必须在后续审查中幸存下来。后面的可观测性章节讨论的是全域级证据与检测,而不是重新定义什么叫 trace。

8. 追踪和 span 的最小字段集合

如果你希望系统真正便于调查,至少要有:

  • trace_id
  • span_id
  • parent_span_id
  • run_id
  • tenant_id
  • principal_id
  • agent_id 或 workflow id
  • status
  • duration_ms
  • 如果发生模型调用,则有 model_name
  • 如果发生工具调用,则有 tool_name
  • 如果有 gate,则有 policy_decision_id

对于这个支持事故,这些字段已经足够把运行时、工具网关和具体外部副作用串起来。

在更成熟的评估程序里,还应该保留足够的验证器感知的审查链接,不只记录运行里发生了什么,也记录哪些追踪与截图后来支撑了 process_scoreoutcome_scorefailure_attribution

9. 追踪的实用规则

如果要把追踪压缩成一组可执行规则,通常这些就够了:

  1. 每次运行都应该有一个不会在策略、模型和工具 span 之间丢失的 trace_id
  2. 追踪应该覆盖控制平面,而不只是模型延迟。
  3. 所有工具调用、审批等待和策略决策都应该留下机器可读事件。
  4. 不确定性要明确记录:side_effect_unknown 比假装成 success 更有价值。
  5. 脱敏和 schema 稳定性应该一开始就设计进去,而不是等第一次事故审查之后再补。
  6. 如果评估或 rollout 依赖验证器判断,追踪就应该保留指向验证器证据的显式链接。

10. 一个工具执行的结构化事件示例

下面这个模板能很好地展示正确的思路:

event_type: tool_execution
trace_id: trc_01HXYZ
span_id: spn_02ABC
run_id: run_9842
tenant_id: tenant_acme
tool_name: create_ticket
status: success
duration_ms: 842
idempotency_key: act_77f1
policy_decision_id: pol_441
side_effect: created

它远比一条 “ticket tool ok” 的日志有用。

10.1. 对这个案例来说,还有四个字段特别重要

如果目标不只是做仪表盘,而是真正调查事故,通常还值得补上:

  • approval_id
  • tool_principal
  • request_id 或其他业务对象 id
  • result_class
  • verifier_id
  • evidence_refs

这些字段也能帮助团队把运营遥测直接连到后续评分或 rollout 审查,而不是事后再手工重建验证器证据。

正是这些字段,往往能帮你区分:

  • 重复工具调用;
  • 迟到重试;
  • 错误的租户作用域;
  • 模糊的外部响应。

11. 一个简单的 span 发出示例

下面这个骨架不是为了替代 tracing SDK,而是为了说明一个原则:span 不只是开始和结束,它还必须把步骤类型和结果记录成可分析的结构。

from dataclasses import dataclass
from time import monotonic


@dataclass
class SpanResult:
    name: str
    status: str
    duration_ms: int


def traced_step(name: str, fn):
    started = monotonic()
    try:
        fn()
        status = "success"
    except Exception:
        status = "failure"
        raise
    finally:
        duration_ms = int((monotonic() - started) * 1000)
        emit_span(SpanResult(name=name, status=status, duration_ms=duration_ms))


def emit_span(result: SpanResult) -> None:
    print({"span_name": result.name, "status": result.status, "duration_ms": result.duration_ms})

这个例子故意很简单。它的重点不是替代 tracing SDK,而是强调:每个重要步骤都应该留下结构化的痕迹。

12. 哪些东西尤其不能原样写进日志

Observability 不应该变成数据泄漏渠道。

所以追踪和事件里必须特别谨慎对待:

  • 完整提示正文;
  • 原始检索文档;
  • 密钥和 token;
  • PII;
  • 敏感工具载荷。

最实用的规则是:

  • 记录元数据和派生事实;
  • 在有帮助时记录标识符和哈希;
  • 没有充分理由时,不要把完整敏感载荷丢进通用遥测流水线。

13. 智能体可观测性最常见的崩坏点

这些问题非常典型:

  • 追踪只覆盖模型调用;
  • 工具调用无法和原始运行关联;
  • 策略决策在代码里可见,但在遥测里不可见;
  • 事件有了,但没有租户/主体上下文;
  • span 太粗或太噪;
  • 事件 schema 经常变化,导致分析系统失效。

一旦这样,团队又会回到猜测和人工读日志的状态。

这时系统也许已经产生了很多遥测尾气,但仍然没有可靠的原始运行历史。

14. 给智能体可观测性做一次快速成熟度测试

团队不应该只因为已经有仪表盘、日志和模型延迟图表,就把可观测性叫做成熟。

更高的标准应该是:

  • 一次运行可以被端到端重建出来;
  • 策略、模型、工具和审批层都真正可见;
  • 不确定性不会被压扁成伪装的成功;
  • 遥测既能支持事故审查,也能支持发布决策;
  • 敏感数据处理是被设计好的,而不是临时即兴发挥。

如果这些条件不满足,系统也许已经会发遥测,但它还没有运营可观测性。

15. 读完这一章后先做什么

如果你想快速检查可观测性模型,可以先过一遍这个短清单:

  1. 能否通过一个 trace_id 重建完整运行路径?
  2. 检索、模型调用、工具调用和策略门是否都有独立 span?
  3. 幂等键和策略决策 ID 是否被记录?
  4. 遥测中是否带租户/主体上下文?
  5. 能否看见运行时间花在哪里、成本在哪里上涨?
  6. 敏感载荷是否没有泄露到追踪中?
  7. 结构化事件 schema 是否稳定?

如果连续几个答案都是否,那可观测性还只是装饰性的,而不是运行层面的。

16. 接下来读什么

沿着同一条故事线,下一步也很明确:当团队已经能还原一次故障的完整路径后,就该定义什么才算系统每天都处在“健康”状态,也就是进入 SLO。