我的 Agent 跑挂了,日志里只有一句"任务失败"。我盯着这五个字看了半天——它到底是在第 3 步歪的,还是第 18 步?是工具超时,还是参数传错了?

一行有用的信息都没有。所以我给它加了 trace,每次工具调用写一行 JSONL。写完大概 60 行代码,是我这段时间投入产出比最高的一次改动。

为什么是 JSONL,不是 JSON 数组

三个理由,都很实在:

  1. 追加写。一行 appendFileSync 就完事,不用读出来、改数组、再整体写回。
  2. 崩了不丢。进程被杀时,前面写的行已经落盘了。整体读写的话,崩在写回那一步就全没了。
  3. 能直接 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,本地真实运行输出。)

Last modification:September 30, 2026
如果觉得我的文章对你有用,请随意赞赏