第十一章最后留下了一个问题:当 skill、MCP、hook、dynamic tool 和多 agent 都进入同一套 LOOP,系统怎样解释“为什么模型没用某项能力”? 传统服务的可观测性主要回答“请求是否成功、耗时多少、哪里报错”。Agent harness 还必须回答更难的问题:模型当时看到了什么、为什么决定调用这个工具、审批和策略怎样改变了行动、结果是否真正回到上下文、恢复后的执行与过去有什么关系。 因此,Codex 的可观测性不只是 logs、traces 和 metrics。前端事件、rollout 与诊断 replay 共同组成了一条从实时现象到历史证据的观察链。 本章关心的不是“埋了多少点”,而是如何把一次不可预先确定的 agent 行为,还原成一条可定位、可归因、可解释的因果链。
12.1 为什么 agent 比普通服务更难观察
普通 HTTP 服务通常有相对确定的控制流:
收到请求 → 校验参数 → 查询数据库 → 返回响应
请求慢了,可以沿着固定调用链检查数据库、缓存和下游服务;请求错了,可以从状态码和异常堆栈定位失败点。
Agent 的控制流不是提前写死的。一次用户输入可能经历:
构造上下文
→ 模型采样
→ 输出 reasoning
→ 调用两个工具
→ 等待用户批准
→ 并行派生三个子 agent
→ 再次采样
→ 触发压缩
→ 切换模型连接
→ 给出最终回答
下一步做什么,由模型根据当时可见的历史、instruction、工具 spec 和工具结果临时决定。相同输入在不同世界状态下,可能走出完全不同的路径。
这给可观测性增加了四种困难。
12.1.1 控制流是运行时生成的
工具调用不是业务代码里固定的一行函数调用,而是模型输出的 item。要解释一次行动,不能只看工具执行阶段,还要知道:
- 本轮有哪些工具对模型可见;
- 模型生成了什么调用;
- harness 是否接受并正确解析;
- policy、审批和沙箱是否允许;
- 工具结果是否回灌;
- 回灌后模型是否继续采样。
只观察执行器,会错过行动产生之前和结果回灌之后的半条链路。
12.1.2 “成功”有多个层次
一次 MCP 请求返回成功,不代表任务成功;命令退出码为零,不代表模型正确理解了输出;turn 正常结束,也不代表用户目标达成。
至少要区分:
| 层次 | “成功”意味着什么 |
|---|---|
| 传输 | 请求和流没有断开 |
| 协议 | item、delta、工具参数能被正确解析 |
| 执行 | 工具或外部服务完成了动作 |
| LOOP | 结果进入上下文,循环正常推进 |
| 任务 | agent 给出了结果或明确说明阻塞 |
| 用户目标 | 最终产物真的满足需求 |
前四层可以由 harness 较可靠地观察;最后一层通常仍需要用户反馈、评测或业务系统验证。
12.1.3 等待与计算混在同一条时间线上
一个 turn 用了十分钟,可能是:
- 模型首 token 很慢;
- 工具在排队;
- 命令执行很慢;
- MCP server 响应很慢;
- 等了用户九分钟才批准;
- 上下文过长,触发了 compaction;
- 模型流断开后发生重试;
- 子 agent 尚未返回。
只记录一个 turn_duration = 10min 几乎没有诊断价值。Agent 可观测性必须把模型时间、工具时间、调度时间、人工等待时间和 harness 自身开销拆开。
12.1.4 现在看到的状态,不一定能解释过去
工具目录会刷新,配置会变化,plugin 会升级,工作区文件会被修改。故障发生半小时后再查看当前状态,可能已经无法回答:
- 当时模型看到的是哪一版 tool spec?
- 那个 skill 是否已经激活?
- 策略为何拒绝了命令?
- 子 agent 收到的究竟是哪条消息?
- 模型请求中实际包含哪些历史 item?
因此,agent harness 不仅需要实时 telemetry,还需要能在事后重建语义关系的 replay 证据。
可观测性的目标不是证明“系统记录过什么”,而是让人能从证据回答:发生了什么、为何发生、时间花在哪里、现在能否安全继续。
12.2 五种证据:每一种都只回答一部分问题
Codex 中可以观察到五类互补的信息:
| 证据 | 最擅长回答 | 不擅长回答 |
|---|---|---|
| 前端 Event | 用户当时看到了什么,turn/item 如何推进 | 跨服务内部耗时、未对前端公开的细节 |
| Logs | 某个时刻发生了什么错误,携带哪些局部字段 | 完整调用树、总体趋势 |
| Traces | 一次请求跨组件经过哪里,各阶段耗时和父子关系 | 长期统计、完整恢复语义 |
| Metrics | 错误率、延迟分布、吞吐、资源和容量趋势 | 单次异常的完整上下文 |
| Replay evidence | 过去的语义事实如何关联,模型实际看到了什么 | 低成本实时告警 |
它们不是重复记录同一件事,而是从不同角度投影同一段执行。
flowchart TB
RUN["一次真实执行"]
RUN --> EVT["前端 Event<br/>实时交互事实"]
RUN --> LOG["Logs<br/>局部诊断细节"]
RUN --> TR["Traces<br/>跨组件因果与耗时"]
RUN --> MET["Metrics<br/>聚合趋势与告警"]
RUN --> REP["Replay evidence<br/>离线语义重建"]
EVT --> UI["重建用户所见"]
LOG --> DEBUG["定位具体错误"]
TR --> PATH["追踪请求路径"]
MET --> SLO["发现系统性退化"]
REP --> WHY["解释过去为何如此行动"]
这五类证据对应五种不同的问题:
Event:界面为什么停在这里?
Log:这一刻具体报了什么?
Trace:时间花在哪条链路?
Metric:这是个例,还是系统性问题?
Replay:模型当时依据哪些事实作出决定?
如果只保留其中一种,都会产生盲区。
- 只有 logs:能搜索错误,却难以拼出并发链路。
- 只有 traces:能看调用树,却未必知道模型上下文和恢复语义。
- 只有 metrics:知道 P95 变差,却不知道哪一次请求发生了什么。
- 只有 rollout:能恢复对话,却没有足够细的 timing 和内部决策证据。
- 只有前端 Event:能复现界面,却不一定能解释底层策略和网络行为。
成熟的设计不是把所有数据塞进一套系统,而是让它们共享稳定的标识符,可以互相跳转和交叉验证。
12.3 先建立坐标:trace ID 不等于业务身份
分布式 tracing 常用 trace_id 串起一次请求。但对 agent 来说,仅靠 trace_id 不够。
一个 thread 可以活几天,包含很多 turns;一个 turn 内可能有多次模型采样和几十次工具调用;一个子 agent 的工作可能由父 agent 触发,却在另一条异步链路上执行;进程重启后,旧 span 已经结束,但同一个 thread 还会 resume。
所以 Codex 同时需要两类坐标。
12.3.1 运行时坐标:这件事属于谁
| 标识符 | 回答的问题 |
|---|---|
thread_id |
发生在哪个 agent thread |
turn_id |
属于哪一次用户驱动的工作 |
root_turn_id |
整棵多 agent 工作由哪个根 turn 发起 |
parent_turn_id |
当前工作直接由哪个 turn 派生 |
item_id |
对应哪条消息、reasoning、工具调用或结果 |
12.3.2 调用坐标:这次交互是哪一次
| 标识符 | 回答的问题 |
|---|---|
call_id |
某次 tool call 与结果如何配对 |
inference_call_id |
属于哪一次模型采样 |
mcp_call_id |
MCP 请求、响应和工具语义如何关联 |
communication_id |
多 agent 消息从发送到接收如何配对 |
trace_id |
这次在线执行跨越了哪些进程和传输 |
可以把二者理解为:
- 业务坐标说明“这是故事中的哪一段”;
- trace 坐标说明“这一次运行经过了哪些机器和函数”。
flowchart LR
ROOT["root_turn_id"]
PARENT["parent_turn_id"]
TURN["turn_id"]
INF["inference_call_id"]
ITEM["item_id"]
CALL["call_id"]
TRACE["trace_id"]
ROOT --> PARENT --> TURN
TURN --> INF
INF --> ITEM
ITEM --> CALL
TRACE -. "贯穿本次在线执行" .-> TURN
TRACE -.-> INF
TRACE -.-> CALL
这套双坐标解决了一个常见误区:span tree 不等于 agent 的业务树。
异步任务可能脱离原来的调用栈;消息经过队列后,发送者和接收者不再是直接父子 span;resume 可能为同一个业务 turn 创建新的 trace;多 agent 的父子关系也不是普通函数调用关系。
因此,tracing 系统应该同时使用:
- parent-child span 表示真实的在线调用嵌套;
- link 或业务 ID 表示跨队列、跨进程、跨恢复的因果关系;
- rollout 中的 lineage 表示长期稳定的 agent 历史关系。
不要为了让 trace 图看起来像一棵整齐的树,就伪造并不存在的同步父子关系。
12.4 端到端 tracing:把一次 turn 串起来
一次来自网络前端的 turn,通常会跨过这些边界:
sequenceDiagram
participant FE as 前端程序
participant AS as App Server
participant Q as Submission Queue
participant T as Turn Task
participant L as LOOP
participant M as Model Service
participant P as Policy / Approval
participant X as Tool / MCP / Exec Server
FE->>AS: thread/turn 请求 + trace context
AS->>AS: app-server request span
AS->>Q: Submission + trace context
Q->>T: dispatch turn
T->>T: turn span
T->>L: run LOOP
L->>M: inference request + W3C trace context
M-->>L: streaming item / delta
L->>P: 工具决策与权限判断
P-->>L: allow / deny / ask user
L->>X: 执行工具 + trace context
X-->>L: tool result
L->>M: 回灌结果,继续采样
M-->>L: final response
L-->>T: turn complete
T-->>AS: Event stream
AS-->>FE: 通知与最终 item
W3C traceparent 和 tracestate 负责让上下游接续同一条 trace。它们可以穿过:
- 前端到 app-server 的请求;
- app-server 到内核的 submission;
- 内核到模型服务的 HTTP 请求;
- WebSocket 建连时的 metadata;
- harness 到独立 exec-server 或其他支持 tracing 的组件。
但“把 header 传过去”只是第一步。真正有诊断价值的 trace,还需要合理的 span 层级。
12.4.1 Span 应围绕语义阶段,而不是每个函数
典型层级可以是:
app-server request
└── submission dispatch
└── turn
├── prepare context
├── inference call #1
│ ├── send request
│ ├── receive stream
│ └── handle response items
├── tool call A
│ ├── wait for concurrency permit
│ ├── policy / approval
│ └── execute
├── tool call B
├── inference call #2
└── finalize turn
好的 span 有三个特点:
- 对应人能理解的阶段。 “模型流接收”比某个内部函数名更稳定。
- 有明确的开始和结束。 结束时才能计算 duration 和 outcome。
- 不会因为实现重构就完全失效。 可观测语义应比代码调用栈更稳定。
如果给每个小函数都建 span,会得到一张巨大但无法阅读的火焰图;如果只给整个 turn 建一个 span,又无法解释时间去哪了。观察粒度应该围绕生命周期边界和外部交互。
12.4.2 Event 与 span 各司其职
持续一段时间的动作适合 span:
- 一次模型采样;
- 一次工具执行;
- 一次 MCP 调用;
- 一次 compaction;
- 一次 hook;
- 一段等待或重试。
瞬时事实适合 event:
- 收到首个 token;
- 模型改道;
- policy 给出 deny;
- sandbox 拒绝;
- SSE 重连;
- token usage 结算;
- 某个 item 完成。
把所有事实都做成 span,会制造大量零时长节点;把长操作只记成开始、结束两条 log,又容易在并发环境中配错。span 表示区间,event 表示区间中的关键点。
12.5 Turn latency:总耗时只是答案的开头
用户最直接的感受是“这个 turn 很慢”。但一个总时长不能指导优化。Codex 会把 turn 的时间拆成若干阶段:
| 阶段 | 含义 |
|---|---|
before_first_sampling |
turn 开始后,到第一次模型采样前的准备时间 |
sampling |
所有模型采样和流式接收累计时间 |
compaction |
上下文压缩消耗的时间 |
between_sampling_overhead |
两次采样之间未归入工具和压缩的 harness 开销 |
tool_blocking |
LOOP 等待工具 future 完成的时间 |
after_last_sampling |
最后一次采样结束后,到 turn 完成前的收尾时间 |
同时还要记录:
- sampling request 数量;
- sampling retry 数量;
- TTFT(time to first token);
- TTFM(time to first meaningful output);
- 输入、输出、缓存和 reasoning token;
- 最终 outcome。
flowchart LR
START["Turn 开始"] --> PRE["采样前准备"]
PRE --> S1["Sampling 1"]
S1 --> MID["采样间阶段"]
MID --> S2["Sampling 2"]
S2 --> POST["采样后收尾"]
POST --> DONE["Turn 完成"]
MID -. "主要等待" .-> TOOL["Tool blocking"]
MID -. "harness 开销" .-> OVER["Between-sampling overhead"]
MID -. "按需触发" .-> COMP["Compaction"]
这张图最重要的含义是:
E2E 慢,不等于模型慢。
例如:
- TTFT 高:更可能是模型排队、网络或请求体过大;
- TTFT 正常但 sampling 长:可能是输出很多、reasoning 很长或流速慢;
- sampling 不长但 tool blocking 高:应继续检查工具、MCP、审批或并发闸门;
- compaction 占比高:说明上下文压力已经进入用户可感知路径;
- between-sampling overhead 高:可能是结果归一化、上下文重建、hook 或调度开销;
- sampling retry 增加:最终请求虽成功,但连接质量或服务限流正在恶化。
12.5.1 TTFT 与 TTFM 不是一回事
第一个流式字节可能只是协议心跳、response created 或空 delta。用户真正感知到“agent 开始回应”,往往要等到首段可显示文本、reasoning summary、计划或工具动作出现。
因此:
- TTFT 更适合观察传输和模型服务是否开始返回;
- TTFM 更接近用户感知到有效进展的时间。
如果 TTFT 很低、TTFM 很高,问题可能不在网络,而在模型长时间生成不可见 reasoning、前端过滤策略或 item 聚合逻辑。
12.5.2 当前时间分类仍然可能有空白
时间分解不是天然准确的。每一毫秒只能在埋点边界足够清楚时被正确归类。
例如,tool_blocking 表示 LOOP 等待工具 future 的时间。这个 future 内部可能包含:
- 等待并发 permit;
- 路由和参数校验;
- policy 判断;
- 等待用户审批;
- 选择 sandbox;
- 真正执行;
- 结果归一化。
所以 tool_blocking = 60s 不能直接推导出“命令执行了 60s”。还要进入单工具 timing 继续拆解。
同样,Human in the loop 的等待虽然是非常重要的用户体验指标,但如果系统没有统一的 approval_wait 或 human_wait 阶段,它就可能被包含在工具 handler 或其他等待中。指标缺少一个分类,本身也是可观测性结论,而不是应该被报表掩盖的问题。
12.6 工具链路:一次调用至少经过四段
第六章把工具调用拆成 spec、路由、policy、执行和结果回灌。可观测性也必须沿着同样的边界展开。
flowchart LR
A["模型生成 tool call"] --> B["解析与路由"]
B --> C["等待 runtime ready<br/>等待并发 permit"]
C --> D["policy / guardian<br/>审批 / sandbox"]
D --> E["真实执行<br/>local / MCP / dynamic"]
E --> F["结果归一化"]
F --> G["tool result 回灌"]
G --> H["下一次 sampling"]
一条有用的工具记录至少应该包含:
- 所属
thread_id、turn_id和trace_id; tool_name、call_id和执行类型;- 开始、结束、总耗时;
- 排队或 dispatch 耗时;
- handler 耗时;
- outcome 和错误类别;
- 是否经过审批、沙箱升级或 guardian;
- 结果大小,而不是默认记录全部结果正文。
12.6.1 不要把 handler duration 当成纯执行时间
工具 timing 常见三个值:
| 指标 | 通常覆盖什么 |
|---|---|
dispatch_duration |
runtime readiness、并发 permit 等进入 handler 前的等待 |
handler_duration |
路由后的完整处理,包括 policy、审批、sandbox 和真实执行 |
total_duration |
从调用被接收到结果返回的总时间 |
handler_duration 并不等于 shell process 的 wall time。它是“handler 拿到调用以后直到返回”的黑盒区间。
如果要回答“为什么命令慢”,还需要执行域自己的子指标:
- exec-server queue duration;
- process spawn duration;
- process running duration;
- stdout/stderr drain 与退出收尾;
- MCP request latency;
- dynamic tool 等待前端响应的时间。
只有逐层拆开,才能区分:
慢在 harness 排队
≠ 慢在等用户
≠ 慢在沙箱准备
≠ 慢在外部服务
≠ 慢在进程本身
12.6.2 错误要按阶段分类
“工具失败”也不是一个充分的错误类别。
| 阶段 | 示例 |
|---|---|
| 模型输出 | tool name 不存在、参数 JSON 不完整 |
| 路由 | catalog revision 已过期、runtime 不可用 |
| policy | 命令被规则拒绝、guardian 否决 |
| approval | 用户拒绝、请求超时、前端断开 |
| sandbox | OS 拒绝访问、沙箱后端启动失败 |
| transport | MCP 断连、请求超时、协议错误 |
| tool result | MCP 请求成功,但工具返回业务错误 |
| feedback | 结果过大、编码失败、无法进入上下文 |
尤其要区分 MCP 的请求失败和工具结果为 error:
- 请求失败表示没有正常完成协议交互;
- error result 表示 server 正常回复,只是业务动作失败。
二者的重试、告警和责任归属完全不同。
12.7 决策也要可观察:不仅记录“做了什么”
Agent 的问题经常不是某个函数报错,而是“为什么没有走预期路径”。
例如用户问:
为什么 agent 明明安装了数据库 skill,却没有调用数据库工具?
要回答这个问题,至少要检查一条完整的能力因果链:
flowchart LR
P["能力来源<br/>plugin / project / user"] --> D["Discovery<br/>是否被发现"]
D --> A["Activation<br/>是否启用与受信任"]
A --> V["Visibility<br/>本 step 是否对模型可见"]
V --> M["Model decision<br/>是否生成调用"]
M --> R["Routing<br/>是否绑定正确 runtime"]
R --> S["Safety<br/>policy / approval / sandbox"]
S --> X["Execution<br/>是否成功执行"]
X --> F["Feedback<br/>结果是否进入上下文"]
如果只记录最后的 tool call,模型没有调用时就什么也看不到。可观测系统还需要记录“未发生之前”的关键状态:
- catalog 是否发现能力;
- activation 是否禁用,以及禁用原因;
- step snapshot 中是否包含对应 tool spec;
- 是否因为 deferred exposure 尚未展开;
- 模型是否输出了相近但不存在的工具名;
- policy 是否在执行前拒绝;
- 结果是否因为过大或协议错误未能回灌。
这并不意味着要保存模型内部不可见的“真实思想”。系统只能观察协议上出现的 reasoning、item、调用和配置事实,不能从 telemetry 推断模型心里一定在想什么。
可解释性必须建立在可验证证据上。记录“模型看到了什么”和“模型输出了什么”,比事后编造一个看似合理的动机更可靠。
12.7.1 安全裁决需要结构化字段
第八章讨论过 policy、guardian 和网络管控。它们的可观测结果不能只是一行自然语言:
request denied
至少应结构化记录:
- decision:allow、deny、ask;
- rule 或 policy 来源;
- risk 分类;
- authorization 类型;
- 是否发生 sandbox escalation;
- 网络目标的域名、协议和端口;
- 耗时和失败阶段。
这样既能聚合“最近 deny 是否暴增”,也能在单次 trace 中解释“谁做了最终决定”。
网络审计尤其应遵循最小披露:通常记录域名、协议、端口和裁决已经足够,不必保存完整 URL、query 或认证信息。
12.7.2 Hook 要区分控制结果和旁路结果
同步 hook 可能阻止流程,异步 hook 通常只做通知或上报。两者在 telemetry 中必须区分:
- hook 名称、来源和版本;
- 触发生命周期点;
- sync 或 async;
- duration;
- allow、block、modify、error 或 timeout;
- 是否改变了最终控制流。
否则一个异步上报失败可能被误读成任务失败,或者一个真正阻断执行的 hook 只留下一条不起眼的 warning。
12.8 多 agent:调用树之外还有一棵协作树
第七章中,多 agent 通过 spawn、消息和结果交付形成一棵 thread tree。可观测性必须同时面对两棵树:
- 执行树:span 的父子关系,表示这次在线调用如何展开;
- 协作树:root turn、parent turn、thread lineage 和 communication,表示任务如何委派。
它们经常重合,但并不等价。
flowchart TB
RT["Root turn T0"]
A["Agent A / thread A"]
B["Agent B / thread B"]
C["Agent C / thread C"]
M1["message c1"]
M2["message c2"]
RT --> A
RT --> B
A --> C
B --> M1 --> A
C --> M2 --> A
subgraph Traces["可能分散在多条在线 trace 中"]
TR1["trace x:spawn A/B"]
TR2["trace y:B 完成后投递"]
TR3["trace z:resume A 并消费结果"]
end
如果只看 trace_id,可能看到三段互不相连的执行;如果只看 thread_id,又无法知道一条消息属于哪个根任务。
多 agent 观测至少需要:
- root、parent、child 的 thread 与 turn 坐标;
- spawn 请求与 agent 启动结果;
- communication ID;
- 发送、排队、投递、消费四个时刻;
- 子 agent 的最终状态和结果 item;
- 并发 permit 的等待时间;
- 父 turn 是否在等待、取消或已经结束。
12.8.1 “子 agent 很慢”仍然需要拆解
父 agent 等待子 agent 30 秒,可能是:
- 子 agent 排队等并发名额;
- 子 agent 自己在等模型;
- 子 agent 在跑工具;
- 结果已产生,但消息尚未投递;
- 消息已投递,但父 agent 尚未进入下一个 step;
- 父 turn 已中断,结果成为迟到消息。
因此,多 agent latency 不能只记录 spawn → result。它应至少区分:
spawn queue
→ child active
→ child result ready
→ message delivered
→ parent consumed
这和第二章的 Event 原则、第五章的 LOOP 边界以及第十章的恢复语义是同一个问题:事实产生、事实送达、事实被消费,是三个不同时间点。
12.9 Metrics:用聚合信号发现系统性问题
Trace 适合解释一次请求,metrics 适合回答:
- 最近一小时 turn 错误率是否升高?
- P95 TTFT 是否只在某个模型上恶化?
- MCP timeout 是否集中于某个 server?
- 工具队列是否接近容量?
- compaction 是否越来越频繁?
- guardian 的 deny 比例是否异常?
- SQLite 初始化或 fallback 是否增加?
12.9.1 三种基础指标
| 类型 | 含义 | Agent 场景 |
|---|---|---|
| Counter | 只增加的累计次数 | turn、请求、错误、重试、tool call |
| Gauge | 某一时刻的当前值 | 活跃 turn、队列长度、连接数、运行中进程 |
| Histogram | 一组数值的分布 | TTFT、turn duration、tool duration、token |
平均值常常会掩盖 agent 的长尾。比如九个 turn 用 5 秒,一个 turn 因审批等待用了 10 分钟,平均值看起来只有约 65 秒,却同时误导了两类用户。
延迟应重点看分位数,并按有意义的阶段拆分:
- P50 反映常见体验;
- P95/P99 揭示长尾;
- max 帮助发现极端挂起,但不适合单独告警。
12.9.2 指标应围绕用户体验和容量
一套实用指标通常覆盖:
请求与模型
- API 请求次数、状态和 duration;
- SSE/WebSocket 连接、事件和重连;
- TTFT、TTFM;
- token usage;
- sampling retries。
Turn 与 LOOP
- turn E2E duration;
- 各 timing phase;
- 每个 turn 的 sampling 次数;
- compaction 次数和耗时;
- complete、interrupt、error 等 outcome。
工具与扩展
- tool、MCP、hook、plugin、skill 的调用或激活结果;
- 排队、handler、执行 duration;
- timeout、deny、approval 和 sandbox outcome;
- catalog 刷新和启动失败。
运行时资源
- submission queue;
- tool concurrency;
- child agent permit;
- exec-server queue、process、connection;
- telemetry 自身队列和丢弃数;
- 持久化初始化、写入和 fallback。
12.9.3 Tag 基数是一项架构约束
Metrics 系统最怕高基数标签。把 thread_id、turn_id、call_id、完整错误信息、文件路径或用户 prompt 放进 tag,会让时间序列数量无限增长。
适合作为 metric tag 的字段通常是:
- model family;
- tool category;
- outcome;
- error class;
- transport;
- sandbox mode;
- approval decision;
- feature name。
不适合作为 tag 的字段通常是:
- 任意 ID;
- URL 和路径;
- 用户名和项目名;
- 原始异常文本;
- 工具参数和输出。
单次 ID 应进入 trace 或 log,聚合维度才进入 metrics。所有 tag 还应经过白名单、长度限制和 sanitize。
Metrics 负责“分组”,traces 负责“定位”。试图让 metrics 同时完成定位,会把监控系统变成另一个高成本数据库。
12.10 三种 replay:恢复、重建界面与诊断不是一回事
“回放”在 agent 系统里经常指三种完全不同的机制。
| Replay | 目的 | 主要输入 | 结果 |
|---|---|---|---|
| Rollout replay | 恢复 Session 和模型历史 | canonical rollout item | 可继续工作的运行时状态 |
| UI event replay | 重建前端看到的 turn/item | 持久化 item 和状态投影 | 对话、工具和状态界面 |
| Rollout-trace replay | 离线分析因果链 | 更细粒度的原始诊断事件与 payload | 语义关系图和调查证据 |
12.10.1 Rollout replay:为了继续工作
第十章已经说明,rollout 保存的是恢复所需的 canonical facts:
- 用户和 assistant item;
- 工具调用与结果;
- turn 边界;
- compaction;
- 配置和世界状态相关事实;
- 多 agent lineage。
它的兼容性要求高,生命周期长,但不会保存所有内部细节。它不需要记录每个函数耗时,也不应该默认保存每个流式 delta。
Rollout replay 的问题是:
“恢复后,模型应该看到怎样的有效历史?”
12.10.2 UI event replay:为了重建用户所见
前端关心的是:
- 哪个 turn 开始或结束;
- 哪个 item 已完成;
- 命令和补丁显示什么状态;
- 哪些审批仍需处理;
- token 与 warning 如何展示。
它可以从 canonical item 与运行时状态生成一份 UI projection。瞬时 delta 丢失后,完整 item 仍能恢复最终界面;但“当时每个字以什么速度出现”通常不值得长期保存。
UI event replay 的问题是:
“重新连接以后,前端应该显示什么?”
12.10.3 Rollout-trace replay:为了理解过去
诊断 replay 需要比 canonical rollout 更细的证据,例如:
- 每次 inference 的请求与响应关系;
- 模型实际接收的 conversation;
- tool call、MCP call 与返回 item 的关联;
- terminal session 和 operation;
- compaction 前后的语义关系;
- 多 agent 消息从来源 item 到目标 conversation 的投递;
- 必要时引用独立保存的大 payload。
它遵循一个关键原则:
Observe first, interpret later:先忠实记录观察到的原始事实,再离线归约成语义图。
flowchart LR
R["Raw events<br/>带 seq 的追加事实"] --> RED["Reducer<br/>严格按 seq 归约"]
PAY["Payload files<br/>请求、响应、输出"] --> RED
RED --> G["Semantic graph"]
G --> TH["threads / turns"]
G --> IN["inference calls"]
G --> TO["tool / MCP calls"]
G --> CO["conversation items"]
G --> CP["compactions"]
G --> ED["interaction edges"]
原始事件按单调递增的 seq 排序,而不是只相信 wall clock。原因很简单:并发任务的系统时间可能相同,跨线程写入也可能出现微小乱序;reducer 需要一个稳定顺序才能得到确定结果。
大 payload 通常与事件信封分开保存,并遵循:
先写 payload
→ 再追加引用它的 event
→ 每条 event 及时 flush
这样 event 一旦可见,它引用的 payload 就已经存在。反过来,即使进程在两步之间崩溃,最多留下一个未被引用的 payload,不会留下指向不存在内容的正式事件。
12.10.4 为什么不能让 rollout-trace 取代 rollout
诊断 trace 更详细,看起来似乎更适合作为“终极事实源”,但这样做会破坏几个重要边界:
- 它可能包含敏感 prompt、response、路径、终端输出和工具参数;
- 它是显式开启、本地保存的诊断机制,不保证始终存在;
- 内部事件结构可以比公共恢复格式更快演进;
- 数据量更大,不适合作为每个 Session 的长期负担;
- 诊断证据描述“运行时观察到什么”,不一定等于“恢复时应向模型呈现什么”。
Canonical rollout 追求长期兼容和安全恢复;rollout-trace 追求故障现场的解释力。二者目标不同,不应合并。
12.11 Reducer:从原始事件还原语义,而不是还原调用栈
Raw trace 只是一串按序到达的证据。真正有价值的是 reducer 构造出的语义图。
为什么需要 reducer?因为并发和流式协议会让事件以“不方便理解”的顺序出现:
- 某个工具结果先被观察到,来源 item 稍后才完成;
- MCP transport ID 与模型 tool call ID 属于不同命名空间;
- 子 agent 先完成,父 agent 过一会儿才消费消息;
- runtime 为方便前端产生了一条 output,但模型并没有看到它;
- compaction 改变了后续 conversation,却没有改写旧事实。
Reducer 的职责是:
- 严格按
seq消费原始事件; - 建立 thread、turn、inference、item、tool 和通信实体;
- 用稳定 ID 连接实体;
- 暂存尚未找到来源的 pending 关系;
- 在后续证据到达时补全关联;
- 区分模型真正看到的 conversation 与 runtime 的便利输出;
- 保留无法解释的原始证据,而不是静默丢弃。
flowchart TB
E1["事件:tool result 到达"]
E2["事件:来源 item 完成"]
P["Pending association"]
C["建立 call → result → item 关系"]
E1 -->|"暂时缺少来源"| P
E2 --> P
P --> C
这是一种比在线日志更稳健的设计。在线路径不必为了立即得到完美语义而阻塞主流程;离线 reducer 可以接受“证据迟到”,只要 ID 和顺序足够稳定。
但 reducer 也不能随意猜测。如果找不到唯一关联,应把关系标为 unresolved,并保留原始引用。错误的确定答案,比明确的不确定更危险。
12.12 隐私:观察得越深,越要限制谁能看到
Agent telemetry 天然接近敏感数据:
- 用户 prompt 可能包含源代码、商业信息和个人数据;
- 工具参数可能包含路径、查询、收件人和命令;
- 工具输出可能包含密钥、日志、数据库记录;
- reasoning 和 assistant response 可能复述上下文;
- 网络 URL 可能包含 query token;
- rollout-trace 可能重建整个工作过程。
所以可观测性不能只有“开或关”,而需要分层。
12.12.1 Trace-safe metadata
默认 trace 应优先记录结构和规模,而不是正文:
- prompt 长度;
- 输入 item 类型和数量;
- 工具名称与参数长度;
- 输出字节数和行数;
- error class;
- duration;
- policy decision;
- token usage。
例如,记录“用户输入 2,431 字符,包含文本和一张图片”,通常足以分析请求大小和模型延迟,不必上传 prompt 正文。
12.12.2 Logs 比 traces 更可能包含细节
为了本地故障排查,logs 可能记录更详细的工具参数、错误和输出。Log exporter 因此应被视为比 trace exporter 更高风险的出口:
- 默认关闭远程导出;
- 明确区分安全 event 与详细 log;
- 用户 prompt 默认 redact;
- 限制字段长度;
- 对认证 header、WebSocket frame 和已知敏感字段做过滤;
- 配置导出目标时清楚说明数据边界。
“日志只给内部人看”不是隐私策略。只要数据离开本机,就应该按外发数据处理。
12.12.3 Rollout-trace 是本地、显式开启的诊断证据包
为了回答“模型当时究竟看到了什么”,rollout-trace 可能不得不保存:
- 完整 prompt 和 response;
- 工具参数与结果;
- 终端输出;
- 工作区路径;
- 多 agent 消息;
- 原始协议 payload。
这种能力不能默认上传,也不应在用户不知情时长期启用。合理边界是:
- 显式 opt-in;
- 本地存储;
- 明确生命周期和清理方式;
- 分享前由用户检查;
- 诊断失败不影响主任务;
- 不把它误当成普通匿名 telemetry。
12.12.4 Feedback 是一次有 consent 的证据提交
用户主动提交反馈时,可以附带诊断日志或其他材料。但这仍然需要:
- 前端明确展示将上传什么;
- 用户主动确认;
- 过滤高风险、高流量且不必要的原始 frame;
- 对附件大小设上限;
- 区分产品反馈与自动 telemetry。
Consent 不是一个永久开关,而是对这次具体数据提交的授权。
12.13 可观测系统自身也会失败
Telemetry 是辅助系统,不能成为 agent 主流程的新单点故障。
合理的失败语义是:
- exporter 初始化失败:记录本地 warning,agent 仍可启动;
- 单次 event 发送失败:吸收错误,不改变 tool 或 turn outcome;
- metrics backend 不可用:丢失指标,但不阻塞 LOOP;
- analytics 队列满:有界丢弃并计数,不能无限占用内存;
- rollout-trace 写入失败:停止或降级诊断记录,不让任务失败;
- shutdown flush 超时:在固定时间预算后退出;
- reducer 遇到未知事件:保留原始证据并继续处理可理解部分。
flowchart LR
MAIN["Agent 主流程"] --> BUF["有界 telemetry 队列"]
BUF --> EXP["Exporter"]
EXP --> BACK["外部 backend"]
BUF -. "队列满:丢弃 + 计数" .-> DROP["Dropped telemetry"]
EXP -. "失败:warning + 重试预算" .-> DEG["降级"]
BACK -. "不可用" .-> DEG
DEG -. "不得反向阻塞" .-> MAIN
这里有一个看似矛盾的要求:
- telemetry 不能影响主任务;
- telemetry 丢失又不能悄无声息。
解决方式不是“永不丢失”,而是有界、可见的失败:
- 队列必须有容量上限;
- 丢弃数量本身是 metric;
- 本地保留简短 warning;
- shutdown 有时间预算;
- 高价值 canonical rollout 与普通 telemetry 使用不同可靠性承诺。
Canonical facts 需要更强的持久化语义;可重建的 metrics 和 traces 可以 best-effort。把所有信号都提升到事务级可靠,会让观察系统反过来绑架主流程。
12.14 一次完整调查:为什么这个 turn 又慢又没用上工具
假设用户反馈:
我让 agent 查询线上告警并修复配置。它停了两分钟,最后只给了文字建议,明明已经安装了运维 plugin。
一个有效调查不应该从搜索某句错误日志开始,而应按因果链逐层缩小范围。
第一步:确认业务坐标和用户所见
从前端 Event 找到:
thread_id和turn_id;- turn 开始、结束和最终 outcome;
- 用户看到了哪些 item;
- 是否出现工具开始、审批或 warning;
- 最终 assistant item 是正常完成还是中断后的残留。
此时确认:前端没有漏渲染工具事件,turn 确实只产生了文字回复。
第二步:用 metrics 判断是否为系统性问题
检查同一时间窗口:
- 模型 TTFT 是否普遍升高;
- MCP server 的 timeout 是否增加;
- tool call 数是否下降;
- plugin activation failure 是否增加;
- sampling retry 是否异常。
结果发现整体 TTFT 正常,只有这个 turn 很慢。问题更像单次上下文、等待或决策链,而不是模型服务整体退化。
第三步:在 trace 中拆解时间
Turn profile 显示:
E2E 121s
before sampling 1s
sampling 14s
tool blocking 105s
finalize 1s
这已经排除了“模型推理用了两分钟”。继续展开 tool span,发现 105 秒主要在等待 dynamic tool 的前端响应,最后以 timeout 结束。
用户没有看到工具,是因为这次 dynamic tool 只产生了一个等待中的内部调用,前端连接切换后没有成功展示并应答。
第四步:检查能力因果链
运维 plugin 的记录显示:
discovered: yes
enabled: yes
skill visible: yes
MCP server required: yes
MCP activation: failed
reason: authentication unavailable
模型看到了运维 skill 的方法说明,却没有看到实际查询告警的 MCP tool。它先尝试让前端 dynamic tool 获取当前环境,超时后退化为文字建议。
因此,“安装了 plugin”与“本 step 能调用 MCP tool”不是同一件事。第十一章的 discovery、activation、visibility、execution 四道门,在这里通过观测证据完整显现。
第五步:用 rollout-trace 验证模型实际输入
离线 replay 显示:
- 本轮 conversation 中包含 skill 摘要;
- tool snapshot 中没有告警查询工具;
- dynamic tool spec 存在;
- 模型生成了 dynamic tool call;
- timeout result 已回灌;
- 第二次 sampling 基于这个失败结果生成了文字建议。
现在可以给出有证据的结论:
慢:主要慢在等待前端 dynamic tool,不是模型。
没用 MCP:server 因认证失败未激活,所以本 step 不可见。
只给建议:dynamic tool timeout 已回灌,模型在缺少可执行能力时完成了降级回答。
进一步的根因修复也很明确:
- 修复 MCP 认证和 required server 的启动反馈;
- 让前端连接切换时显式失败所有悬空 dynamic tool 请求;
- 单独记录
frontend_tool_wait,不要只归入笼统的 tool blocking; - required capability 不可用时应尽早、响亮地告知用户,而不是让模型带着残缺能力继续。
这就是可观测性的价值:不是给现象贴标签,而是把性能、能力、安全和协议证据拼成同一条因果链。
12.15 常见失败模式
| 失败模式 | 表面现象 | 根因 | 更好的做法 |
|---|---|---|---|
| 只记录 turn 总耗时 | 知道慢,却不知道慢在哪里 | 模型、工具、人工和调度混在一起 | 按 sampling、tool、compaction、human wait 等阶段拆分 |
| 把 handler time 当执行时间 | 错怪 shell 或 MCP 性能 | handler 还包含 policy、审批和 sandbox | 继续分解 queue、approval、sandbox、process |
| 只靠 trace ID 关联 | resume 和多 agent 链路断裂 | trace 是在线执行身份,不是长期业务身份 | 同时记录 thread、turn、item、call 和 lineage |
| 每个函数都建 span | trace 巨大、无法阅读、成本失控 | 把调用栈当成业务语义 | 围绕生命周期阶段和外部交互建 span |
| 只记录发生的工具调用 | 无法解释为什么没调用某工具 | 缺少 discovery、activation 和 visibility 证据 | 记录能力从来源到回灌的完整链 |
| 把所有错误归为 tool error | 无法决定由谁修复、是否重试 | 混淆模型输出、policy、transport 和业务结果 | 按失败阶段和责任域分类 |
| 把 prompt 放进 metric tag | 指标基数爆炸并泄露数据 | 混淆聚合维度与单次证据 | 低基数 tag;详细内容只进受控 log/trace |
| 默认上传详细 trace | 源码、路径和工具输出外泄 | 诊断深度没有对应隐私边界 | trace-safe 默认;详细 evidence 本地 opt-in |
| 用 rollout-trace 做恢复源 | 无 trace 时无法恢复,格式难兼容 | 混淆诊断证据与 canonical fact | rollout 负责恢复,rollout-trace 负责解释 |
| 用 wall clock 排 replay | 并发事件顺序不稳定 | 时钟不能提供可靠全序 | writer 分配单调 seq,reducer 按 seq 处理 |
| Reducer 猜测缺失关联 | 报告看似完整,结论却错误 | 把不确定性隐藏了 | pending、unresolved 和原始引用显式保留 |
| Telemetry 队列无界 | 后端故障拖垮 agent 内存 | 观察系统没有资源预算 | 有界队列、丢弃计数、固定 flush 预算 |
| Telemetry 失败中断主任务 | 监控故障变成用户故障 | 辅助系统侵入控制流 | best-effort 隔离,canonical 持久化另行保证 |
| 只看机器时间,不看人等多久 | 审批型 turn 被误判为性能退化 | Human in the loop 没有独立阶段 | 显式记录 ask、display、answer 和 timeout |
| 把产品 analytics 当 tracing | 能看使用量,不能还原因 | 产品事实流缺少单次因果关系 | analytics、telemetry、feedback 分别建模 |
这些失败背后有一个共同问题:系统记录了结果,却没有记录结果成立所依赖的边界和关系。
12.16 更深一层:可观测性是运行时模型的可验证投影
回看前十一章,会发现可观测性并不是最后才加上的横切功能。它是所有设计边界是否真实存在的一次验收。
- 第一章定义了 thread、turn、task、step;可观测系统必须能按这些层级归因。
- 第二章区分 Op、Event、item 与 delta;可观测系统必须知道哪些是权威事实,哪些只是实时表现。
- 第三章区分请求构造、传输、流处理与重试;模型 latency 必须沿这些阶段拆开。
- 第四章强调历史增量、窗口和片段注入;诊断 replay 必须能回答模型实际看到了什么。
- 第五章要求 LOOP 在边界上推进;span 和 timing 也应围绕这些边界建立。
- 第六章拆开 spec、policy、执行与回灌;工具 trace 不能只包住最终进程。
- 第七章定义多 agent lineage 与通信;trace tree 之外必须保留协作树。
- 第八章把安全做成结构化裁决;审计必须记录决策来源和结果。
- 第九章把等待人类视为显式状态;latency 也应把人的时间独立出来。
- 第十章区分 canonical fact 与 runtime state;rollout 和诊断 replay 因而不能混用。
- 第十一章区分 discovery、activation、visibility 与 execution;“为何能力未生效”必须沿这四道门调查。
如果某个模块无法被观察,往往意味着它的边界还没有被真正建模。
例如:
- 无法区分工具排队和执行,说明调度边界不清;
- 无法解释谁拒绝了命令,说明 policy 决策没有统一结构;
- 无法关联子 agent 结果,说明通信缺少稳定身份;
- 无法重建模型输入,说明上下文构造没有留下证据;
- 无法分开恢复和诊断,说明 canonical state 的定义还不够清楚。
因此,可观测性并不是“给代码加日志”,而是把运行时模型投影成一套可验证证据。
12.16.1 从故障定位走向行为解释
传统 observability 通常停在:
哪个组件失败?
Agent harness 还要继续追问:
当时有哪些能力?
模型看到了哪些事实?
哪个主体作了哪项决定?
决定经过哪些安全边界?
外部世界发生了什么?
结果如何影响下一次采样?
这不是要求系统解释模型的全部内部机制,而是要求 harness 对自己掌握的部分负责。模型为什么选择某个词,可能无法完全解释;但某个工具是否可见、某条 policy 是否命中、某个结果是否回灌,必须是可证明的工程事实。
12.16.2 可观测性也是一份成本预算
观察越细,成本越高:
- 更多 span 和 event 增加 CPU 与序列化开销;
- 更长保留期增加存储成本;
- 更详细 payload 增加隐私风险;
- 更高基数增加 metrics 成本;
- 同步写入增加关键路径 latency;
- 完整 replay 增加格式兼容负担。
所以每一类信号都应该回答四个问题:
- 它用于实时告警、单次定位、历史恢复,还是深度诊断?
- 它是否需要完整内容,还是元数据已经足够?
- 它需要保留多久,谁可以读取?
- 丢失它会影响用户任务,还是只降低诊断能力?
这四个问题决定了信号应该进入 metric、trace、log、rollout 还是本地 rollout-trace。
12.16.3 最好的诊断路径是从聚合到证据
一个高效的调查顺序通常是:
Metrics 发现异常范围
→ Event 确认用户所见
→ Trace 定位阶段与组件
→ Logs 查看局部错误
→ Replay 验证历史语义和模型输入
不必每次都走到最后。系统性延迟可能在 metrics 和 traces 阶段就能定位;只有涉及“为什么做出这个决定”“恢复前后发生了什么”时,才需要更昂贵的 replay 证据。
这是一种 progressive disclosure:不仅上下文和工具按需展开,诊断证据也应按需展开。
12.17 小结:可观测性的七条设计原则
-
观察完整因果链,不只观察最终动作。 从能力来源、step visibility、模型输出、policy、审批、执行到结果回灌,每个边界都应留下可关联的事实。没有发生的调用,也要能从上游状态解释原因。
-
业务坐标与 trace 坐标同时存在。
trace_id描述一次在线执行,thread、turn、item、call 和 lineage 描述长期业务关系。异步队列、多 agent 与 resume 需要 link 和稳定 ID,不能硬塞进一棵 span tree。 -
总耗时必须按责任域拆开。 E2E latency 要区分 sampling、tool、compaction、调度、人工等待和收尾;工具还要继续区分 queue、policy、approval、sandbox 与真实执行。无法分类的时间应该显式暴露,而不是藏进“其他”。
-
Logs、traces、metrics、Event 与 replay 各司其职。 Metrics 找趋势,traces 找路径,logs 看局部细节,Event 重建用户所见,replay 解释历史语义。它们通过稳定 ID 互相连接,但不强行合并成一种万能数据。
-
恢复 replay 与诊断 replay 分离。 Rollout 保存长期兼容的 canonical facts;UI replay 重建展示;rollout-trace 以本地、显式开启的方式保存更详细证据。Replay 历史永远不等于 re-execute 副作用。
-
隐私、基数和保留期都是架构边界。 默认 trace 记录长度、类型、耗时和结果类别;正文、路径、参数和输出进入更受控的通道。Metrics 只使用低基数 tag,详细诊断数据必须 opt-in、有上限、可清理。
-
可观测系统必须 best-effort,但失败要可见。 Exporter、analytics 和诊断 writer 不能阻塞或改变 agent outcome;队列、重试和 shutdown 都有预算。Canonical 持久化获得更强保证,辅助 telemetry 可以降级,但丢弃和缺口必须被记录。
留给读者思考的几个问题:
- 如果一个 turn 同时等待两个并行工具和一次人工审批,
tool_blocking应按 wall time、各任务耗时之和,还是关键路径计算?哪一种最能解释用户感知,哪一种最适合容量规划? - 模型没有调用某个工具时,应该保存整份 step tool snapshot,还是只保存 catalog revision 与内容摘要?二者在可解释性、存储和隐私上如何权衡?
- Rollout-trace 能重建模型所见,但也可能包含最敏感的数据。怎样设计自动脱敏,才不会在删除秘密的同时破坏 tool call 与结果之间的语义关联?
- 多 agent 的 communication 跨越多个 trace 和进程。应该用 span link、独立 message trace,还是只依赖业务 ID?不同 backend 对这些关系的查询能力是否会反过来限制协议设计?
- Telemetry 必须 best-effort,canonical rollout 又必须可靠。如果二者对同一事件给出不同结果,调查工具应该以谁为准,怎样向用户表达“不完整证据”?
- TTFM 比 TTFT 更接近用户感知,但“meaningful”会随前端而变化:文本、reasoning、plan、工具开始,哪一个才算首个有效反馈?这个定义应该由 harness 统一,还是由前端分别计算?
- 当可观测系统已经能完整还原模型输入、工具行为和结果回灌时,我们能解释的是 harness 的因果链,而不是模型全部内部动机。产品界面应该怎样清楚表达这条能力边界,避免把证据推断包装成确定解释?
至此,我们已经从运行时层级、协议、模型、上下文、LOOP、工具、多 agent、安全、Human in the loop、持久化、扩展一直走到可观测性。十二个模块最终汇合成同一个判断标准:一个生产级 agent harness 不仅要能行动,还要能约束行动、恢复行动,并对自己的行动给出可验证的解释。