# 18. Agent 可观测体系

# 18.1 原理:从系统可用性到 Agent 行为质量

生产 Agent 的可观测性应包含五层:

  1. Log:离散诊断文本,适合错误栈和局部上下文。
  2. Metric:低基数聚合,适合延迟、吞吐、错误率、Token 和成本告警。
  3. Trace:还原 Session → Turn → Model → Tool → Sandbox/MCP/Subagent 的因果链。
  4. Event/Rollout:可回放的业务事实,保留模型输入输出、状态迁移和证据引用。
  5. Eval:对轨迹与最终结果做正确性、安全性、效率和用户体验评价。

传统服务只看 QPS/P99/5xx 不够,因为 Agent 可能“HTTP 200,但做错了”。应同时测系统指标和行为指标。

# 18.2 Trace 与事件模型

推荐层级:

trace(session or interaction)                                   // 根 Trace 覆盖一次会话或用户交互,承载全链路关联标识
└─ turn                                                         // Turn Span 表示处理一次用户目标的端到端生命周期
   ├─ context.build / retrieval / compaction                    // 记录上下文构建、召回和压缩各阶段的耗时与 Token 变化
   ├─ model.request(attempt=1..N)                                // 每次模型请求独立成 Span,并标记重试次数、模型和用量
   ├─ tool.call(name, call_id)                                  // 每个工具调用用稳定 call_id 关联提议、执行结果和 Rollout 事件
   │  ├─ permission.wait                                        // 单独度量等待用户或策略审批造成的暂停时间
   │  ├─ sandbox.exec                                           // 记录受限进程的启动、执行、退出状态和沙箱违规信息
   │  └─ hook.pre / hook.post                                   // 追踪工具执行前后 Hook 的延迟、修改和失败情况
   └─ subagent.run → linked child trace                         // 子 Agent 使用独立 Trace,并以 Link 保留与父 Turn 的因果关系

每个事件至少带:session_id/thread_id/turn_id/step_id/call_id/agent_id/parent_id/sequence/timestamp/model/tool/status。跨 MCP、远程执行或子 Agent 时传播 trace context;无法建立严格父子 Span 时使用 span link。

不要把完整 Prompt、密钥、文件内容直接作为 Metric tag 或默认 Trace attribute。高敏原文进入受控 Rollout;Telemetry 只记录哈希、大小、分类和显式允许的采样内容。

# 18.3 指标体系

类别 核心指标
可靠性 turn success/abort、tool error、sandbox denial、resume success、stuck turn
延迟 TTFT、TTFM、turn E2E、model/tool/approval/compaction duration
效率 input/output/cache token、模型调用数、工具调用数、重试数、压缩率
质量 task completion、verifier pass、replan rate、用户纠正率、回滚率
安全 approval requested/denied、越权请求、Prompt Injection 命中、沙箱违规
Multi-Agent spawn success、queue time、消息延迟、重复工作率、handoff success

Metric 标签只保留有限枚举,例如 model family、tool class、status、sandbox type;不要使用 session id、完整命令、文件路径等高基数字段。

# 18.4 Claude Code 源码映射

Claude Code 初始化 OpenTelemetry 的 Log、Metric 与 Trace Provider,并支持 Console、OTLP、Prometheus 等导出方式:instrumentation.ts:1instrumentation.ts:69instrumentation.ts:130

Session Trace 将一次用户交互作为根 Span,下设 llm_request/tool/tool.blocked_on_user/tool.execution/hook;通过 AsyncLocalStorage 维持异步上下文,并用 TTL 清理因异常中断而没有正常结束的 Span:sessionTracing.ts:1sessionTracing.ts:49sessionTracing.ts:65sessionTracing.ts:85

工具链会记录 schema/语义校验错误、权限决策来源、Hook 与工具结果大小。权限非 ask 时补发 tool_decision OTel 事件,避免 headless 路径丢失决策观测:toolExecution.ts:614toolExecution.ts:916toolExecution.ts:948

# 18.5 Codex 源码映射

Codex SessionTelemetry 将 thread、模型、来源、终端和认证模式等作为 Session 元数据,并统一暴露 counter、histogram 和 duration API:session_telemetry.rs:87session_telemetry.rs:164。指标常量覆盖 tool/API/SSE/WebSocket、Turn E2E/TTFT/TTFM、Memory、Token、Hook 和 Goal:names.rs:1

TurnTimingState 不只测 TTFT/TTFM,还把一次 Turn 分解为采样、压缩、工具阻塞和采样间开销,并统计采样请求与重试次数:turn_timing.rs:43turn_timing.rs:58turn_timing.rs:145。这比一个总耗时更能回答“慢在模型、工具、压缩还是等待用户”。

