a↗Agent 工程实战中文课程 / v1.0.0⌕ 搜索课程/
本页目录

M08 并发、调度、重试与取消

本章产出:有界并发、显式排队、公平性和可解释取消语义。目标不是“并发数越大越快”,而是在限制内稳定完成任务。

1. 当一个快Demo变成一千个任务

单个任务只需几秒,不代表一千个任务同时到来仍只需几秒。模型限流、数据库连接、CPU、网络和工具API都有各自容量。无限创建协程可以把等待压力从队列转移到内存,并不会创造容量。

先区分入队任务数、活跃任务数、正在调用模型的数量、正在执行工具的数量与等待人工的数量。等待审批的任务应该只占持久状态,不占模型并发额度。

2. Little定律给你数量级,不给你万能容量规划

在稳定系统、统计口径一致的条件下,平均系统内任务数L、到达率λ与平均停留时间W满足L=λW。例如每秒接收2项任务,平均从进入到离开为5秒,平均在系统内约10项。

这10项不是“需要10个线程”的结论,也不直接给出P95。若长期到达率超过处理能力,队列持续增长,不再满足稳态假设。若W包含人工等待,L会很大,但执行资源未必同样大。

对Agent,模型与工具多次交替使资源需求随阶段变化。一个run级大Semaphore易于实现,但可能在等待工具时霸占模型配额。更细粒度的模型并发、工具并发、租户预算有时更有效,需要测量复杂度是否值得。

3. 并行什么时候保持语义

同一订单的“查询物流”和“读取当前政策”在独立输入、无共享写入时可以并行。计算退款资格依赖它们的结果,不能提前执行。两个写操作若争用同一余额,不能仅因模型同时提出就并行。

把工具调用看成依赖图,只有前置依赖已满足的节点可以进入就绪队列。写冲突、审批和预算也是依赖,不只是数据依赖。并行工具返回后应按call_id关联,不能按完成顺序误配。

Python的TaskGroup提供结构化并发,子任务失败会影响同组任务;asyncio取消是协作式控制,不会自动停止已经在远程系统执行的请求。S28

4. 排队策略是有代价的选择

三个任务在0时刻到达,运行时长为8、1、1。单Worker FIFO完成时刻为8、9、10,平均9;已知时长的短任务优先完成时刻为1、2、10,平均约4.33。这个例子展示队头阻塞,但实际系统通常不知道真实时长。

用估计时长做调度会有预测误差,长期短任务优先还会饿死长任务。可以引入等待老化、每租户轮转或加权公平策略,并报告长任务与小租户的尾延迟,不只报告全局均值。

本课程模拟器的运行时长是公开输入,因此其SJF结果是教学上界对照,不能当成真实Agent调度效果。

I16 · 互动推演独立打开 ↗

长短任务怎么排?

比较平均完成时间、尾延迟、makespan及公平性。

合成教学数据 · 未运行真实模型
静态推演与完整规格

I16 长短任务怎么排?

关联章节:M08 · 交互类型:scheduler · 状态:提供纯逻辑参考函数,UI待实现

教学目标

比较平均完成时间、尾延迟、makespan及公平性。

初始状态

{
  "jobs": [
    {
      "id": "long",
      "arrival": 0,
      "duration": 8
    },
    {
      "id": "s1",
      "arrival": 0,
      "duration": 1
    },
    {
      "id": "s2",
      "arrival": 0,
      "duration": 1
    }
  ],
  "policy": "fifo",
  "workers": 1
}
操作控件
  • FIFO/SJF切换

  • 修改到达/时长

  • worker数量1–4

  • 单步时间线

  • 比较表格

  • 重置

状态变化规则
  • 非抢占调度,只有ready任务可选择。

  • 时长已知仅为教学假设;相同时按到达/输入顺序。

  • 显示每任务等待/开始/结束、平均完成时间、makespan。

黄金案例与反馈
fifo

输入:


