跳转到内容

第 28 课:可观测性与 Trace Replay

设计 token、cost、tool latency、failure taxonomy、session replay、event timeline 和命令证据,让 agent 失败可定位、过程可回放。

Trace Replay 的目标不是“保存更多日志”,而是让一次 agent 运行可以被重新理解:它什么时候开始、用了哪个模型、消耗多少 token 和 cost、调用了哪些工具、每个工具耗时多久、失败属于哪一类、最终证据是否支撑结论。

可观测性解决实时诊断,replay 解决事后复盘。前者回答“现在卡在哪里”,后者回答“上一次为什么失败”。生产级 coding agent 必须把 event timeline、命令证据、session transcript、工具结果、成本统计和失败分类放在同一条 trace 上,否则你只能靠猜测排查。

阶段二已经做过 trace.jsonl,阶段三读过 agent loop,阶段四做过长任务和 worktree。现在的问题是:

当一个后台 agent 任务失败后,你如何在不重新跑模型的情况下解释它为什么失败?

这节课用 /Users/ryanchen/codespace/pi-agent-course-lab/src/18-trace-replay.ts 做起点,把“事件数量统计”扩展成“可回放 timeline 和失败报告”。

一条 trace 可以看成三种数据的对齐:

第一是事件时间线。agent_startturn_startmessage_updatetool_execution_starttool_execution_endmessage_endagent_end 组成运行骨架。

第二是资源账本。assistant message 的 usage 提供 token 和 cost;工具事件的开始结束时间提供 latency;session stats 提供累计消息、工具调用和上下文使用情况。

第三是证据链。命令、exit code、stdout/stderr 摘要、diff 文件、测试结果和错误消息证明 agent 是否真的完成任务。

Trace Replay 不是重新执行危险命令,而是重放这些记录:按时间排序展示 agent 看到了什么、做了什么、证据是什么、失败在哪里出现。

starter 里的 TraceEvent 只有 attypetoolNameisError。这是好的第一步,但还不够定位问题。建议扩展成:

type TraceEvent = {
at: string;
type: string;
turn?: number;
messageId?: string;
toolCallId?: string;
toolName?: string;
command?: string;
exitCode?: number;
usage?: { input: number; output: number; cost: number };
isError?: boolean;
errorClass?: string;
};

字段设计的原则是:timeline 能排序,tool latency 能计算,token/cost 能汇总,failure taxonomy 能过滤。

不要把 trace 当作无结构日志。把每一行都归一化成事件,再按 at 排序:

00.000 agent_start
00.120 turn_start #1
01.430 message_update assistant text
02.010 tool_execution_start read
02.060 tool_execution_end read ok 50ms
03.500 tool_execution_start bash npm test
05.800 tool_execution_end bash exit 1 2300ms
05.900 agent_end

有了 timeline,排查顺序就稳定了:先看失败前最后一个事件,再看它所属 turn,再看对应工具或 assistant message。

token 和 cost 通常来自 assistant message 的 usage。Pi 的 AgentSession.getSessionStats() 会聚合 input、output、cacheRead、cacheWrite 和 cost。工具 latency 则由同一个 toolCallId 的 start/end 时间差计算。

注意:token/cost 是诊断指标,不等于任务质量。低成本失败不值得庆祝,高成本成功也可能不可接受。评测报告里要把它们放在“质量结果”旁边,而不是混成一个分数。

失败分类要面向行动。推荐从这几类开始:

  • model_error:provider 返回错误或 stopReason 为 error。
  • tool_error:工具执行失败或参数校验失败。
  • command_failed:bash 命令非零退出。
  • timeout:工具或任务超过限制。
  • permission_blocked:权限策略阻断。
  • context_overflow:上下文太长或压缩失败。
  • false_success:没有证据却宣称完成。

分类不是为了贴标签,而是为了决定下一步:改 prompt、改工具、改权限、改 fixture,还是改 runtime。

Replay 是读取旧 trace,不重新调用模型,也不重新执行命令。它适合 code review、事故复盘、成本分析和失败归档。

Rerun 是把同一个 task 在同一个 fixture 上重新执行。它适合验证修复是否有效。

如果 agent trace replay <id> 会重新跑 npm test 或写文件,它就不再是 replay,而是 rerun。产品命令必须把这两种行为分开。

coding agent 的最终报告必须能回答:

  • 哪些命令运行了。
  • exit code 是多少。
  • 关键输出是什么。
  • 这些命令是否覆盖了任务成功标准。

命令证据最好单独结构化保存,不要只混在 assistant 文本里。这样 code review agent、benchmark runner 和 trace viewer 都能复用。

本课 starter 在:

Terminal window
cd /Users/ryanchen/codespace/pi-agent-course-lab
sed -n '1,220p' src/18-trace-replay.ts

