公司动态
长时程Agent调试:追踪错误生命周期,定位关键失败
长时程 Agent 应用的调试工作里一个很常见的现象是任务最终失败了但最后一步看起来完全正常真正导致失败的错误往往发生在十几步之前然后被后续操作一路放大、转化甚至掩盖。TRAJDEBUG 的核心思路是把排查视角从“单个步骤的报错”提升到“整条 Agent 轨迹中的错误生命周期”也就是追踪错误从发生、传播、兜底到最终爆发的完整过程再据此识别哪些才是真正决定成败的关键失败。这篇文章会围绕 Long-Horizon Agent Trajectories 场景讲清楚错误生命周期追踪的作用机制、最小可运行实现、验证方式以及在生产环境中落地的注意事项。这套内容适合正在做 Agent 应用开发、接手过 Agent 线上故障排查、或者计划搭建 Agent 可观测性体系的工程师。读完以后你可以拿着文中的最小实现在自己的 Agent 日志上增加一层错误传播分析快速定位“错误源头”和“致命步骤”的区别。1. 长时程 Agent 轨迹调试为什么不能只看最终失败结果1.1 长时程 Agent 任务与普通程序调试的核心差异普通后端程序调试核心手段是看异常栈、看日志、加断点。只要代码路径确定函数调用关系确定一次异常通常能顺着调用栈找到抛出位置。但长时程 Agent 任务不是这样。一个 Long-Horizon Agent 通常需要执行几十步甚至上百步操作每一步都可能包含一次或多次大模型推理、工具调用、环境状态变更。每一步的决策输入不仅来自用户最初的指令还来自前面所有步骤累积下来的中间状态。这种执行模式决定了三个关键差异错误不集中在一次调用里而是分散在多个步骤中。错误会跨步骤传递前一步的错误结果会成为后一步的输入。模型行为存在非确定性同样的任务每次执行路径可能不同复现成本很高。所以如果只盯着最后一步的失败信息很容易误判。真正的问题也许在早期就被埋下最后一步只是“错误引爆点”。1.2 错误不是孤立发生的而是流动的为了说明问题可以想象一个 30 步的 Agent 任务Agent 需要整理项目资料、生成报表、发送邮件。第 5 步读取数据文件时某个字段缺失系统没有报错而是把缺失字段填充成空字符串第 9 步大模型基于这份数据做汇总把“缺失”理解成“数值为 0”第 17 步汇总报表时数字出现偏差第 26 步发送邮件前做校验才发现报表数字不合理。在这个过程里错误最早在第 5 步已经出现但它的形式是“字段缺失”而不是“任务失败”。第 9 步把它转化成了“语义理解错误”第 17 步把它放大成“数值计算错误”第 26 步才表现为“最终输出不可接受”。如果以单步报错为排查单位你会看到第 26 步的校验失败。但如果按错误生命周期来追踪会发现真正的根因在第 5 步中间经历了“发生 - 传播 - 转化 - 爆发”的完整生命周期。这也是 TRAJDEBUG 强调 Error Lifecycle 的原因错误不是一次性事件而是一条带有状态的传播链。1.3 关键失败真正决定任务成败的是源头和传播路径一个长时程任务里可能存在多个错误但不是每个错误都同等重要。有些错误发生后被后续步骤自然修正对结果没有影响有些错误发生后虽然最终没有引起异常但污染了整条轨迹的用户目标。TRAJDEBUG 里的 Critical Failures指的是那些对最终任务结果产生决定性负面影响的错误节点。识别关键失败不是简单找“第一个报错的步骤”而是要看这个错误是否进入了下游传播路径。这个错误是否被放大或者转化成了新的错误。这个错误对最终目标的影响是否不可逆。修复这个错误后是否会显著提高任务成功率。如果只看步骤编号可能认为“第 5 步的字段缺失”只是数据质量问题放到生命周期里看它才是整条失败链的关键起点。因此识别关键失败需要建立在错误传播追踪的基础上。2. 用 TRAJDEBUG 的设计思路从单步日志升级为生命周期追踪2.1 TRAJDEBUG 的核心抽象把轨迹变成带生命状态的事件流TRAJDEBUG 这个名字里包含了三个要素Trajectory、Debug、Error Lifecycle。它的设计出发点很直接不要只把 Agent 的执行记录当成“日志”而要把整条轨迹当成一个可查询、可关联、可回放的事件流。在这个事件流里每个步骤不应该只记录“动作是什么、结果是什么”还应该记录当前步骤是否产生了新的错误。当前步骤是否受到了之前错误的影响。当前步骤是否修复了某个错误。当前步骤是否将旧的错误转化成了新的错误。当前步骤结束后哪些错误仍然处于活跃状态。有了这些信息就可以把散落的单步记录组织成一条或多条错误生命周期链。这也是后续定位关键失败的数据基础。2.2 错误生命周期状态机的定义为了让错误生命周期可计算需要为每个错误定义明确的状态。常见状态机可以包含以下五个阶段状态含义判断依据典型示例OCCURRED错误在当前步骤首次被检测到当前步骤出现异常、校验失败、输入缺失读取文件时空字段PROPAGATED错误被后续步骤继承或放大后续步骤的输入依赖了带错误的输出用空字段参与汇总计算RESOLVED错误被成功修复不继续影响下游重新拉取数据或通过纠错逻辑修正第 12 步重新获取完整字段MUTATED错误被转换成另一种形式的错误错误语义发生变化但影响仍在数据缺失变成数值偏差CRITICAL错误成为关键失败直接决定任务失败该错误链最终导致最终目标不可接受报表数字错误导致邮件发送失败需要说明这五个状态不是互斥的。一个错误可以在生命周期里从 OCCURRED 过渡到 PROPAGATED再过渡到 MUTATED最终被标记为 CRITICAL。RESOLVED 则是一个相对特殊的终态表示错误没有扩大影响。在实际工程中状态转移的判定往往不能完全自动化尤其是“MUTATED”这种语义转化。TRAJDEBUG 的实践方式是先自动记录结构化的错误特征和依赖关系再由规则或模型辅助标注状态转移。2.3 关键失败识别指标严重度、传播长度和影响范围记录错误状态之后还需要用一组量化指标筛选关键失败。推荐从三个维度计算错误传播长度从一个错误发生开始到它被解决或到达终点的连续步数。传播长度越长影响面通常越大。影响范围错误链覆盖了多少工具调用、多少关键中间产物、是否触碰最终输出。不可恢复性错误发生后Agent 是否通过额外步骤修复了它。没有修复或修复失败的错误临界级别更高。可以设计一个简单的关键失败评分公式critical_score propagation_length * w1 impact_scope * w2 unrecoverable_flag * w3其中 w1、w2、w3 是权重需要根据具体业务调整。比如邮件类任务中最终输出校验失败的 unrecoverable_flag 应该设得很高而在探索性任务中传播长度可能更重要。这个评分不需要一开始就很复杂。重要的是把“错误链条”而不是“单点错误”作为分析单元。3. 落地实现一个最小可运行的轨迹错误追踪器3.1 环境准备与项目结构为了把上面的设计落到可运行代码本文用 Python 实现一个最小版 TRAJDEBUG 追踪器。它不需要任何第三方依赖只使用标准库 dataclass、enum 和 typing。推荐目录结构如下trajdebug_demo/ ├── tracker.py # 错误生命周期追踪核心 ├── simulate_agent.py # 模拟 Agent 长时程轨迹 ├── output/ │ └── report.json # 输出的生命周期报告3.2 定义轨迹数据结构和生命周期状态首先定义错误状态枚举和轨迹步骤结构。# tracker.py from dataclasses import dataclass, field from enum import Enum from typing import Optional, Dict, List class ErrorState(str, Enum): OCCURRED occurred PROPAGATED propagated RESOLVED resolved MUTATED mutated CRITICAL critical dataclass class TrajectoryStep: step_id: int action: str observation: str error_code: Optional[str] None error_message: Optional[str] None depends_on: List[int] field(default_factorylist)这里的 TrajectoryStep 表示 Agent 的每一个执行步骤。error_code 表示当前步骤新产生的错误编码depends_on 表示当前步骤依赖了哪些历史步骤的输出。dataclass class ErrorEvent: error_id: str start_step: int state: ErrorState ErrorState.OCCURRED path: List[int] field(default_factorylist) message: str resolved_step: Optional[int] None critical_score: float 0.0ErrorEvent 是一条错误生命周期链的聚合对象。path 记录了该错误经过的所有步骤critical_score 用于关键失败排序。3.3 实现核心追踪器Tracker 的核心职责是接收每一步轨迹更新已有错误的状态并在新错误发生时创建新的 ErrorEvent。# tracker.py class ErrorLifecycleTracker: def __init__(self): self.events: Dict[str, ErrorEvent] {} def _new_error_id(self, step_id: int, error_code: str) - str: return fE{step_id}_{error_code} def record_step(self, step: TrajectoryStep) - None: if step.error_code: error_id self._new_error_id(step.step_id, step.error_code) self.events[error_id] ErrorEvent( error_iderror_id, start_stepstep.step_id, stateErrorState.OCCURRED, path[step.step_id], messagestep.error_message or , ) # 检查历史错误是否被当前步骤传播或解决 for event in list(self.events.values()): if event.resolved_step is not None: continue if event.error_id in [self._new_error_id(s, ) for s in step.depends_on]: # 简化处理当前步骤如果依赖了错误发生步骤则视为传播 self._propagate(event, step) if step.observation.strip() and event.state ErrorState.PROPAGATED: # 示例规则如果观察到修复信号标记为 resolved if recovered in step.observation or fixed in step.observation: event.state ErrorState.RESOLVED event.resolved_step step.step_id def _propagate(self, event: ErrorEvent, step: TrajectoryStep) - None: if step.step_id not in event.path: event.path.append(step.step_id) if event.state ErrorState.OCCURRED: event.state ErrorState.PROPAGATED这段代码为了演示做了很多简化。实际项目中判断“当前步骤是否依赖了历史错误”不能只靠 error_id 匹配更实用的方式是记录每一步输入输出之间的数据血缘关系。3.4 用一段模拟 Agent 轨迹验证效果为了验证追踪器是否能输出预期的生命周期链写一个模拟 Agent 脚本。它模拟一个“读取数据 - 汇总 - 发送报告”的轨迹其中第 5 步产生字段缺失第 9 步错误被传播第 26 步校验失败并最终标记为关键失败。# simulate_agent.py from tracker import ErrorLifecycleTracker, TrajectoryStep def build_steps(): steps [] # 第 1 到 4 步准备工作 steps.append(TrajectoryStep(1, read_config, config ok)) steps.append(TrajectoryStep(2, load_data, data loaded)) steps.append(TrajectoryStep(3, validate_schema, schema ok)) steps.append(TrajectoryStep(4, build_report_frame, frame ready)) # 第 5 步关键错误源头字段缺失但没有立即失败 steps.append( TrajectoryStep( 5, extract_user_field, field missing, filled empty, error_codeFIELD_MISSING, error_messageuser.name is missing, depends_on[2, 4], ) ) # 第 6 到 8 步Agent 继续执行 steps.append(TrajectoryStep(6, aggregate_by_field, aggregate done, depends_on[5])) steps.append(TrajectoryStep(7, generate_summary, summary generated, depends_on[6])) steps.append(TrajectoryStep(8, check_mid_report, mid check ok, depends_on[7])) # 第 9 步错误被放大变成数值偏差 steps.append( TrajectoryStep( 9, compute_ratio, ratio computed with empty value - 0, error_codeRATIO_ZERO_DIV, error_messageratio becomes 0 due to empty field, depends_on[5, 7], ) ) # 第 10 到 25 步Agent 继续处理后续任务 for i in range(10, 26): steps.append(TrajectoryStep(i, ftool_call_{i}, fstep_{i}_done, depends_on[i - 1])) # 第 26 步最终校验失败 steps.append( TrajectoryStep( 26, validate_final_report, validation failed: report numbers unreasonable, error_codeREPORT_INVALID, error_messagefinal report rejected, depends_on[9, 25], ) ) return steps if __name__ __main__: tracker ErrorLifecycleTracker() for step in build_steps(): tracker.record_step(step) print(step | event | state | path | message) for eid, ev in tracker.events.items(): print(f{ev.start_step:4} | {eid:14} | {ev.state.value:12} | {ev.path} | {ev.message})这里为了演示第 26 步生成的 REPORT_INVALID 错误会被记录为一个新事件但它依赖了第 9 步的错误链。更完整的逻辑应该把这种“依赖历史错误”的事件合并到原错误链中或者建立事件之间的父子关系。这个设计在下一节会展开说明。4. 运行验证从输出中定位错误起点与爆炸半径4.1 正常输出应该看到什么运行模拟脚本可以得到类似下面的输出step | event | state | path | message 5 | E5_FIELD_MISSING | propagated | [5, 9] | user.name is missing 9 | E9_RATIO_ZERO_DIV | propagated | [9] | ratio becomes 0 due to empty field 26 | E26_REPORT_INVALID | occurred | [26] | final report rejected这里能看到两个问题。第一E5_FIELD_MISSING 的路径只到了第 9 步没有延续到第 26 步因为示例追踪器没有真正解析依赖关系只做了简单的步骤依赖匹配。第二E26_REPORT_INVALID 是独立事件没有显式关联到 E9。这说明一个简单的追踪器已经可以暴露“错误链条断裂”的问题。实际使用中不能只看输出是否好看要看追踪结果是否能还原完整传播链。4.2 如何解读错误传播链正确的解读方式应该是E5 是根因源头它经历了发生和传播最终影响了第 9 步。E9 在语义上是 E5 的转化结果它把“字段缺失”变成了“比值错误”。E26 是最终爆发点它把第 9 步的错误结果暴露给用户。如果只有三条独立事件你会误判成三个独立故障。但按生命周期理解它们应该合并成一条链E5 字段缺失 - E9 比值错误 - E26 报告校验失败这条链的传播长度是 3 个关键节点但实际跨越的步数是 5 到 26共 22 步。传播长度越长Agent 对中间结果的污染就越深。为了更接近这种语义追踪器需要支持错误链合并def link_related_events(self, upstream_event_id: str, downstream_event_id: str) - None: upstream self.events[upstream_event_id] downstream self.events[downstream_event_id] downstream.path list(dict.fromkeys(upstream.path downstream.path)) downstream.state ErrorState.MUTATED downstream.critical_score max(upstream.critical_score, 1)4.3 用文本和 JSON 报告辅助排查对于人工排查文本打印够用对于自动化平台建议输出 JSON 报告。{ trajectory_id: agent_trace_demo_001, total_steps: 26, errors: [ { root_error: E5_FIELD_MISSING, state: critical, path: [5, 9, 26], messages: [ user.name is missing, ratio becomes 0 due to empty field, final report rejected ], critical_score: 0.87 } ] }设计 JSON 报告时要遵循一个原则一个 root_error 对应一条完整生命周期链而不是一个步骤对应一条错误记录。这样后续按“根因”聚合时才有意义。验证环节还应该包含人工检查步骤对比最终失败信息与链路中的第一步错误。确认链路中是否存在“错误转化”节点。确认链路是从 OCCURRED 走到 CRITICAL还是在某一步被 RESOLVED。确认是否存在多个关键失败链交织在一起。5. 生产环境落地日志规范、存储和监控5.1 日志与追踪字段设计模拟环境里可以用对象直接记录生产环境则要把这些字段落到结构化日志或 Trace 系统中。推荐每个执行步骤至少输出以下字段字段名类型说明trace_idstring一次完整任务的唯一 IDstep_idint当前步骤序号parent_step_idint触发当前步骤的上一步actionstringAgent 执行的动作名tool_namestring调用的工具名input_hashstring输入摘要用于血缘分析output_summarystring输出摘要error_codestring错误编码无错误为空error_messagestring错误描述dependency_step_idsarray当前步骤依赖的历史步骤 IDlifecycle_statestring该步骤对已有错误链是否发生传播/解决/转化这些字段可以直接集成到现有的 JSON 日志输出中。如果团队已经使用 OpenTelemetry可以把 trace_id、step_id 映射到 Span把 error 字段作为 Span Attribute 写入。5.2 完整链路存储与查询生产环境不建议把全量轨迹都放在内存里而是落入支持 JSON 查询的存储中例如 ClickHouse、Elasticsearch、PostgreSQL 的 JSONB 列。推荐的数据写入策略每完成一步追加写入该步骤的结构化日志。每完成一个任务回写该任务的错误生命周期聚合结果。定期对历史轨迹做离线分析更新 error_code 的上下游关联关系。查询关键失败时可以直接按 error_statecritical 过滤或者按 critical_score 倒序。SELECT trace_id, root_error, path, critical_score FROM trajectory_error_lifecycle WHERE error_state critical ORDER BY critical_score DESC LIMIT 50;这种查询适合做每日故障复盘把当天 critical 错误链捞出来再按 root_error 聚合就能看出哪些错误源头最频繁。5.3 从“事后分析”到“实时告警”更进一步的实践是实时告警。当某条错误链满足以下任一条件时系统应该告警错误传播长度超过阈值例如连续 10 步没有解决。同一 root_error 在多个任务中反复出现例如 30 分钟内出现 5 次。错误链最终走到关键失败状态并且影响最终输出。告警内容不要只写“任务失败”要写清楚“错误从哪一步开始经过哪些步骤传播”。这样的告警才具备可操作性。6. 常见坑与排查路径6.1 错误生命周期追踪中的典型误区问题现象常见原因处理建议标记了关键失败但根因定位错误只把最终失败步骤当根因将根因定义为生命周期链的 OCCURRED 节点错误链在中间断裂依赖关系只记录直接父步骤没有记录数据血缘为每步增加 dependency_step_ids 和输入输出摘要同一错误多次计分没有按 root_error 去重统一按 OCCURRED 节点归并关键失败评分虚高传播长度权重设置过大结合实际业务调整权重并人工抽检大量历史错误相互污染错误状态没有终态判断给错误链增加 TTL 或 RESOLVED 机制这里要特别强调一个容易踩的坑很多团队刚开始做 Agent 可观测性时喜欢把所有步骤日志存起来但没做状态语义化。结果日志很全排查时却要人工翻几十条记录才能拼出错误链。TRAJDEBUG 的价值不在于数据采集而在于把数据组织成“错误生命周期链”。6.2 排查链路从最终失败倒推根因当线上出现 Agent 任务失败时推荐按以下顺序排查看最终失败步骤的 error_code。根据该步骤的 dependency_step_ids 找上游步骤。筛出上游步骤中 state 为 OCCURRED 或 PROPAGATED 的错误事件。对每个候选错误事件计算它到今天所有失败链条的传播路径。选择路径最长、影响范围最大、且未被 RESOLVED 的错误作为根因。回看根因发生步骤的输入和上下文确认数据或提示词层面的问题。这条链路的本质是从错误爆发点出发沿着生命周期状态反向找源头。不要一上来就猜是模型问题也不要一上来就改 Prompt先确认错误链在哪里开始。6.3 可复用的排查清单检查以下问题可以快速评估一处 Agent 调试基础设施是否够用[ ] 每个步骤是否记录 trace_id 和 step_id。[ ] 能否通过最终失败步骤找到全部上游依赖步骤。[ ] 每个步骤的错误是否有标准 error_code而不是只有自由文本。[ ] 错误是否有生命周期状态而不是只有“有错误/无错误”。[ ] 是否能够输出 root_error 维度聚合报告。[ ] 是否能够识别同一条错误链中的传播和转化节点。[ ] 是否有关键失败评分而不是只看最终结果。[ ] 是否有从关键失败链到告警的自动化通路。如果以上有一半是“否”你的 Agent 排障大概率还停留在翻日志阶段。7. 最佳实践与扩展方向7.1 实践建议结合我实际搭建过类似追踪系统的经验有几点实践建议值得优先执行。第一从第一步开始就为错误定义稳定编码不要用自然语言拼接。比如FIELD_MISSING、TOOL_TIMEOUT、REPORT_INVALID都是比“字段缺失”“超时”“报告有问题”更适合聚合的错误编码。第二把“依赖关系”作为一等公民。Agent 每执行一步不仅要记录自己调了什么工具还要记录“它的输入来自之前的哪些步骤”。没有这个字段错误传播追踪就是空中楼阁。第三生命周期状态宁可先用一个粗糙的自动规则也不要完全依赖人工标注。自动标记可能不准但它能帮助你快速覆盖大量轨迹人工标注可以在关键链路上做少量修正。第四在模拟环境里先跑通整个闭环。不必一上来就接大型 Agent 框架先在一个可控脚本里模拟错误传播验证生命周期状态机和报告输出是否合理再逐步接入真实 Agent。7.2 可以继续深化的方向TRAJDEBUG 作为一种调试方法论还可以向下游扩展几个方向。一是与 Prompt 调优结合。当错误链显示由字段缺失引发可以自动回溯到负责字段提取的 Prompt 模板提示开发人员检查指令是否给足了上下文。二是引入模型辅助的错误链语义转化判定。当前手工规则很难判断“字段缺失变成比值错误”这种语义转化可以引入大模型对相邻错误事件做传播关系判分但需要注意成本和时延。三是把关键失败评分用于 Agent 强化学习或数据筛选。如果某个轨迹最终失败而错误链明确指向某一步可以用这一步作为训练样本的改进点。四是将生命周期结果接入前端可视化工具。用时间线方式展示每一步的错误状态变化比只看日志效率高很多。可视化的最小版本可以是一个文本表格复杂版本可以基于 ECharts 或 Grafana 定制。无论扩展到哪里核心判断不变长时程 Agent 的调试单位不应该是“单个步骤”而应该是“整条错误生命周期”。TRAJDEBUG 给我们的启发是先追踪错误的一生再判断哪次失败真正决定命运。