{
  "policy": "fifo",
  "workers": 1
}

期望:


{
  "end_times": [
    8,
    9,
    10
  ],
  "mean_completion": 9,
  "makespan": 10
}

短任务被长任务阻塞。

sjf

输入:


{
  "policy": "sjf",
  "workers": 1
}

期望:


{
  "end_times": [
    1,
    2,
    10
  ],
  "mean_completion": 4.333333333333333,
  "makespan": 10
}

平均改善但总工作量没变。

parallel

输入:


{
  "policy": "fifo",
  "workers": 2
}

期望:


{
  "makespan": 8
}

并发不是无限资源;可加租户额度讨论。

完成条件
  • 预测并核对两策略

  • 指出SJF的时长预测与饥饿问题

反馈层次
  • 提交预测后显示结果;允许跳过预测但记录跳过,不阻止阅读

  • 结果同时显示状态变化、原因与被拒绝动作;不是只给红绿灯

  • 第一次错误给边界提示,第二次提供最小反例;参考推导始终可主动打开

  • 重置恢复固定初始状态并保留历史尝试供用户手动清除

无障碍与静态替代
  • 全部操作有原生button/input/select替代,不依赖拖拽

  • 图与动画提供同步文字状态表;支持prefers-reduced-motion

  • 状态改变用aria-live=polite摘要,不逐字朗读长日志

  • 焦点不跳走;错误提示关联输入;颜色不是唯一编码

无JavaScript时直接展示初始输入、规则和黄金案例表,读者仍能完成纸面推演。状态动画仅作解释,不能成为获取答案的唯一途径。

持久化与重置

使用版本化键agent-course:1:I16,仅保存本地学习状态。支持单卡重置和全站清除;存储失败时以内存模式继续,不阻断阅读。

必须避免的误导

不要把已知真实duration的oracle比较称作可直接部署的最优调度器。

执行边界

纯本地教学状态机;不执行任意代码,不连接真实资金/模型/宿主Shell。

逻辑参考

web/reference/engines.mjs中的schedule;对应测试在web/reference/engines.test.mjs。UI可复用或等价移植,但不得改变黄金结果。

5. 并发限制、速率限制、预算限制不是一回事

并发限制约束同一时刻正在执行的请求数;速率限制约束一个时间窗口或令牌桶中的请求/token消耗;预算限制约束一项任务或租户累计能花多少资源。

一个请求可能需要先预留最大可能费用,再按实际usage结算释放余额。若多个Worker分别读“还剩100”后各花80,没有原子预留就会超预算。真实usage不可得时要明确估算策略和对账机制,不能用零填补缺失值。

模型输入token可以预估,输出token需要设置上限。工具费用、sandbox时间与重试费用也应计入预算。只限制LLM调用次数会放过一次巨大的工具输出或长时间子进程。

6. 重试风暴如何产生

假设三层调用各自最多尝试3次,最坏可能放大到27次底层尝试。每一层都自认为“只重试两次”,组合后却耗尽配额。

重试应集中在明确拥有恢复语义的一层,采用总截止时间和总尝试预算。退避与随机抖动避免大量任务在同一时刻再次冲击服务;但抖动不是权限错误、格式错误或业务否决的修复方法。

对于读工具的暂时错误,可以在预算内重试;对写工具结果未知,先核对同一operation,不生成新操作。对429等错误需遵守实际服务返回的限制信息,并防止SDK内部重试与应用层重试叠加。幂等语义仍是重试的前提。S29

7. 取消应沿任务树传播

run取消后,停止创建新模型请求和工具任务;取消可取消的本地协程;关闭结果消费者;对sandbox子进程发终止信号,超过宽限期后强制停止;回收租约和临时资源。每一步都要区分“已请求”和“已确认”。

将阻塞函数放进线程并不能保证取消协程时线程停止。HTTP请求超时也不能保证服务器没执行。对子进程还要考虑其派生进程,不能只杀一个父PID就认为全部结束。

