针对 Node.js Agent 多轮调用,文章建议将需要处理的失败送入异常追踪,将重试、决策、延迟和用量写入结构化日志,并用同一关联 ID 串联。这样既能减少重试告警噪声,也能保留排查高延迟和高成本请求的证据。
简单来说:对于运行 AI Agent 循环的 Node.js SaaS,把需要处理的异常发送到错误追踪系统,用结构化日志记录完整执行过程,并在两者中使用同一个 correlation ID。这样既能调查延迟和 token 成本,也不会让每一次重试都触发告警。
运维上的关键约束是信号质量。Agent 在给出回答之前,可能会多次调用模型和工具,因此,一次用户请求即使最终成功,中间也可能出现多次缓慢或失败的尝试。把每次尝试都作为异常捕获,会制造噪声;只保留最终异常,又会掩盖导致请求成本高昂的执行路径。合理的划分依据,是每类信号要回答什么问题,而不是哪种传输方式最容易接入。
具体来说:错误追踪负责需要排查处理的失败,结构化日志负责按顺序记录尝试、决策、延迟和用量等证据。通过关联标识,把两者串成一次完整的调查。
当异常越过运维处理边界时,使用错误追踪:Agent 循环无法生成可接受的响应、工具违反约定,或者后台任务耗尽重试次数。事件应该保留异常类型、调用栈、发布版本标识,以及操作名称等少量稳定维度。事件还应该携带 correlation ID,但不要包含完整的 prompt 或工具载荷。
结构化日志用于记录能够解释正常和异常执行过程的事件:循环开始、模型调用完成、工具选择、重试、校验结果,以及循环结束。为所有事件使用一致的 schema。在这个场景中,有用的字段包括 event_name、correlation_id、trace_id、agent_run_id、attempt、duration_ms、input_tokens、output_tokens、outcome 和 error_type。重试通常应该是一条日志事件。重试预算耗尽后仍然存在的异常,通常才应该成为错误追踪事件。
最后,使用 correlation ID 作为连接键,贯穿 HTTP 请求处理、Agent 执行器、队列任务,以及日志和错误追踪这两个目标系统。如果已经有分布式追踪,W3C traceparent 格式提供了标准的追踪上下文;不要再创建一个毫无关联的身份标识,然后指望值班人员凌晨两点把它们对应起来。独立的业务标识,例如 agent_run_id,仍然可能有用,因为一次逻辑运行的生命周期可能比传输层操作更长,也可能跨越多个传输层操作。
这个区别很重要。correlation ID 可以连接记录,但不能替代 trace 中的时间信息和父子关系。
最简单的做法,是把每次不成功的模型或工具调用都算作错误。在 notebook 里,这看起来很全面。但到了生产环境,它会造成三种失真:预期内的重试推高失败计数,重复捕获把同一个事故拆成许多事件,而团队开始忽略那些不再意味着用户受到影响的告警。把这些尝试改标为 warning,也解决不了数据模型本身的问题。
应该从 Agent 运行的最终状态出发。如果一次暂时性超时经过重试后运行成功,就把这次尝试记录到结构化日志中,附上耗时和 token 用量。如果循环因为无法满足截止时间、重试预算或输出约定而停止,就只捕获一次终止异常,并把它关联到此前的尝试。这样既能保留证据,回答“为什么这次运行花了 8.4 秒,还调用了四次模型?”,也不会把每条内部分支都当成值得呼叫值班人员的事件。
但有一个例外:如果中间某次失败本身就代表安全事件、数据完整性风险,或违反了服务级别目标,那么只追踪终止异常就不合适。在这些情况下,即使后续回退成功,也应该创建一个独立、需要处理的事件。反过来,对于大规模审计记录、逐 token 计量和常规延迟分析,应该使用日志,而不是错误追踪系统;异常分组和排查处理工作流并不适合这些任务。
不存在通用的重试阈值。我不确定什么样的失败预算适合你的用户,日志库和错误追踪系统也都无法替你决定。要消除这种不确定性,需要使用能够代表实际工具组合的 eval 集合,再找出那个临界点:继续增加尝试,对任务成功率的改善已经不足以抵偿额外的延迟和 token 成本。
让事件约定足够精简,应用代码才能始终一致地生成事件。下表会先明确各自的职责,避免实现细节模糊这条边界。
不要让 error 对象直接变成 schema。应该在边界处将它规范化:稳定的错误类型、经过安全处理的消息、适用时保留的调用栈、操作名称,以及各类 ID。OWASP 的日志指南建议,排除或掩码处理 access token、密码、敏感个人数据和其他秘密信息。这一点对 AI 应用尤其重要,因为 prompt 和工具参数可能包含用户文本,而这些文本原本根本不打算进入可观测性存储系统。
导出之前先脱敏。
Node.js AsyncLocalStorage 可以在异步操作之间携带请求作用域的上下文。在入口处初始化上下文,如果已有标准追踪上下文,就复制过来,并通过队列消息显式传递所需标识。把传入的 correlation 值视为不可信输入:校验格式、限制长度,并在必要时生成新的内部值。日志组件和异常适配器应该读取同一份上下文,而不是让每个调用位置分别拼装并传入 ID 字段。
从 notebook 走向生产环境,应该先导出经过脱敏的事件并进行 eval,而不是先添加一条告警规则。下面的 Python 示例会对一组刻意保持精简的合成测试数据进行评分。它检查终止失败是否被捕获、重试后成功的事件是否混入待处理队列,以及日志中的运行记录是否保留了足够的成本和延迟字段用于分析。请用具有代表性、经过脱敏的导出数据替换这些测试数据;这段代码并不说明生产环境中的实际发生率。
from collections import defaultdict
events = [
{
"agent_run_id": "run-a",
"kind": "step",
"outcome": "retry",
"duration_ms": 900,
"input_tokens": 420,
"output_tokens": 0,
"tracked_exception": False,
},
{
"agent_run_id": "run-a",
"kind": "run",
"outcome": "success",
"duration_ms": 2400,
"input_tokens": 880,
"output_tokens": 190,
"tracked_exception": False,
},
{
"agent_run_id": "run-b",
"kind": "run",
"outcome": "failed",
"duration_ms": 5100,
"input_tokens": 1300,
"output_tokens": 75,
"tracked_exception": True,
},
]
def evaluate_signal_policy(rows):
runs = defaultdict(list)
for row in rows:
runs[row["agent_run_id"]].append(row)
terminal_failures = 0
captured_terminal_failures = 0
noisy_retry_captures = 0
measurable_runs = 0
for run_events in runs.values():
terminal = next(row for row in run_events if row["kind"] == "run")
has_measurements = all(
field in terminal
for field in ("duration_ms", "input_tokens", "output_tokens")
)
measurable_runs += int(has_measurements)
if terminal["outcome"] == "failed":
terminal_failures += 1
captured_terminal_failures += int(terminal["tracked_exception"])
noisy_retry_captures += sum(
row["kind"] == "step"
and row["outcome"] == "retry"
and row["tracked_exception"]
for row in run_events
)
return {
"terminal_capture_rate": (
captured_terminal_failures / terminal_failures
if terminal_failures
else None
),
"noisy_retry_captures": noisy_retry_captures,
"measurement_coverage": measurable_runs / len(runs),
}
print(evaluate_signal_policy(events))
在持续集成中,针对标注过的 eval 用例运行这项检查,然后在部署后与生产环境的聚合数据比较。有用的指标包括:终止失败的捕获覆盖率、每次运行产生的重复异常事件数、重试后成功却被送去排查的事件数、关联覆盖率,以及度量字段覆盖率。延迟和 token 用量的分布要按操作和结果分别统计;单一平均值可能掩盖缓慢的重试路径。
对 prompt 成本的关注也应该放在这里。成本分析需要成功和失败尝试的用量字段,而错误追踪只需要足够的上下文,帮助定位相关运行。把完整 prompt 同时发送到两个系统,只会增加暴露风险和存储量,并不能保证更容易诊断。只要能回答运维问题,就优先使用 prompt 模板版本、模型标识、token 数量、工具名称,以及脱敏后的哈希值。
首先,衡量终止失败有多大比例携带可用的调用栈和 correlation ID、一次运行有多大比例会产生多个待排查事件,以及多少次成功运行包含内部重试。接着,衡量处理人员能否凭借一个标识,从异常跳转到完整的日志轨迹。一个 ID 即使技术上存在,只要它在队列边界发生变化,实际效果就等于没有。
还要检验基数和保留策略。用户 ID、原始 URL、prompt 文本,以及任意异常消息,都不适合作为聚合维度,而且可能携带敏感数据。稳定的操作名称和取值范围受限的错误类型更容易分析。对于重复出现的成功日志,可以考虑采样,但要保留如实估算延迟和 token 用量所需的字段,并为涉及安全的记录制定单独的策略。
在 eval 回答以下两个问题之前,不要照搬这种目标系统划分:错误追踪事件是否可靠地意味着有人可以采取行动?关联日志是否能在不暴露敏感内容的前提下,还原成本高昂的执行路径?如果任一问题的答案是否定的,先调整事件约定和重试边界。工具选型放在后面。
如需进一步采取行动,可以考虑屏蔽此人和/或举报滥用行为。