跳到正文
Agent 工程实践任务调度与容错
返回课程/Agent 工程实践
第 12 / 15 章 约 7 分钟

任务调度与容错

处理调度、取消、超时、恢复与容错。

这个模块在 Agent 里的位置

这个模块决定 Agent 是否可停止、可恢复、可幂等。它影响:

  • 任务是否会挂死。
  • 用户取消后是否还有副作用。
  • worker 重启后是否重复执行。
  • 旧 worker 是否会覆盖新 worker。
  • 外部动作是否重复发生。

一个不能被可靠停止和恢复的 Agent,不能称为生产系统。

先记住几个概念

概念含义为什么重要
Model timeout单次模型请求超时控制单次调用
Tool timeout单次工具执行超时控制外部依赖
Run timeout整个逻辑任务超时控制总资源
AbortSignal取消传播信号停止模型和工具
Leaseworker 执行租约判断谁有权继续
Generation执行代数防止旧 worker 覆盖新 worker
Idempotency key外部动作幂等键防止重复执行

从一个真实问题开始:一个工具卡住,整个 run 挂了 40 分钟

一个 Agent 需要查询 30 亿行事件。

预期:

查聚合表 -> 2 秒

实际:

工具误扫 raw 表 -> 40 分钟

系统没有任何超时:

  1. 模型请求已结束。
  2. 工具还在执行。
  3. run 一直处于 running。
  4. 用户无法取消。
  5. worker 被这个任务占用。
  6. 队列开始堆积。

根因是只有模型超时,没有 tool timeout 和 run timeout。

三层超时必须分开

层语义示例
model call单次模型请求60s
tool call单次工具执行10s
run整个逻辑任务,含压缩和子任务10min

关键点:

  1. run timeout 不能被 compaction 重置。
  2. tool timeout 不能被子任务无限叠加。
  3. model timeout 不代表任务完成。
  4. 超时后必须有终态,不能永远 running。

取消不是改 UI 状态

用户点取消后,系统至少要做:

runtime 标记 cancelling
  -> abort model request
  -> abort tool signal
  -> stop subagents
  -> block pending action
  -> write terminal cancelled

如果只改 UI:

  1. 模型还在生成。
  2. 工具还在查库。
  3. 子任务还在跑。
  4. pending action 可能继续执行。

典型事故:

页面显示已取消,几分钟后用户收到一封任务完成邮件。

AbortSignal 必须传到工具

工具接口不能只有 input。

错误设计:

async function run(input: Input): Promise<Output>;

正确设计:

async function run(
    input: Input,
    context: {
        signal: AbortSignal;
        toolCallId: string;
    },
): Promise<Output>;

工具内部要:

  1. 把 signal 传给数据库或 HTTP client。
  2. 在长循环中检查 signal.aborted。
  3. 超时后清理临时资源。
  4. 返回稳定的 TIMEOUT 或 CANCELLED 错误。

worker 恢复与 fencing

场景:

  1. worker A claim 了 run。
  2. worker A 崩溃。
  3. lease 过期。
  4. worker B 接管。
  5. worker A 恢复。

如果 A 恢复后继续写结果,会覆盖 B 的状态。

正确做法:

worker A: generation=1
worker B: generation=2
A 恢复后只能写 immutable checkpoint
B 才能写当前结果

数据库字段:

alter table agent_runs
  add column timeout_at timestamptz,
  add column cancel_requested_at timestamptz,
  add column terminal_status text,
  add column terminal_at timestamptz;

create index agent_runs_timeout_idx
  on agent_runs (status, timeout_at)
  where status in ('running', 'compacting', 'cancelling');

恢复前要检查什么

恢复不是简单“继续跑”,至少要检查:

  1. lease 是否有效。
  2. generation 是否当前。
  3. checkpoint 是否完整。
  4. 外部动作是否已执行。
  5. run 是否已被用户取消。
  6. 是否已超过 run timeout。

一个恢复记录:

{
    "runId": "run-9f27",
    "recovery": {
        "generation": 4,
        "checkpointRef": "s3://checkpoints/run-9f27/gen-4.json",
        "canResume": true,
        "externalActionsExecuted": ["send-email-771"]
    }
}

如果外部动作已执行,不能简单重试。

外部动作必须有幂等键

动作表:

create table agent_actions (
  id bigint primary key,
  run_id bigint not null,
  action_key text not null,
  status text not null,
  result jsonb,
  created_at timestamptz not null,
  unique (run_id, action_key)
);

示例:

notify:run-9f27:owner:9182

只要 action key 相同:

  1. 网络超时后重试不会重复发邮件。
  2. worker 恢复后不会重复执行。
  3. 用户重复点击不会重复写数据。

超时后的重试策略

不能所有超时都自动重试。

错误是否重试原因
model timeout可重试一次可能是瞬时
tool timeout谨慎可能是查询设计错误
run timeout不自动重试成本会翻倍
permission denied不重试权限不会自动恢复
budget exceeded不重试预算不会恢复

重试前必须确认:

  1. 幂等。
  2. 预算允许。
  3. 生成代数仍当前。
  4. 用户没有取消。

和传统后端的关系

这套机制和后端任务系统高度同构:

Agent 里的概念传统后端里的对应物
Run异步 job
Workerjob worker
Lease租约 / 心跳
Generation版本号 / fencing token
Retry policy重试队列
Dead letter死信队列
Backpressure队列限流
Idempotency key幂等键

如果你做过消息队列、任务调度或分布式 worker,这一章的大部分经验都可以直接复用。

常见工程错误

错误实际后果正确做法
只有模型超时工具挂死三层 timeout
取消只改 UI后台继续执行AbortSignal 传播
恢复不校验 generation旧 worker 覆盖新 workerfencing
外部动作无幂等重复发通知 / 写数据action key
超时自动重试成本翻倍retry policy
run 永远 running任务挂死terminal status

上线前检查

  1. 三层 timeout 显式。
  2. cancel 传播到 model / tool / subagent / action。
  3. run 有 terminal status。
  4. 恢复有 generation。
  5. 外部动作幂等。
  6. 超时重试有策略。
  7. 取消事件可审计。