若取消到来时退款已提交,则run可能进入RECONCILING,待确认结果后给出准确状态。取消传播与业务结果核对是两个协作流程。

I17 · 互动推演独立打开 ↗

重试、取消与预算放大

看清多层重试乘法和取消不等于回滚。

合成教学数据 · 未运行真实模型
静态推演与完整规格

I17 重试、取消与预算放大

关联章节:M08 · 交互类型:retry-budget · 状态:提供完整状态/验收规格,UI待实现

教学目标

看清多层重试乘法和取消不等于回滚。

初始状态

{
  "layers": [
    3,
    3,
    3
  ],
  "cancel_requested": false,
  "external_state": "NOT_SENT"
}
操作控件
  • 调整三层最大尝试次数1–5

  • 计算最坏请求数

  • 在提交前/后取消

  • 选择用户可见状态

  • 重置

状态变化规则
  • 每层独立可重试时,最坏尝试数为乘积,不是总和。

  • 取消后禁止新的动作,已发外部请求可能继续;结果未知时进入RECONCILING。

黄金案例与反馈
amplification

输入:


{
  "layers": [
    3,
    3,
    3
  ]
}

期望:


{
  "max_leaf_attempts": 27
}

最大重试数与最大尝试数必须区分;本输入明确是尝试次数。

cancel-before

输入:


{
  "external_state": "NOT_SENT",
  "cancel": true
}

期望:


{
  "state": "CANCELLED"
}

可以在未派发时终止。

cancel-after

输入:


{
  "external_state": "SENT_UNKNOWN",
  "cancel": true
}

期望:


{
  "state": "RECONCILING"
}

不能告诉用户资金变化已回滚。

完成条件
  • 正确算乘积

  • 区分取消请求与终止/回滚

反馈层次
  • 提交预测后显示结果;允许跳过预测但记录跳过,不阻止阅读

  • 结果同时显示状态变化、原因与被拒绝动作;不是只给红绿灯

  • 第一次错误给边界提示,第二次提供最小反例;参考推导始终可主动打开

  • 重置恢复固定初始状态并保留历史尝试供用户手动清除

无障碍与静态替代
  • 全部操作有原生button/input/select替代,不依赖拖拽

  • 图与动画提供同步文字状态表;支持prefers-reduced-motion

  • 状态改变用aria-live=polite摘要,不逐字朗读长日志

  • 焦点不跳走;错误提示关联输入;颜色不是唯一编码

无JavaScript时直接展示初始输入、规则和黄金案例表,读者仍能完成纸面推演。状态动画仅作解释,不能成为获取答案的唯一途径。

持久化与重置

使用版本化键agent-course:1:I17,仅保存本地学习状态。支持单卡重置和全站清除;存储失败时以内存模式继续,不阻断阅读。

必须避免的误导

不通过前端setTimeout模拟“真实进程已被杀死”。

执行边界

纯本地教学状态机;不执行任意代码,不连接真实资金/模型/宿主Shell。

8. 过载时拒绝比排无限长队更诚实

系统可以限制排队长度、按租户配额拒绝新任务、给出可重试响应和预计状态。这里的“预计”不能假装精确。任务具有截止时间时,应避免执行已不可能有用的工作。

背压需要向上游传递:工具变慢使就绪队列增长,协调器减少发起新任务,API适当拒绝,而不是每层继续无限缓存。观察队列年龄比只看队列长度更能发现被长期忽视的任务。

9. 实验与岗位表达

L07先用确定性调度器比较FIFO与SJF,再为sandbox任务加入CPU/输出/时间约束。测量平均完成时间、最差等待时间、每租户分布和取消后资源回收。

面试回答不能停在“用了asyncio和Redis”。你应该说清楚:资源在哪里受限,为什么选择这个调度粒度,重试由哪层负责,旧Worker如何处理,取消在外部副作用上有什么边界。

做当章练习 →