# 18. Agent 可观测体系
# 18.1 原理:从系统可用性到 Agent 行为质量
生产 Agent 的可观测性应包含五层:
- Log:离散诊断文本,适合错误栈和局部上下文。
- Metric:低基数聚合,适合延迟、吞吐、错误率、Token 和成本告警。
- Trace:还原 Session → Turn → Model → Tool → Sandbox/MCP/Subagent 的因果链。
- Event/Rollout:可回放的业务事实,保留模型输入输出、状态迁移和证据引用。
- 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:1、instrumentation.ts:69、instrumentation.ts:130。
Session Trace 将一次用户交互作为根 Span,下设 llm_request/tool/tool.blocked_on_user/tool.execution/hook;通过 AsyncLocalStorage 维持异步上下文,并用 TTL 清理因异常中断而没有正常结束的 Span:sessionTracing.ts:1、sessionTracing.ts:49、sessionTracing.ts:65、sessionTracing.ts:85。
工具链会记录 schema/语义校验错误、权限决策来源、Hook 与工具结果大小。权限非 ask 时补发 tool_decision OTel 事件,避免 headless 路径丢失决策观测:toolExecution.ts:614、toolExecution.ts:916、toolExecution.ts:948。
# 18.5 Codex 源码映射
Codex SessionTelemetry 将 thread、模型、来源、终端和认证模式等作为 Session 元数据,并统一暴露 counter、histogram 和 duration API:session_telemetry.rs:87、session_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:43、turn_timing.rs:58、turn_timing.rs:145。这比一个总耗时更能回答“慢在模型、工具、压缩还是等待用户”。
事件发送链先记录 Turn/Tool Trace,再持久化协议事件、写入 protocol trace 并交付客户端:session/mod.rs:1826、session/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:43、session/mod.rs:2061。若 Trace 记录的是调用前参数、Rollout 保存的是 Hook 改写后参数,就必须把两者建成两个显式事件,不能覆盖成看似同一动作。
数据治理同样是可观测原理的一部分:Metric 禁止高基数 ID 和原文,Trace 默认记录大小、hash、分类与脱敏错误,敏感 Prompt/ToolResult 只进入受控 Rollout并受保留策略管理;Eval 读取生产轨迹时采用授权采样。质量回归要沿失败分类聚合,例如 Context 缺失、规划错误、权限拒绝、工具失败、验证失败,而不是只看“用户是否点了赞”。