应用工程

Agent 失败任务死信队列设计

Agent 任务跑在异步队列里,失败之后按 标准重试策略 退避重试是第一道防线。但重试次数用完之后呢?很多团队的答案是”打日志然后不了了之”——任务静默消失,用户等不到结果,运维也不知道哪些任务卡死了。死信队列(Dead Letter Queue,DLQ)就是补上这道缺口的机制。本文讲清楚怎么为 Agent 任务场景设计 DLQ,而不是照搬传统消息队列的现成方案。

一、为什么普通重试机制不够用

Agent 错误恢复机制解决的是”这次调用失败了怎么重试”,但重试本身有边界:

  • 重试次数或时限用完后,任务必须有一个明确的”最终去处”,不能就地消失。
  • 有些失败重试无意义:比如工具返回的是业务级拒绝(权限不足、参数非法),而不是网络抖动,重试 100 次结果都一样。
  • 失败任务如果不集中管理,运维排查时只能翻日志,无法批量查看”现在有多少任务卡在失败状态、都是什么原因”。

DLQ 的作用就是把”重试耗尽或判定不可重试”的任务,从主队列移出来,放进一个可观察、可人工介入、可选择性重放的独立通道。

二、什么时候该把任务打入死信

不是所有失败都该直接进 DLQ,先做失败分类:

失败类型是否该进 DLQ原因
网络超时、限流 429否,先走标准重试大概率是瞬时问题,重试能自愈
工具调用返回业务拒绝(权限/参数错误)是,且应跳过剩余重试直接进入重试不会改变结果,浪费配额还拖长用户等待
LLM 输出连续多次不符合预期 schema达到重试上限后进入可能是 prompt 本身有问题,需要人工看
依赖的外部服务持续不可用(超过熔断阈值)是避免整条队列被这一个依赖拖死
任务执行中途进程崩溃/被强杀视情况,恢复后先补一次重试再决定需要区分是任务本身的问题还是基础设施问题

核心判断标准:这个任务如果原样再跑一次,结果会不会不一样。会变——继续重试;不会变——直接死信,没必要浪费重试次数。

三、死信任务的数据结构

死信记录不能只存”失败了”三个字,至少要包含足够排查和重放的信息:

interface DeadLetterRecord {
  taskId: string;
  originalPayload: unknown;      // 原始任务输入,重放时需要
  failureReason: {
    stage: 'llm_call' | 'tool_call' | 'validation' | 'unknown';
    errorCode: string;
    errorMessage: string;
    retryCount: number;
  };
  executionTrace: {
    completedSteps: string[];    // 已经成功执行、产生副作用的步骤
    lastCheckpoint: string | null;
  };
  enqueuedAt: string;
  status: 'pending_review' | 'replayed' | 'discarded';
}

其中 executionTrace.completedSteps 是最容易被忽略但最关键的字段:Agent 任务往往是多步的,失败发生在第 3 步不代表前 2 步没有产生副作用(比如已经发出去一封邮件)。不记录这个字段,重放时要么重复执行已完成的步骤,要么无法判断从哪一步继续,这一点和 Saga 补偿机制处理多步失败时的思路是一致的。

四、死信任务怎么处理

进入 DLQ 之后,通常有三条出路:

  • 告警 + 人工介入:涉及资损、外部副作用(已扣款、已发送)的任务,必须触发告警而不是静默堆积。人工看完之后手动决定重放、丢弃还是走补偿流程。
  • 规则化自动重放:如果失败原因明确可修复(比如依赖服务当时不可用,现在已恢复),可以配一条规则在满足条件后自动重新入队,不需要每次都人工点一下。
  • 归档丢弃:确认任务已经没有业务价值(比如用户请求已经过期、数据源已失效),标记为 discarded 并归档,避免死信队列本身无限膨胀。

重放前必须做的一件事:检查 executionTrace,判断是从头重放还是从 lastCheckpoint 续跑。无脑从头重放,对于已经产生真实副作用的步骤(发消息、扣费、写外部系统),会造成重复执行——这也是死信重放和普通消息重试最大的差异点。

五、和传统消息队列 DLQ 的差异

如果你之前做过后端,可能对 RabbitMQ/SQS 的 DLQ 机制很熟悉,但直接照搬到 Agent 场景会漏掉几个关键差异:

  • 非确定性:传统消息重放,同样的输入基本能得到同样的处理逻辑;Agent 任务里 LLM 的输出本身就有随机性,“原样重放”不代表”结果一致”,重放更多是”给一次新的机会”而不是”复现同一个结果”。
  • 副作用已发生:传统消息队列的任务通常设计成幂等,重放是安全的;Agent 多步任务里间隔调用了外部工具,副作用可能已经不可逆,重放前必须像上一节说的那样先看执行轨迹。
  • 失败原因更难分类:网络层的失败传统系统很容易识别(超时、连接拒绝),但”LLM 输出格式不对""工具调用参数被模型编错了”这类失败,需要额外的分类逻辑才能判断是否值得重试,不能只看 HTTP 状态码。

六、常见误区

  • 把 DLQ 当成日志替代品:只写不读,死信堆积无人处理,等同于换了个地方消失,起不到实际作用。
  • 所有失败都无差别进 DLQ:瞬时性错误也塞进去,会让 DLQ 里塞满噪音,掩盖真正需要人工关注的任务。
  • 重放不检查副作用:这是最容易踩的坑,尤其在任务涉及资金、外部通知类操作时,重复执行的代价可能远大于任务失败本身。

七、相关阅读