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 里塞满噪音,掩盖真正需要人工关注的任务。
- 重放不检查副作用:这是最容易踩的坑,尤其在任务涉及资金、外部通知类操作时,重复执行的代价可能远大于任务失败本身。