它的 summarizeTrace() 已经能输出 startedAtendedAteventCounttoolEventCountfailureCount。下一步可以加两个能力:

  1. toolCallIdtoolName 计算工具耗时。
  2. 根据 isErrorexitCodetype 推断 failure taxonomy。

课堂上先从 starter 的输入开始:

const events = readJsonl<TraceEvent>("trace.jsonl");
const summary = summarizeTrace(events);

这时 events 是原始记录,summary 只是计数。为了做 replay,需要加一个中间结构:

type TimelineItem = {
at: string;
label: string;
severity: "info" | "warning" | "error";
evidence?: string;
};
function buildTimeline(events: TraceEvent[]): TimelineItem[] {
return events
.toSorted((a, b) => a.at.localeCompare(b.at))
.map((event) => ({
at: event.at,
label: formatEvent(event),
severity: event.isError ? "error" : "info",
evidence: event.command,
}));
}

变量流是:trace.jsonl 进入 eventsevents 进入 summarytimeline,最终 viewer 只读 summary + timeline。viewer 不需要知道 agent loop 内部怎么跑,也不应该重新调用工具。

然后加入 latency 聚合:

function toolLatency(events: TraceEvent[]) {
const starts = new Map<string, TraceEvent>();
const rows = [];
for (const event of events) {
const key = event.toolCallId ?? `${event.toolName}:${event.at}`;
if (event.type === "tool_execution_start") starts.set(key, event);
if (event.type === "tool_execution_end") {
const start = starts.get(key);
rows.push({
toolName: event.toolName,
ms: start ? Date.parse(event.at) - Date.parse(start.at) : undefined,
isError: event.isError,
});
}
}
return rows;
}

这里的控制点是 key。真实系统应该优先使用 toolCallId,因为同一个 turn 里可能有多个 bash 或多个 read。只有缺字段时才退化到 toolName + at

最后做失败分类:

function classify(event: TraceEvent): string | undefined {
if (event.type === "tool_execution_end" && event.exitCode && event.exitCode !== 0) {
return "command_failed";
}
if (event.type === "tool_execution_end" && event.isError) {
return "tool_error";
}
if (event.type === "message_end" && event.isError) {
return "model_error";
}
}

这段推演让学生看到:可观测性不是多打印几行,而是把事件流转成可计算的数据结构。

  • /Users/ryanchen/codespace/pi-agent-course-lab/src/18-trace-replay.ts:课程 lab 的 trace replay starter,包含 TraceEventsummarizeTrace() 的最小实现。
  • /Users/ryanchen/codespace/external-sources/pi/packages/agent/src/agent-loop.ts at 61babc2:发出 agent、turn、message 和 tool execution 事件,是 event timeline 的主要来源。
  • /Users/ryanchen/codespace/external-sources/pi/packages/agent/src/types.ts at 61babc2:定义 AgentLoopConfig、tool hooks、tool execution mode 和事件相关类型,是理解事件语义的入口。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.ts at 61babc2AgentSessionEvent 扩展了低层事件,并提供 getSessionStats()getContextUsage()、retry、compaction 等 session 级诊断信息。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/agent-session-stats.test.ts at 61babc2:验证 session stats、token 和 context usage 的测试,可作为 token/cost 报告的测试参考。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/suite/regressions/3982-message-end-cost-override.test.ts at 61babc2:证明 message_end 上 finalized assistant usage cost 可以被扩展修正,是 cost 观测链路的具体回归测试。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/timings.ts at 61babc2:启动耗时 instrumentation,展示 Pi 中最小 timing hook 的实现方式。
  • /Users/ryanchen/codespace/external-sources/pi/packages/agent/docs/observability.md at 61babc2:Pi 对 trace、span、异步上下文和 observability event 的设计说明,可作为把 trace 转成 OTel/Sentry 的后续阅读。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/export-html/template.js at 61babc2:HTML session export 中聚合 message、tool calls、tokens、cost 并渲染系统提示和工具执行,可作为 trace viewer 的产品形态参考。

学完本课后,你应该能做到:

  • 设计一条 agent trace 的最小事件格式,并解释每个字段用于哪个诊断问题。
  • 从事件流构建 event timeline,而不是只输出原始 JSONL。
  • 计算 token、cost、tool latency、失败数量和失败类型。
  • 区分 replay 和 rerun,并说明为什么 replay 不应该重新执行命令。
  • 设计 agent trace view <id>agent trace replay <id> 的输出边界。
  • 用命令证据判断 agent 最终报告是否可信。
  1. 实践题:扩展 /Users/ryanchen/codespace/pi-agent-course-lab/src/18-trace-replay.ts,新增 buildTimeline()toolLatency(),输出每个工具的耗时、错误状态和 timeline 行。
  2. 思考题:如果 trace 里显示 agent 运行了 npm test 但没有保存 stdout/stderr,只保存了 exit code,这条命令证据是否足够支撑“测试通过”?为什么?