第 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_id、session_id、idempotency_key、策略决策、审批状态,以及最终 create_support_ticket 结果。这样,“是模型重复调用,还是重试制造了重复工单?”就不再是猜测,而是对同一条事件链的检查。
5. 哪些东西适合做成独立 span¶
没必要给每个小细节都建 span,但整个运行只有一个巨大 span 也几乎没用。
一个实用规则是:
- 编排步骤单独一个 span;
- 检索单独一个 span;
- 模型调用单独一个 span;
- 每次工具调用单独一个 span;
- 如果策略决策会改变行为,就给它单独一个 span;
- 如果存在人工审批等待,也单独建一个 span。
这样追踪既保持可读,又能真正告诉你时间、成本和可靠性到底花在了哪里。
6. 结构化事件在纯文本只会碍事的地方最有价值¶
一个常见错误是:有价值的运营事实被写进了给人看的日志里,结果以后既无法分析,也无法调查。
结构化事件尤其适合这些地方:
- 策略决策;
- 工具结果;
- 提示组装元数据;
- token 用量;
- 成本归因;
- 幂等键;
- 租户和主体上下文;
- 记忆写入;
- 关于运行之后如何被评分的验证器证据。
也就是说,事件应该回答的不是“这条日志怎么写”,而是“以后哪些信息需要被机器当作证据来分析”。
这个区分很重要。追踪还不是裁决,不是策略决策,也不是事故响应动作。它是最底层的原始捕获层,没有它,后续这些功能都无法可靠成立。
7. 好的追踪模型展示的是控制平面,而不只是 LLM 延迟¶
如果可观测性最终只剩模型响应时间,团队看到的图景会非常扭曲。
现实里,同一个支持运行往往坏在别处:
- 检索开始返回噪音;
- 策略引擎过度阻断;
- 审批等待被拉长;
- 工具适配器退化;
- 后台更新堵住队列;
- 提示组装膨胀了上下文;
- 写工具返回了模糊结果。
所以追踪模型应覆盖整个控制流,而不只是推理步骤。
但它仍然应该保持为追踪模型。它负责为后续调查保留执行历史。更靠后的可观测性层才负责把许多追踪连接成全域级证据、检测逻辑与运营可见性。
因此,本章会停留在捕获边界上:哪些东西必须被记录、应该如何结构化,以及哪些内容必须在后续审查中幸存下来。后面的可观测性章节讨论的是全域级证据与检测,而不是重新定义什么叫 trace。
8. 追踪和 span 的最小字段集合¶
如果你希望系统真正便于调查,至少要有:
trace_idspan_idparent_span_idrun_idtenant_idprincipal_idagent_id或 workflow idstatusduration_ms- 如果发生模型调用,则有
model_name - 如果发生工具调用,则有
tool_name - 如果有 gate,则有
policy_decision_id
对于这个支持事故,这些字段已经足够把运行时、工具网关和具体外部副作用串起来。
在更成熟的评估程序里,还应该保留足够的验证器感知的审查链接,不只记录运行里发生了什么,也记录哪些追踪与截图后来支撑了 process_score、outcome_score 或 failure_attribution。
9. 追踪的实用规则¶
如果要把追踪压缩成一组可执行规则,通常这些就够了:
- 每次运行都应该有一个不会在策略、模型和工具 span 之间丢失的
trace_id。 - 追踪应该覆盖控制平面,而不只是模型延迟。
- 所有工具调用、审批等待和策略决策都应该留下机器可读事件。
- 不确定性要明确记录:
side_effect_unknown比假装成success更有价值。 - 脱敏和 schema 稳定性应该一开始就设计进去,而不是等第一次事故审查之后再补。
- 如果评估或 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_idtool_principalrequest_id或其他业务对象 idresult_classverifier_idevidence_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. 读完这一章后先做什么¶
如果你想快速检查可观测性模型,可以先过一遍这个短清单:
- 能否通过一个
trace_id重建完整运行路径? - 检索、模型调用、工具调用和策略门是否都有独立 span?
- 幂等键和策略决策 ID 是否被记录?
- 遥测中是否带租户/主体上下文?
- 能否看见运行时间花在哪里、成本在哪里上涨?
- 敏感载荷是否没有泄露到追踪中?
- 结构化事件 schema 是否稳定?
如果连续几个答案都是否,那可观测性还只是装饰性的,而不是运行层面的。
16. 接下来读什么¶
沿着同一条故事线,下一步也很明确:当团队已经能还原一次故障的完整路径后,就该定义什么才算系统每天都处在“健康”状态,也就是进入 SLO。