Agent 上线后出错,如何用链路追踪定位
文章讨论测试阶段未暴露的工具死循环、重试异常和延迟激增,说明仅看错误日志为何难以定位原因。通过生产案例介绍如何追踪执行步骤和工具调用,辅助还原故障过程。
AI 深度解读
Agent 通过离线评测,上线后仍可能反复查同一条库存,或在多层重试中耗掉几十秒。原文的核心主张是:排查这类故障,要把一次请求中的模型步骤、工具调用和耗时串起来,再给执行过程设置总预算。
这篇值得读的地方,是把「单次调用正常,整个任务却失败」讲清楚了。不过,文中的案例缺少完整统计,示例代码也有明显边界。对正在给业务接入 Agent 的开发者,它更适合作为排障思路,而非生产实现模板。
为什么每一步都正常,任务还是完不成
原文引用 sarkars/agent-failure-modes 和 onTracify/Tracify 的案例,指出合成评测容易漏掉工具循环、特殊提示词尾部触发的失控调用链,以及重试叠加导致的延迟。
其中最具体的数据是:Tracify 工程师检查一个新 RAG Agent 上线前 48 小时的日志,发现 11 起步骤循环或延迟过高事件,QA 和实验室评测都没复现。原文还提到,部分 Agent 通过全部 OpenAI Eval 后,上线第一天仍陷入无法推进的状态,但没有交代评测集、请求量和通过标准。
问题出在观察粒度。库存工具 inventory.check(1234) 返回缺货,Agent 接着重查,结果还是缺货。每次调用都能成功返回,业务任务却没有进展。
另一个例子更隐蔽:用户问商店 X 现在是否营业,Agent 在 get_store_hours(X) 和 get_current_local_time(X) 之间来回切换。营业时间已经返回「Open until 9PM」,它仍未把两个结果组合成答案。
工具调用成功,不等于任务正在向完成推进。
普通错误日志能告诉你发生了 timeout、422 或断言失败;链路追踪(Tracing)则把同一次请求的步骤排成时间线,让你看到它从哪里开始重复、在哪个分支失去退出条件。这里需要的是执行路径和可观察的决策信息,不必把它理解为模型内部完整的思维过程。
延迟为什么会在多个正常组件之间失控
原文的另一个案例是重试叠加。用户遇到偶发的数分钟等待,局部日志却只显示每次调用重试一两次。
给出的具体 trace 从 fetch_item_details(id=42) 开始:5 秒后超时,10 秒时再次超时,15 秒时转向备用工具;备用工具又因 429 触发重试,最后在 30 秒后返回失败。这个片段展示了 30s+ 的链路,没有完整解释数分钟等待的全部过程。
单看各个组件,它们可能都遵守了自己的重试规则。把主工具、备用工具和上层调度连起来,才发现每层都在消耗时间。
因此,原文建议同时设置单次调用限制和请求总预算。下面这段代码试图把执行时间限制在 20 秒内:
TOTAL_BUDGET_SEC = 20
start = time.time()
retries = 0
while (time.time() - start) < TOTAL_BUDGET_SEC:
try:
result = tool_call(...)
if result['ok']:
break
except SomeToolException:
retries += 1
continue
else:
# hit overall budget cap
logger.error(f"Aborted after {retries} retries and {TOTAL_BUDGET_SEC} seconds")
要看清 while 那一行:它只在下一轮开始前检查时间。假如 tool_call(...) 一直阻塞,这段代码不能在第 20 秒强制终止调用;它也只统计 retries,没有设置重试次数上限。
实际接入 Node 服务时,需要把剩余预算传到下游请求,并让主工具与备用工具共用截止时间。只在最外层放一个计时循环,仍可能超时。
埋点之后,还要有能阻止失控的规则
原文提出的最低追踪要求包括:每个请求有独立 trace ID,每个决策步骤和工具调用有子跨度(child span),记录步骤耗时、API 结果及相关输入。
它提供的 Python 包装器用 uuid.uuid4() 生成标识,以 time.time() 计时,通过 trace_agent_step 装饰函数,成功时记录结果,失败时记录异常再抛出。结果只保留 str(result)[:300],有助于控制记录体积,也可能截掉关键线索。示例中的 time.sleep(0.2) 只是模拟工具耗时。
这个包装器能帮助理解埋点,但没有真正建立父子 span,也没处理跨服务传播和持久化。原文称 Tracify 的完整实现兼容 OpenTelemetry,不能据此把示例视为完整实现。
拿到 trace 后,接下来才是设置护栏。原文提到用 max_steps 限制最大步数,并提供重复检测代码:
class LoopDetector:
def __init__(self, max_repeats=3):
self.history = []
self.max_repeats = max_repeats
def check(self, action):
self.history.append(action)
if self.history[-self.max_repeats:] == [action] * self.max_repeats:
raise RuntimeError(f"Detected {self.max_repeats} repeats of {action}")
detector = LoopDetector()
for act in agent_action_sequence:
detector.check(act)
# run act...
关键是比较历史切片的那一行:默认 max_repeats=3,只能发现同一个 action 连续出现 3 次。前面营业时间与当前时间交替调用的 A、B、A、B 循环,并不会被它识别。
所以,这段实现与原文声称的「切断多工具交替循环」并不完全对应。检测周期性序列还需要额外逻辑,而 max_steps 可以提供更直接的最终兜底。
我的判断
我赞同原文把排障单位从单次 API 调用提升到完整请求。Agent 会动态选择工具,只记录最终输出和异常,很难解释中间为什么走偏。相比不断增加零散日志,统一请求标识和步骤时间线更容易定位问题。
但「合成评测漏掉大多数生产故障」「追踪是唯一经过证明的方法」都说得过满。48 小时、11 起事件只是一个案例,没有总请求量,也没有与其他排障方案的对照数据。作者称统一预算消除了生产尾延迟事件,同样没有提供观察周期和延迟分位数,只能按案例经验看待。
追踪也不能替代评测。合理的闭环是:线上发现异常路径,修复退出条件,再把真实失败输入加入回归用例。固定工作流还能在编排层直接限制合法状态转换,减少模型自由选择带来的循环空间。
此外,原文要求记录所有「thoughts」并不严谨。能记录什么取决于 API 实际返回什么;工具参数、结果摘要、状态转换和耗时,通常才是可以明确落地的观察对象。
能拿来做什么
- 给现有 Node Agent 增加请求级 trace ID,串起模型调用、工具执行、重试和备用分支,让一次用户请求能被完整检索。
- 同时设置
max_steps、总重试次数和总耗时限制,并让下游请求使用剩余时间,避免每一层重新获得完整预算。 - 用真实 trace 检查连续重复和多工具交替调用,区分必要复查与无进展循环,再决定检测规则和中止条件。
- 把线上失败输入及关键工具返回整理成回归用例;前端展示超时或中止状态时,也保留请求标识,方便从用户反馈追到具体执行路径。