告别大模型连环网络等待:用 DAG 异步调度实现 Agent 多工具并发执行
配套代码:
../v3_rewoo/代码地址:撕开黑盒学大模型-从白盒状态机演进到工业级Agent框架
本文目标:用一个克制版 Plan-then-Execute Demo 说明如何把串行工具调用改造成 DAG 异步调度。
文章目录
- [告别大模型连环网络等待:用 DAG 异步调度实现 Agent 多工具并发执行](#告别大模型连环网络等待:用 DAG 异步调度实现 Agent 多工具并发执行)
-
- [1. 问题与背景](#1. 问题与背景)
- [2. 计划文本:把隐式步骤显式化](#2. 计划文本:把隐式步骤显式化)
- [3. 依赖解析:找出哪些节点可以并发](#3. 依赖解析:找出哪些节点可以并发)
- [4. 异步执行与超时 Observation](#4. 异步执行与超时 Observation)
- [5. 运行与验证](#5. 运行与验证)
- [6. 时间线可视化](#6. 时间线可视化)
- [7. 风险边界与生产化迁移](#7. 风险边界与生产化迁移)
- [8. 总结](#8. 总结)
- [9. 环境与复现范围](#9. 环境与复现范围)
- 参考资料
1. 问题与背景
ReAct 的优势是简单:模型每一步决定下一步做什么,程序执行工具,然后把 Observation 写回上下文。
但这种模式在多工具任务里有明显问题。假设任务需要:
- 查询 GPU 价格;
- 查询杭州天气;
- 查询库存状态;
- 汇总出门采购建议。
传统串行模式可能是:
text
模型判断 -> 查价格 -> 模型判断 -> 查天气 -> 模型判断 -> 查库存 -> 模型汇总
如果每一步都涉及网络等待,总耗时会不断累加。很多工具之间其实没有依赖关系,却被迫串行。
v3_rewoo/ 演示的是一种简化版 ReWOO 思路:先规划,再执行。Planner 一次性生成包含依赖关系的计划,Scheduler 再根据依赖图并发执行无依赖节点。
2. 计划文本:把隐式步骤显式化
planner.py 生成的计划文本如下:
text
E1: fetch_price[GPU]
E2: fetch_weather[杭州]
E3: unstable_inventory[GPU]
E4: summarize[结合 #E1、#E2、#E3 给出出门采购建议]
这里有两个关键点:
- 每个步骤都有唯一 ID,例如
E1、E2; - 后续步骤可以用
#E1引用前置步骤结果。
parser.py 负责把计划文本解析成结构化 PlanStep:
python
PLAN_LINE_RE = re.compile(
r"^(?P<step_id>E\d+):\s*(?P<tool>\w+)\[(?P<argument>.*)\]\s*$"
)
这一步看起来简单,但很重要。只要计划文本被解析成结构化数据,后续就可以做依赖分析、并发调度、超时控制和可视化。
3. 依赖解析:找出哪些节点可以并发
scheduler.py 中用正则查找参数里的依赖占位符:
python
DEPENDENCY_RE = re.compile(r"#(?P<step_id>E\d+)")
def dependencies(step: PlanStep) -> set[str]:
return set(DEPENDENCY_RE.findall(step.argument))
对于上面的计划:
E1没有依赖;E2没有依赖;E3没有依赖;E4依赖E1、E2、E3。
所以 Scheduler 可以先并发执行 E1、E2、E3。等它们都有结果后,再执行 E4。
参数替换由 resolve_argument() 完成:
python
def resolve_argument(argument: str, results: dict[str, str]) -> str:
for step_id, value in results.items():
argument = argument.replace(f"#{step_id}", value)
return argument
这就是 DAG 调度的核心:不是让模型一步一步等,而是把依赖关系显式化。
4. 异步执行与超时 Observation
并行执行用的是 asyncio.gather:
python
completed = await asyncio.gather(*(run_step(step) for step in ready))
每个工具调用都包了一层超时控制:
python
async def call_tool(step: PlanStep, argument: str, timeout: float) -> str:
return await asyncio.wait_for(TOOLS[step.tool](argument), timeout=timeout)
如果工具超时,不直接让整个任务崩溃,而是返回错误 Observation:
python
except asyncio.TimeoutError:
value = f"ERROR: {step.tool} timed out after {timeout}s"
return step.step_id, value
这体现了一个重要工程原则:Agent 的工具失败应该变成可见事实,而不是吞掉或直接中断。后续节点可以看到这个失败,并在最终回答中说明限制。
5. 运行与验证
运行:
powershell
python v3_rewoo\benchmark.py
一次验证输出如下:
text
serial ReAct-like execution: 1.619s
E1: GPU 预算价格区间已获取
E2: 杭州 小雨,建议带伞
E3: ERROR: unstable_inventory timed out after 0.6s
E4: 综合结论:结合 GPU 预算价格区间已获取、杭州 小雨,建议带伞、ERROR: unstable_inventory timed out after 0.6s 给出出门采购建议
parallel ReWOO-like execution: 0.820s
E1: GPU 预算价格区间已获取
E2: 杭州 小雨,建议带伞
E3: ERROR: unstable_inventory timed out after 0.6s
E4: 综合结论:结合 GPU 预算价格区间已获取、杭州 小雨,建议带伞、ERROR: unstable_inventory timed out after 0.6s 给出出门采购建议
这个结果说明两件事:
- 并发调度确实降低了端到端等待;
- 超时工具没有阻塞最终汇总,而是作为错误 Observation 进入结果。
注意,具体耗时会随机器负载略有变化,文章中不应该把某一次数字写成绝对性能结论。更稳妥的表述是:在本 demo 的模拟延迟条件下,并行版本明显短于串行版本。
这组耗时来自 worker.py 中的确定性模拟延迟:
| 工具 | 模拟耗时 | 调度含义 |
|---|---|---|
fetch_price |
0.4s | 可独立执行 |
fetch_weather |
0.4s | 可独立执行 |
unstable_inventory |
0.8s,但被 0.6s 超时截断 | 失败应回写 Observation |
summarize |
0.2s | 必须等待 E1、E2、E3 |
因此,串行版本接近 0.4 + 0.4 + 0.6 + 0.2 = 1.6s,并行版本接近 max(0.4, 0.4, 0.6) + 0.2 = 0.8s。这不是泛化性能承诺,而是用可控延迟验证"无依赖节点可以重叠执行"这个调度事实。
6. 时间线可视化
运行后会生成:
text
v3_rewoo/trace.json
打开:
text
v3_rewoo/visualization.html
可以看到串行和并行两组事件:
- start;
- success;
- timeout;
- 每个事件的 timestamp;
- 每个节点的工具名和参数。

时间线对理解调度非常重要。没有时间线时,读者只能看到总耗时;有时间线后,读者能看到哪些节点重叠执行,哪些节点等待依赖。
运行后可以直接检查 trace.json,确认并行分支不是口头描述:
json
{
"runs": [
{ "label": "serial ReAct-like execution", "elapsed": 1.632 },
{ "label": "parallel ReWOO-like execution", "elapsed": 0.809 }
],
"timeline": [
{ "runner": "parallel", "step_id": "E1", "event": "start" },
{ "runner": "parallel", "step_id": "E2", "event": "start" },
{ "runner": "parallel", "step_id": "E3", "event": "start" },
{ "runner": "parallel", "step_id": "E3", "event": "timeout" },
{ "runner": "parallel", "step_id": "E4", "event": "start" }
]
}
这里最值得关注的不是加速倍率,而是事件顺序:并行版本里 E1、E2、E3 同时 start,E4 在依赖节点结束后才 start。这说明调度器确实按依赖图执行,而不是简单把所有工具一起扔进 gather。
7. 风险边界与生产化迁移
当前实现是克制版 Plan-then-Execute,不包含复杂生产能力。它刻意只保留计划解析、依赖分析、并发执行、超时 Observation 和时间线,避免把调度核心淹没在框架细节里。
生产环境至少还要补齐这些边界:
| 边界 | 当前 Demo | 生产系统需要补齐 |
|---|---|---|
| 循环依赖 | 发现无 ready 节点后抛错 | 在入队前校验 DAG,给 Planner 反馈可修复错误 |
| 工具失败 | 转成 ERROR: ... Observation |
区分可重试、不可重试、可降级、需人工介入 |
| 超时控制 | 每个工具统一 0.6s |
按工具 SLA 配置超时、重试次数和熔断策略 |
| 取消传播 | 未实现 | 用户取消或上游失败时,取消未完成分支并清理资源 |
| 幂等控制 | 未实现 | 为写操作增加 request id、去重键和补偿逻辑 |
| 持久化 | 只写 trace.json |
checkpoint、任务状态机、恢复执行、审计日志 |
| 可观测性 | 本地时间线 | trace id、span、指标、错误分类、耗时分布 |
迁移到工业级 Agent 框架时,不要只问"能不能并发"。更关键的问题是:并发分支失败后怎么恢复?工具是否幂等?超时结果是否应该参与最终总结?状态是否能从 checkpoint 恢复?这些问题决定了调度器能不能进生产。
一个更稳的落地路径是:
- 先把计划解析成结构化 DAG,不允许隐式字符串依赖到处散落。
- 在调度前做 DAG 校验,阻断循环依赖、未知工具和未定义变量。
- 为每个工具声明超时、重试、幂等、是否可降级。
- 把每次执行写入 trace,用于复盘"哪个节点等待了谁"。
- 再接入持久化 checkpoint,支持失败恢复和人工审计。
本文 demo 只完成第 1 到第 4 步的最小闭环。它适合作为理解 ReWOO / DAG 调度的白盒模型,不应该直接当成生产编排引擎。
8. 总结
ReAct 适合建立 Agent 工具调用认知,但它天然偏串行。面对多工具任务,可以把"模型逐步决定"改造成"模型先规划,调度器再执行"。
DAG 异步调度的关键不是 asyncio.gather 这一行代码,而是四个工程动作:
- 把计划结构化;
- 解析依赖;
- 并发执行无依赖节点;
- 把失败转成 Observation。
第 3 期的重点不是宣称 Plan-then-Execute 一定优于 ReAct,而是给读者一套可观察的判断方法:看计划是否结构化,看依赖是否显式,看无依赖节点是否重叠执行,看失败是否进入最终上下文。理解这套机制后,再看 LangGraph 的并行分支、持久化执行或更复杂 Agent 编排,就会更容易判断框架能力边界。
9. 环境与复现范围
本文配套代码是一个无第三方服务依赖的教学 Demo,用于复现 DAG 调度和超时 Observation,不依赖真实网络 API。
| 项目 | 说明 |
|---|---|
| 操作系统 | Windows 10/11、macOS、Linux 均可 |
| Python | 建议 Python 3.10+ |
| 第三方依赖 | 无,使用 Python 标准库 asyncio、json、re、time |
| 运行目录 | 仓库根目录或 v3_rewoo/ 目录 |
| 输出文件 | v3_rewoo/trace.json |
| 可视化页面 | v3_rewoo/visualization.html |
如果从仓库根目录运行:
powershell
python v3_rewoo\benchmark.py
如果已经进入 v3_rewoo/ 目录运行:
powershell
python benchmark.py
运行后会刷新 trace.json。再打开 visualization.html,可以看到串行和并行两组时间线。
参考资料
- ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models:ReWOO 将推理过程和工具 Observation 解耦,本文借用的是"先规划、再执行"的核心思想。
- LangGraph Workflows and Agents:包含 parallelization、routing、orchestrator-worker 等编排模式,可用于理解并行分支和聚合。
- LangGraph Persistence:说明 checkpoint、thread、short-term memory 等持久化概念。
- Python
asyncioTasks:gather()与wait_for()是本文 demo 并发和超时控制的标准库基础。
下一篇:小z疯狂码字ing...
感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!
