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调度效果。
长短任务怎么排?
比较平均完成时间、尾延迟、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 重试、取消与预算放大
关联章节: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如何处理,取消在外部副作用上有什么边界。