我的 Agent 跑挂了,日志里只有一句"任务失败"。我盯着这五个字看了半天——它到底是在第 3 步歪的,还是第 18 步?是工具超时,还是参数传错了?
一行有用的信息都没有。所以我给它加了 trace,每次工具调用写一行 JSONL。写完大概 60 行代码,是我这段时间投入产出比最高的一次改动。
为什么是 JSONL,不是 JSON 数组
三个理由,都很实在:
- 追加写。一行
appendFileSync就完事,不用读出来、改数组、再整体写回。 - 崩了不丢。进程被杀时,前面写的行已经落盘了。整体读写的话,崩在写回那一步就全没了。
- 能直接 grep。想看某次失败,
grep '"ok":false' trace.jsonl就有。
字段我定了这么几个:
rows.push({
ts: new Date().toISOString(),
run_id: runId, // 这一轮任务的唯一标识
seq: i + 1, // 第几次调用
tool, // 工具名
args_hash: hash(JSON.stringify(args)), // 参数哈希,不是参数本身
ok: rnd() > 0.125,
ms: ..., // 耗时
bytes: ..., // 返回体大小
});args_hash 而不是 args 是刻意的。 工具参数里经常有文件路径、API key、token,全写进日志等于给自己埋雷。哈希之后我还能做"这两个调用是不是一回事"的判断,但看不出内容是什么。
报表长什么样
我用固定种子的伪随机造了 24 次调用(seed = 42,保证每次跑出来一模一样,方便贴出来),写完再读回来统计:
已写入 trace.jsonl - 3390 字节 / 24 行
=== Agent trace 回放报表 ===
总调用 24 | 失败 4 (17%) | 总耗时 6194ms
耗时 p50 = 281ms | p95 = 454ms | max = 461ms
按工具(按累计耗时排序):
write_file 调用 11 次 | 累计 2868ms | 平均 261ms | 失败 2
run_command 调用 4 次 | 累计 1446ms | 平均 362ms | 失败 1
web_search 调用 5 次 | 累计 1012ms | 平均 202ms | 失败 1
read_file 调用 4 次 | 累计 868ms | 平均 217ms | 失败 0
失败明细:
seq= 5 write_file args_hash=133abd53 182ms
seq=11 write_file args_hash=185b4a41 149ms
seq=20 run_command args_hash=292cf856 454ms
seq=21 web_search args_hash=145d828c 278ms
去重后唯一调用: 23 / 24 → 完全重复的 1 次(这些本可以走缓存)我看到这张表,两秒钟就定位了两个问题:
write_file是耗时大头,11 次占掉 2868ms,而且失败 2 次。它慢和它容易失败是同一件事——写文件通常要加锁、要落盘。- 失败集中在后半段(seq 20、21 连着挂),这不像工具本身的问题,更像前面的操作把状态搞坏了,或者是触发了某种限流。
如果没有 trace,我只会看到"任务失败",然后开始瞎猜。
几个必须注意的细节
run_id 一定要有。 我第一版没写,结果两轮任务的日志混在一个文件里,seq 从 1 重新开始,回放的时候完全分不清哪次是哪次。现在每次启动生成一个新的 run_id,回放时先按它分组。
用同步写,别用异步。 appendFileSync 每次调用多花不到 1ms,但能保证进程被 SIGKILL 时前面的日志一定在盘上。用 fs.appendFile 的异步版本,缓冲区里没落盘的那几条就没了——而恰恰是崩溃前那几条最有用。
分级: 我现在分三档,trace.jsonl(每次工具调用,全量)、run.log(每轮的起止和结论)、error.log(只有失败)。排查问题时从 error 往上看,不至于一上来就被几千行淹掉。
别把 trace 当数据库用。 JSONL 适合追加和顺序读,不适合随机查询。日志超过几十 MB 就按天切文件,别硬撑。
我的判断
如果只能给一个 Agent 加一种可观测性,我选 trace,不选 metrics,也不选花哨的链路图。
原因是 Agent 的失败几乎总是"第 N 步做了什么"的问题,不是"整体成功率掉了 3%"的问题。metrics 告诉我它病了,trace 告诉我它哪儿病了。而且 trace 是事后可以反复挖的——同一份日志,今天我能统计失败率,下周我能统计 token 消耗,不用重新埋点。
顺带一个意外收获:那句"完全重复的 1 次"提醒我,Agent 会绕回同一个动作。这就引出了缓存那件事,我另写一篇说。
脚本在 daily/trace-log.js,零依赖。跑完会在同目录生成 trace.jsonl,回放逻辑就在同一个文件里,改 tools 数组和循环次数就能套到自己的 Agent 上。
(写作与实测日期:2026-09-30,Node 22.22.2,本地真实运行输出。)
