设计 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_start、turn_start、message_update、tool_execution_start、tool_execution_end、message_end、agent_end 组成运行骨架。
第二是资源账本。assistant message 的 usage 提供 token 和 cost;工具事件的开始结束时间提供 latency;session stats 提供累计消息、工具调用和上下文使用情况。
第三是证据链。命令、exit code、stdout/stderr 摘要、diff 文件、测试结果和错误消息证明 agent 是否真的完成任务。
Trace Replay 不是重新执行危险命令,而是重放这些记录:按时间排序展示 agent 看到了什么、做了什么、证据是什么、失败在哪里出现。
1. 定义 trace event 的最小字段
Section titled “1. 定义 trace event 的最小字段”starter 里的 TraceEvent 只有 at、type、toolName 和 isError。这是好的第一步,但还不够定位问题。建议扩展成:
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 能过滤。
2. 从事件流构建 timeline
Section titled “2. 从事件流构建 timeline”不要把 trace 当作无结构日志。把每一行都归一化成事件,再按 at 排序:
00.000 agent_start00.120 turn_start #101.430 message_update assistant text02.010 tool_execution_start read02.060 tool_execution_end read ok 50ms03.500 tool_execution_start bash npm test05.800 tool_execution_end bash exit 1 2300ms05.900 agent_end有了 timeline,排查顺序就稳定了:先看失败前最后一个事件,再看它所属 turn,再看对应工具或 assistant message。
3. 计算 token、cost 和 latency
Section titled “3. 计算 token、cost 和 latency”token 和 cost 通常来自 assistant message 的 usage。Pi 的 AgentSession.getSessionStats() 会聚合 input、output、cacheRead、cacheWrite 和 cost。工具 latency 则由同一个 toolCallId 的 start/end 时间差计算。
注意:token/cost 是诊断指标,不等于任务质量。低成本失败不值得庆祝,高成本成功也可能不可接受。评测报告里要把它们放在“质量结果”旁边,而不是混成一个分数。
4. 建立 failure taxonomy
Section titled “4. 建立 failure taxonomy”失败分类要面向行动。推荐从这几类开始:
model_error:provider 返回错误或 stopReason 为 error。tool_error:工具执行失败或参数校验失败。command_failed:bash 命令非零退出。timeout:工具或任务超过限制。permission_blocked:权限策略阻断。context_overflow:上下文太长或压缩失败。false_success:没有证据却宣称完成。
分类不是为了贴标签,而是为了决定下一步:改 prompt、改工具、改权限、改 fixture,还是改 runtime。
5. 区分 replay 和 rerun
Section titled “5. 区分 replay 和 rerun”Replay 是读取旧 trace,不重新调用模型,也不重新执行命令。它适合 code review、事故复盘、成本分析和失败归档。
Rerun 是把同一个 task 在同一个 fixture 上重新执行。它适合验证修复是否有效。
如果 agent trace replay <id> 会重新跑 npm test 或写文件,它就不再是 replay,而是 rerun。产品命令必须把这两种行为分开。
6. 把命令证据纳入报告
Section titled “6. 把命令证据纳入报告”coding agent 的最终报告必须能回答:
- 哪些命令运行了。
- exit code 是多少。
- 关键输出是什么。
- 这些命令是否覆盖了任务成功标准。
命令证据最好单独结构化保存,不要只混在 assistant 文本里。这样 code review agent、benchmark runner 和 trace viewer 都能复用。
本课 starter 在:
cd /Users/ryanchen/codespace/pi-agent-course-labsed -n '1,220p' src/18-trace-replay.ts它的 summarizeTrace() 已经能输出 startedAt、endedAt、eventCount、toolEventCount、failureCount。下一步可以加两个能力:
- 按
toolCallId或toolName计算工具耗时。 - 根据
isError、exitCode、type推断 failure taxonomy。
课堂代码推演
Section titled “课堂代码推演”课堂上先从 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 进入 events,events 进入 summary 和 timeline,最终 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,包含TraceEvent和summarizeTrace()的最小实现。/Users/ryanchen/codespace/external-sources/pi/packages/agent/src/agent-loop.tsat61babc2:发出 agent、turn、message 和 tool execution 事件,是 event timeline 的主要来源。/Users/ryanchen/codespace/external-sources/pi/packages/agent/src/types.tsat61babc2:定义AgentLoopConfig、tool hooks、tool execution mode 和事件相关类型,是理解事件语义的入口。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.tsat61babc2:AgentSessionEvent扩展了低层事件,并提供getSessionStats()、getContextUsage()、retry、compaction 等 session 级诊断信息。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/agent-session-stats.test.tsat61babc2:验证 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.tsat61babc2:证明message_end上 finalized assistant usage cost 可以被扩展修正,是 cost 观测链路的具体回归测试。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/timings.tsat61babc2:启动耗时 instrumentation,展示 Pi 中最小 timing hook 的实现方式。/Users/ryanchen/codespace/external-sources/pi/packages/agent/docs/observability.mdat61babc2:Pi 对 trace、span、异步上下文和 observability event 的设计说明,可作为把 trace 转成 OTel/Sentry 的后续阅读。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/export-html/template.jsat61babc2: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 最终报告是否可信。
- 实践题:扩展
/Users/ryanchen/codespace/pi-agent-course-lab/src/18-trace-replay.ts,新增buildTimeline()和toolLatency(),输出每个工具的耗时、错误状态和 timeline 行。 - 思考题:如果 trace 里显示 agent 运行了
npm test但没有保存 stdout/stderr,只保存了 exit code,这条命令证据是否足够支撑“测试通过”?为什么?