事件发送链先记录 Turn/Tool Trace,再持久化协议事件、写入 protocol trace 并交付客户端:session/mod.rs:1826session/mod.rs:2061。ItemStarted/Completed 还保存开始和结束时间:session/mod.rs:2083

# 18.6 埋点伪代码

function runTool(call, traceCtx):                                // 定义同时具备 Trace、Metric 和 Rollout 记录的工具执行入口
    span = tracer.startSpan("tool.call", parent=traceCtx, attrs={ // 创建工具 Span,并挂到当前 Turn 或 Step 的追踪上下文下
        tool_class: classify(call.name),                         // 只记录低基数工具类别,避免把任意工具名变成高基数标签
        sandbox: call.sandboxMode                                // 记录所用沙箱模式,便于比较不同安全配置的成功率和延迟
    })                                                          // 结束 Span 初始属性对象的构造
    timer = monotonicNow()                                       // 用单调时钟记录起点,避免系统时间调整导致负耗时
    try:                                                         // 进入主执行分支,确保任何退出路径最终都会关闭 Span
        decision = permission.check(call)                        // 对规范化工具参数执行权限策略判断,得到允许、拒绝或询问
        metrics.count("approval", tags={decision, tool_class})  // 按有限枚举累计权限决策次数,监控审批摩擦与拒绝率
        result = sandbox.execute(call)                           // 在有效权限 Profile 对应的沙箱内执行工具,阻止越界副作用
        metrics.hist("tool.duration", now()-timer, tags={status:"ok"}) // 记录成功路径端到端工具耗时的直方图
        rollout.append(redactedToolEvent(call, decision, result)) // 将脱敏后的调用、决策和结果追加到可回放事实日志
        return result                                            // 向 Agent Loop 返回结构化结果,作为下一次模型采样的观察
    catch error:                                                 // 捕获权限、沙箱或工具本身抛出的异常
        span.recordException(redact(error))                      // 将脱敏异常写入 Trace,避免泄露命令内容、密钥或文件正文
        metrics.count("tool.error", tags={classify(error), tool_class}) // 按稳定错误类别和工具类别累计失败指标
        throw error                                              // 重新抛出异常,让上层 Runtime 决定重试、重规划或终止
    finally:                                                     // 无论成功还是异常都执行清理逻辑
        span.end()                                               // 关闭 Span 并计算持续时间,避免产生永不结束的追踪节点

# 18.7 高频面试题

问:如何定位 Agent 一次任务为什么失败?

先按 trace 找到失败 Turn,再看计划状态、Context 版本、模型 stop reason、工具和审批链;用 Rollout 重放确定是知识缺失、规划错误、工具错误、权限阻断还是验证错误。最后用同一失败分类聚合 Metric,判断个例还是系统性回归。

问:Trace 与 Eval 的关系?

Trace/Rollout 提供可评测轨迹;Eval 给轨迹和结果打分。没有稳定事件模型,Eval 只能看最终文本,无法评价工具选择、步骤效率、权限风险和错误恢复。

# 18.8 原理深化:Trace、Metric、Rollout 与 Eval 必须同源关联

四类数据承担不同职责:Rollout 保存可回放业务事实,Trace 保存一次执行的因果与时间,Metric 保存低基数聚合,Eval 保存对结果与轨迹的判定。它们不应分别埋点后靠时间戳猜关联,而要共享 thread_id/turn_id/step_id/call_id,并从同一领域事件派生。这样一条 ToolCallCompleted 可以同时追加 Rollout、结束 Span、增加 counter,并成为 Eval 的一个轨迹节点。

源码中的 TurnTimingState 把模型采样、压缩、工具阻塞和采样间开销分开,说明端到端慢不是单一指标;Session 事件发送路径又同时处理 protocol trace、持久化和客户端投递,说明观测应靠统一出口而非每个 Tool 自行打印:turn_timing.rs:43session/mod.rs:2061。若 Trace 记录的是调用前参数、Rollout 保存的是 Hook 改写后参数,就必须把两者建成两个显式事件,不能覆盖成看似同一动作。

数据治理同样是可观测原理的一部分:Metric 禁止高基数 ID 和原文,Trace 默认记录大小、hash、分类与脱敏错误,敏感 Prompt/ToolResult 只进入受控 Rollout并受保留策略管理;Eval 读取生产轨迹时采用授权采样。质量回归要沿失败分类聚合,例如 Context 缺失、规划错误、权限拒绝、工具失败、验证失败,而不是只看“用户是否点了赞”。

最后更新: 2026/8/5 22:05:21