题目重点:不同agent的任务可并行,但任何工具只能在全部依赖成功后启动。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- agents映射每个agent的本地task ID到依赖ID集合和无参callable;不跨agent依赖。训练使用共享thread pool,而非规定一个agent所有工具必须跑在同一物理线程。
- 依赖失败则后代skipped,其他独立任务继续;callable抛Exception标failed。任务图在开始前固定,循环/缺失依赖在任何副作用前拒绝。
- 全局limit限制同时执行的工具数,线程主要适合I/O型工具;Python CPU密集任务可另用进程/远端执行。示例无超时、运行中强制取消或持久化恢复。
解题路线
- 先对所有图做Kahn拓扑检查,再执行。运行时remaining计数归零的任务进入ready,只有全部依赖status=success才能submit;失败后仍推进依赖计数,让后代被明确标skipped而非永久等待。
- 调度线程统一修改状态,worker只调用工具并返回结果。running future数不超limit;wait FIRST_COMPLETED推进依赖,不让worker持有slot去等待还未提交的子任务,避免线程池死锁。
- 从单agent到多agent只需把key拓展成(agent,task),所有ready共享pool,因此limit是全局而不是每agent各一个limit。每agent公平需轮转ready队列/配额,当前FIFO不承诺公平延迟。
- 动态增加tool call、重试和cancel需要显式任务状态机/attempt ID。Python future.cancel不能终止已运行线程;外部shell任务应有进程组和协作取消协议,取消后仍要回收日志和资源。
手动走一遍
a依赖b,b失败:b failed、a skipped;另一个agent的c无依赖仍成功。limit2,两个agent各有独立工具,用threading.Barrier证明确实同时运行;所有任务依赖开始顺序、上限和失败传播经904项真实线程核验。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
from concurrent.futures import ThreadPoolExecutor,wait,FIRST_COMPLETED
from collections import deque
def schedule(agents,limit):
# agents[agent][task] = (local_dependency_ids, zero-argument callable)
if not isinstance(limit,int) or limit<1:raise ValueError('positive worker limit')
tasks={};children={};remaining={}
for agent,specs in agents.items():
for task,(deps,fn) in specs.items():
key=(agent,task);deps=set(deps)
if not callable(fn) or any(d not in specs for d in deps):raise ValueError('task/dependency')
tasks[key]=(set((agent,d) for d in deps),fn);remaining[key]=len(deps);children[key]=[]
for key,(deps,_) in tasks.items():
for dep in deps:children[dep].append(key)
# Validate the entire graph before executing any side effect.
counts=remaining.copy();queue=deque(k for k,v in counts.items() if not v);visited=0
while queue:
key=queue.popleft();visited+=1
for child in children[key]:
counts[child]-=1
if not counts[child]:queue.append(child)
if visited!=len(tasks):raise ValueError('dependency cycle')
ready=deque(k for k,v in remaining.items() if not v);status={};results={};running={}
def finish(key,state,value=None):
status[key]=state;results[key]=value
for child in children[key]:
remaining[child]-=1
if not remaining[child]:ready.append(child)
with ThreadPoolExecutor(max_workers=limit) as pool:
while ready or running:
while ready and len(running)<limit:
key=ready.popleft();deps,fn=tasks[key]
if any(status[d]!='success' for d in deps):finish(key,'skipped');continue
running[pool.submit(fn)]=key
if not running:continue
done,_=wait(running,return_when=FIRST_COMPLETED)
for future in done:
key=running.pop(future)
try:finish(key,'success',future.result())
except Exception as error:finish(key,'failed',error)
return status,results
复杂度
V任务E依赖,预检与调度元数据O(V+E),空间O(V+E+limit)。实际工期取决于任务时间和DAG,至少max(关键路径,总工作/limit),FIFO不承诺最优makespan。
易错点与追问
容易失分的边界
- 在worker里等待同pool其他future会耗尽slot。
- 每agent各创建limit线程会把全局并发放大到agent数×limit。
- 任务执行之后才发现环会留下无法撤销的副作用。
继续深挖
- 动态任务依赖如何禁止环并让重试不重复外部副作用?
- 不同agent优先级与公平配额怎样避免一个大图占满ready队列?
