跳转至

第 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 结果。这样,“是模型重复调用,还是重试制造了重复工单?”就不再是猜测,而是对同一条事件链的检查。

追踪案例主线说明(Trace case-spine note):追踪结构(trace structure)应该区分三个规范案例(canonical cases)。支持分诊(Support triage)需要关联工具跨度(tool spans)、审批状态(approval status)、idempotency_key 和最终 create_support_ticket 结果(outcome)。内部知识助手(Internal knowledge assistant)需要带有来源标识(source identifiers)、新鲜度标记(freshness markers)、租户范围(tenant scope)和记忆写入事件(memory-write events)的检索跨度(retrieval spans)。事故协调(Incident coordination)需要升级跨度(escalation spans)、通知结果(notification outcomes)、响应者角色变更(responder-role changes)和事故状态事件(incident-state events),让事件后复盘(post-incident review)看到决策链(decision chain),而不只是最终状态(final status)。

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

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

一个实用规则是:

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

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

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

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

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

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

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

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

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

8.1. 生产可观测性需要可搜索 span,而不只是漂亮 trace

AWS AgentCore AgentOps 和 Microsoft Foundry 最近的材料说明,生产可观测性很快会超出“打开一条 trace 看看”的范围。134 团队需要可以按模型、工具、错误、延迟、成本、策略状态和隐私状态过滤的 searchable spans。否则 trace 虽然适合手工调试,但很难支撑 regression review、值班和成本评估。

AWS 关于用 AgentCore Observability 调试生产智能体的材料又补了一条实用门槛:trace 不只要能调查显式异常,还要能调查 silent failure、infinite loop 和 tool invocation failure。2 这意味着 span 不能只有泛泛的 status,还要能看到 reasoning step、被选中的 tool selection、工具结果、latency/retry context,以及 workflow break 发生的位置。这样 debugging workflow 才能从症状走到具体 decision/tool span,而不是停在“模型不知为何循环了”。

最小 production span contract 应该扩展出这些字段:

  • span_typemodel_calltool_callretrievalpolicy_gateapproval_waithandoffmemory_write
  • input_refoutput_ref:链接、哈希或 redacted artifact pointers,而不是原始 prompt/output 正文;
  • latency_msretry_counterror_classresult_class
  • token_input_counttoken_output_counttoken_costmodel_name
  • tool_nametool_principalapproval_statepolicy_decision_id
  • pii_redactedredaction_policy_idretention_class
  • trace_search_tags:owner、scenario、release、eval dataset 或 incident id。

这样的契约让 trace 不只回答“发生了什么”,还可以回答“这个 release 后有哪些相似 runs”、“cost 为什么上涨”、“哪个 tool 开始退化”、“哪些 span 可以在不带 PII 的情况下交给 verifier”,以及“哪些 traces 应进入 regression review”。

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

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

9. 追踪的实用规则

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

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

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 审查,而不是事后再手工重建验证器证据(verifier evidence)

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

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

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

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

from time import monotonic

from agent_runtime_ref.models import ToolResult


def traced_step(name: str, fn):
    started = monotonic()
    status = "success"
    try:
        result = fn()
        if isinstance(result, ToolResult) and result.status != "success":
            status = "failure"
        return result
    except Exception:
        status = "failure"
        raise
    finally:
        duration_ms = int((monotonic() - started) * 1000)
        emit_span(name=name, status=status, duration_ms=duration_ms)


def emit_span(*, name: str, status: str, duration_ms: int) -> None:
    print(
        {
            "span_name": name,
            "status": status,
            "duration_ms": duration_ms,
        }
    )

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

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

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

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

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

最实用的规则是:

  • 记录元数据和派生事实;
  • 非密钥哈希只用于非秘密完整性校验;敏感值关联应使用 keyed HMAC;
  • 没有充分理由时,不要把完整敏感载荷丢进通用遥测流水线。

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

这些问题非常典型:

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

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

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

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

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

更高的标准应该是:

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

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

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

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

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

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

16. 接下来读什么

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