用零依赖 Node.js 日志追踪智能体执行
文章提供可在模拟智能体循环中运行的 Node.js 追踪示例,无需 API 密钥。方案用统一请求 ID 串联步骤,以 JSON 行记录工具执行耗时、成功状态和错误信息,并建议向用户展示追踪 ID。
AI 深度解读
Agent 答错了,只看聊天记录,往往不知道它调用过哪些工具、哪一步失败、时间花在哪里。作者给出的办法很直接:先用一个零依赖的 Node.js 日志包装器,把每次请求的执行步骤串起来,再谈复杂的观测平台。
这份示例用假 Agent 演示,不需要 API key。作者称已在 Node 24 上运行,断言通过。它值得借鉴的地方,是用很少的代码回答三个排障问题:做过什么、有没有失败、哪里最慢。但它能记录执行过程,不能直接解释模型为什么做出某个判断。
一次请求的所有步骤,必须能串起来
聊天记录能告诉你用户问了什么、Agent 回了什么,却未必包含中间的工具调用。答案看起来正常,也可能有一步失败后被跳过了。
作者提出的最低要求有三项:同一次请求共享一个 traceId;每一步记录耗时和成功状态;失败时保留具体错误消息。再给事件加上时间戳,就能按请求还原执行过程。

其中,traceId 还应该返回给用户。用户反馈答案有问题时,客服拿这个 ID 就能搜索整次执行,不必靠时间戳猜是哪条请求。
下面的包装器负责计时、记录结果,并提供最慢步骤查询:
const { randomUUID } = require("node:crypto");
const events = [];
function logEvent(event) {
events.push({ ts: new Date().toISOString(), ...event });
}
async function traced(traceId, step, fn) {
const start = performance.now();
try {
const result = await fn();
logEvent({ traceId, step, ms: Math.round(performance.now() - start), status: "ok" });
return result;
} catch (err) {
logEvent({
traceId,
step,
ms: Math.round(performance.now() - start),
status: "error",
error: String(err.message || err),
});
throw err;
}
}
function slowestSteps(traceId, top = 3) {
return events
.filter((e) => e.traceId === traceId)
.sort((a, b) => b.ms - a.ms)
.slice(0, top)
.map((e) => `${e.step}: ${e.ms} ms (${e.status})`);
}
最该留意的是 throw err:记录错误之后继续抛出,让调用方决定是否恢复。日志包装器不应顺手把业务异常吞掉。耗时使用 performance.now(),避免像 Date.now() 那样受到系统时钟调整的影响;Math.round() 则把结果取整为毫秒。
这里的 events 数组只是为了缩短示例、方便断言检查。作者建议真实服务每一步输出一行 JSON,写入日志流,便于当前过滤和以后接入日志系统。
最终回答成功,不代表中间没有失败
假 Agent 的执行顺序是规划、查订单、查政策、生成回答。每一步都经过 traced,但政策查询失败后,业务代码选择继续:
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function runAgent(question) {
const traceId = randomUUID();
await traced(traceId, "plan", () => sleep(30));
const order = await traced(traceId, "tool:lookup_order", async () => {
await sleep(120);
return { id: "A1", status: "shipped" };
});
try {
await traced(traceId, "tool:check_policy", async () => {
await sleep(15);
throw new Error("policy service timeout");
});
} catch (_) {
// Agent 继续执行,追踪记录保留失败证据
}
await traced(traceId, "final_answer", () => sleep(40));
return { traceId, answer: `Order ${order.id} is ${order.status}.` };
}
要看的是外层 catch:允许执行继续的是 Agent 的业务逻辑,包装器仍然把错误抛了出来。最终回答为 Order A1 is shipped.,日志里却保留了 policy service timeout。
用户看到的成功,和每一步执行成功,是两回事。
示例分别等待 30、120、15、40 毫秒。这些是模拟延迟,不能当作真实模型或工具的性能数据。作者用 node:assert 检查了回答内容、总计 4 条事件、其中 1 条错误,并通过 slowestSteps(traceId, 1) 确认最慢步骤是 tool:lookup_order。这验证的是示例行为,没有证明线上吞吐或日志开销。
真实流量进来后,可以优先找三类现象:某个工具每次都慢;错误被捕获后悄悄跳过;同一个工具被反复调用。它们分别提供性能优化、失败处理和循环排查的线索,但仍需要结合业务确认原因。
作者还建议在数据规则允许时记录用户角色和工具参数,并在写入前脱敏、设置保留期限。工具参数与模型输出可能包含个人数据,记录更多细节也会增加存储风险。
至于告警,原文举的是某个工具的耗时中位数一夜翻倍。它是监控思路,并不是现有 slowestSteps 已经具备的能力:后者只排序单次请求中的步骤,没有跨请求聚合和历史比较。
我的判断
我认同先把执行证据留下来。对于刚开始接入工具调用的 Node 服务,这个包装器容易理解,也便于检查错误是否被业务代码悄悄处理掉。作者任职于 AI Hive 的 Agent 平台,但示例不依赖该平台。
不过,它适合作为最小起点,离完整的追踪系统还有距离。
首先,错误后能否继续取决于业务。查询订单状态时跳过某项辅助检查,和必须完成政策校验才能作答,要求完全不同。示例只演示了继续执行,没有证明这样回答正确。
其次,作者说包装后不会改变代码行为,这个说法有前提。正常记录时,它保留返回值并重新抛出异常;如果 logEvent 自己失败,示例并没有隔离这种影响。将它换成真实日志流时,需要重新考虑日志故障如何处理。
最后,同一个请求 ID 能关联事件,却不足以表达多个 Agent 相互调用的父子关系。原文也承认需要继续补充这层结构。对于已经接入统一链路追踪的团队,我更倾向于沿用现有体系,补齐 Agent 步骤,而不是另建一套 ID。作者没有提供与成熟方案的对照数据,关于排障收益和接入成本的判断,仍属于经验之谈。
能拿来做什么
- 给 Node Agent 的规划、工具调用和最终回答统一加上
traced,先确保每一步都有traceId、耗时、状态和错误消息。 - 在前端反馈入口保留并展示服务端返回的
traceId,让用户报告问题时能带上对应执行记录。 - 检查允许失败后继续的工具调用,明确哪些错误可以降级、哪些必须终止回答,同时保留失败证据。
- 上线时把数组记录替换为结构化日志流,先确定参数脱敏与保留期限;需要耗时告警或多 Agent 分析时,再补跨请求聚合和父子关系。