Lazy loaded image
Agent丨当 Agent 需要调用多个工具或工具之间存在依赖关系时,调度引擎应该如何设计?
Words 6875Read Time 18 min
2026-5-3
type
Post
status
Published
date
May 3, 2026
slug
agent28
summary
tags
Agent
category
icon
password
核心结论:
Agent 的工具调度引擎不能简单写成“LLM 返回多个 tool calls,然后 Promise.all 执行”。
真正可落地的调度引擎应该是:
模型决策层 + 工具依赖图 DAG + 权限/风险策略 + 资源锁 + Lane Queue + 可恢复执行日志 + 结果回注机制

1. 为什么多工具调度是 Agent 工程化的分水岭?

很多 Agent Demo 的工具调用流程是这样的:
这在单工具场景下没问题,例如:
但生产级 Agent 很快会遇到更复杂的任务:
这背后可能涉及:
这些工具之间有明显依赖关系:
如果调度引擎不理解依赖关系,直接并发执行,结果可能是灾难性的:
测试还没跑完就开始改代码;
文件还没读完就开始 edit;
多个 edit 同时改同一个文件导致 patch 冲突;
一个子 Agent 在跑测试,另一个子 Agent 同时删除依赖;
工具失败只被日志吞掉,LLM 误以为任务成功。
Claude Code 的 Agent SDK 文档把 Agent loop 描述为:模型评估当前状态,可能请求一个或多个工具调用,SDK 执行工具并把结果返回给模型,然后重复直到模型不再调用工具。官方文档还强调复杂任务可能跨越许多 turn,工具结果会驱动下一步决策。(Claude Code)
这说明:Agent 调度不是一次函数调用,而是一个持续运行的执行循环。

2. 从 Claude Code、OpenAI、OpenCode、OpenClaw 看成熟 Agent 的调度共性

2.1 Claude Code:一轮多工具调用是“无序”的

Claude 的 parallel tool use 文档非常关键:同一个 assistant turn 里出现的多个 tool calls 是无序的,应用层可以并发、串行或任意顺序执行;Claude 不会假设其中一个调用已经先于另一个完成;如果存在依赖,Claude 应该跨多个 turn 发起后续调用。(Claude Platform)
这给调度引擎一个非常重要的设计原则:
同一轮返回的多个工具调用,默认只能被视为互相独立。
如果工具 B 的参数依赖工具 A 的结果,B 不应该和 A 出现在同一个无序批次里。
调度引擎即使看到多个 tool calls,也要先做依赖和资源冲突检查,再决定是否并发。
OpenAI 的 function calling 文档也采用类似模式:模型产生 tool call,应用执行工具,再把 tool output 带着 call_id 返回给模型;模型可以在一轮里选择多个函数调用,也可以通过 parallel_tool_calls 关闭并行工具调用。(OpenAI开发者)

2.2 OpenCode:把子 Agent 当成 task 工具,但要有权限、后台任务和结果回注

OpenCode 的公开源码中,task 工具支持把某个任务交给指定 subagent 执行,并且包含几个非常值得借鉴的设计点:
第一,task 工具支持 background=true,把子任务异步启动,主线程可以继续工作。(GitHub)
第二,子 Agent session 会从父 session 和父 agent 派生权限,避免子 Agent 默认拥有过大的工具权限。(GitHub)
第三,如果同一个后台任务已经在 running,会直接失败并提示使用 task_status 查询,防止重复启动。(GitHub)
第四,后台任务完成后,会等父 session 进入 idle,再把结果注入主线程并触发后续 loop。(GitHub)
这说明成熟 Coding Agent 不会把“子 Agent”当成一个普通函数随便并发调用。它至少需要:

2.3 OpenClaw:Lane Queue 是调度引擎的基础设施

OpenClaw 的公开源码里有一个 command-queue 设计,注释明确写着这是一个用于序列化命令执行的 in-process queue。默认 main lane 保持原有串行行为,额外 lane 可以扩展不同执行通道。(GitHub)
它的 lane state 里包含:
默认 maxConcurrent 是 1,也就是每个 lane 默认串行执行。(GitHub)
OpenClaw 还维护 draining 状态、active task waiter、lane snapshot、queue size 等运行时状态,方便重启、背压、观测和故障处理。(GitHub)
这背后的工程思想是:
不要让所有工具调用都在一个全局 Promise 池里乱跑。
应该按 session、资源、风险等级、工具类型拆 lane。
默认串行,明确安全时才并发。

2.4 LangGraph 与 AutoGen:并发工具需要显式控制

LangGraph 的 ToolNode 是一个预置工具执行节点,官方文档说明它处理并行工具执行、错误处理和状态注入。(LangChain 文档)
AutoGen 文档则给出了一个很实际的警告:如果把 Agent 或 Team 包装成工具,必须关闭 parallel tool calls,因为 Agent 和 Team 维护内部状态,不能被并发调用,否则会产生并发冲突。(Microsoft GitHub)
这进一步说明:
并发不是越多越好。
对无状态、只读、幂等工具可以并发;
对有状态、有副作用、共享上下文的工具要串行或加锁。

3. 调度引擎的总体架构

推荐的生产级架构如下:
可以拆成七层:
作用
Tool Call Parser
从模型输出中解析工具名、参数、call_id
Schema Validator
校验参数 schema、strict mode、必填字段
Dependency Analyzer
判断工具之间是否有数据依赖、资源冲突
Policy Engine
判断权限、风险、是否需要用户确认
Scheduler
决定串行、并发、排队、重试、取消
Executor
真正执行 bash、read、edit、search、browser、MCP 等工具
Journal & Context Manager
记录执行状态,把结果压缩后回注给模型

4. 工具元数据:调度引擎必须知道“工具是什么”

很多系统只给 LLM 一份函数 schema:
这对模型来说够用,但对调度引擎不够。调度引擎还需要知道:
推荐设计一个内部 ToolSpec:
关键点是:
给模型看的工具定义,不等于给调度器看的工具定义。
模型只需要知道怎么调用;调度器必须知道怎么安全执行。

5. 并行、串行、DAG:调度决策怎么做?

5.1 同一轮多个工具调用:先判断是否独立

例如:
模型可能在一轮里发出:
这三个调用是:
因此可以并发执行。
但如果是:
即使模型在同一轮里返回了两个工具调用,调度器也不应该盲目并发,因为 edit 依赖 read 的内容,而且两个工具访问同一个文件资源。

5.2 工具依赖图 DAG

对于复杂任务,调度器可以把工具调用抽象成 DAG:
DAG 节点表示一次工具调用,边表示依赖。
节点状态机:
核心原则:
只有依赖全部完成、权限通过、资源锁拿到之后,节点才能从 READY 进入 RUNNING。

6. Lane Queue:比全局并发池更适合 Agent

推荐不要只有一个全局 executor,而是设计多条 lane:
一个工具调用会被映射到一个或多个 lane:
这样做的好处是:
OpenClaw 的 command queue 默认 maxConcurrent=1,正是这种“先串行保证正确性,再按 lane 放开并发”的思路。(GitHub)

7. 一个可落地的 TypeScript 调度器骨架

下面是一个简化版 DAG Scheduler。它不是某个项目源码的复制,而是根据上述工程模式抽象出的实现骨架。
调度主循环:
这个调度器做了几件关键事情:
不是所有 ready 节点都直接并发,而是先按资源冲突分 batch;
每个工具调用都必须经过 schema 校验、权限检查、资源锁;
失败不是简单 throw,而是结构化写入 ToolError;
可重试只对幂等工具开放;
依赖失败会向下游传播,避免后续工具拿不到输入却继续执行。

8. 依赖关系从哪里来?

通常有三种方式。

8.1 模型显式生成计划

让 LLM 先输出结构化计划:
优点是可解释、可审计。
缺点是模型可能生成错误依赖,所以调度器必须二次验证。

8.2 调度器根据工具参数推断

例如:
调度器可以根据资源冲突推断:
再比如:
如果参数里引用了上游变量,就可以自动建立依赖。

8.3 ReAct 多轮自然形成依赖

Claude 文档里提到,复杂任务会跨多个 turn 调用工具,模型根据上一轮工具结果调整下一步。(Claude Code)
这种模式下,依赖关系不是一次性完整 DAG,而是在多轮 loop 里逐步形成:
推荐策略是:
小任务使用 ReAct 自然多轮;
大任务先生成粗粒度 DAG,再在每个节点内部允许 ReAct 微调。

9. 权限与审批:调度引擎必须挡在工具前面

工具调用本质上是让 LLM 影响真实世界。尤其是这些工具:
都不能只靠 prompt 约束。
推荐设计 Policy Engine:
OpenCode 的 plan agent 默认只读、拒绝文件编辑、bash 需要确认;build agent 则是完整开发权限。(GitHub)
这类 agent mode 的本质就是:
不同 Agent 拥有不同工具权限集。
调度引擎根据 Agent mode、session、工具风险和用户授权共同决定是否执行。

10. 结果回注:不要把所有工具输出原样塞回上下文

工具结果需要结构化返回:
对于不同工具结果,采用不同策略:
工具类型
回注策略
read_file
小文件直接返回,大文件返回摘要 + range
grep/search
返回 top-k 命中,保留文件路径和行号
bash
返回 exit code、stderr 摘要、关键日志
test
返回失败测试名、错误栈、重现命令
browser
返回页面标题、关键文本、截图引用
database
返回 schema 化 JSON,限制行数
subagent
返回结论、证据、未完成项、artifact
OpenCode 的 compaction 逻辑里也体现了类似思想:完成的 tool 输出会被估算 token 并按策略裁剪,避免上下文被历史工具输出撑爆。(GitHub)

11. 多工具调度的典型场景设计

11.1 独立只读工具:并发

调度策略:
Google Vertex AI 的 parallel function calling 示例也展示了类似模式:模型建议对多个地点调用天气函数,应用可以收集多个函数响应后一次性返回给模型。(Google Cloud Documentation)

11.2 文件编辑:按文件资源串行

如果 D 和 E 修改不同文件,可以并发;如果两个 edit 都修改同一个文件,则必须串行。
资源锁可以这样设计:
关键注释:

11.3 子 Agent 调度:不要随便并发调用同一个 Agent 实例

当子 Agent 被包装成工具时,本质上是:
这个子 Agent 可能有自己的:
因此它不是无状态函数。
AutoGen 明确提醒:AgentTool 或 TeamTool 不能并发运行,因为 agent/team 维护内部状态,并发会冲突。(Microsoft GitHub)
推荐做法:

12. Prompt 怎么写?只能作为提示,不能替代调度器

可以在 system prompt 或 tool guidance 里加入:
但这只是“建议模型更好地输出工具调用”。
生产系统必须在调度器里硬性 enforce:
原因很简单:
Prompt 是软约束,调度器是硬边界。

13. 错误处理:工具失败必须回到模型,而不是只写日志

一个好的 ToolError 应该长这样:
LLM 收到后可以继续:
错误处理策略:
错误类型
处理方式
schema invalid
返回模型,让模型修正参数
permission denied
告诉用户或请求授权
timeout
返回 timeout,允许模型缩小范围或重试
transient network error
幂等工具可重试
write conflict
排队或重新读取后再 patch
dependency failed
下游节点 skipped
side-effect partial success
不自动重试,走补偿或人工确认
特别要注意:
对非幂等工具不要自动重试。
例如 send_email、payment、deploy、git push。
除非工具支持 idempotency key,否则重试可能造成重复发邮件、重复扣款、重复部署。

14. 可恢复执行日志:Agent 调度必须能从中断中恢复

生产级调度引擎应该有 journal:
它解决几个问题:
OpenClaw 的 queue state 中包含 draining、activeTaskIds、generation、lane snapshot 等字段,本质也是为了让运行时状态可管理、可观察、可安全重启。(GitHub)

15. 推荐的生产级调度策略

可以总结为一张规则表:
工具类型
默认策略
原因
search、weather、read-only API
可并发
无共享写资源
read_file
可并发,但限制文件大小
只读
grep/glob
可并发,但限制 IO
只读但可能重 IO
edit_file/write_file
同文件串行
避免 patch 冲突
bash
同 workspace 串行
shell 状态和文件系统共享
browser 操作
同 browser context 串行
页面状态共享
database read
可并发限流
防止压垮 DB
database write
按表/行/事务键串行
避免写冲突
send_email
人工确认 + 幂等键
外部副作用
deploy
人工确认 + 串行
高风险
subagent task
同 session 串行
agent 有内部状态
background task
独立 lane + task_id
避免阻塞主 loop

16. 最佳实践:从简单到复杂逐步演进

第一阶段:Turn-level executor

适合 MVP:

第二阶段:Resource-aware scheduler

加入:

第三阶段:DAG executor

加入:

第四阶段:Multi-agent runtime

加入:

第五阶段:生产级平台

加入:

17. 一个完整执行例子:修复测试失败

这里的关键点是:

18. 结论:调度引擎的本质是“把 LLM 的意图变成安全、可控、可恢复的执行”

LLM 负责判断“下一步想做什么”,但不应该直接决定“怎么并发、怎么加锁、怎么重试、怎么审批”。
一个好的 Agent 调度引擎应该做到:
让无依赖的只读工具尽可能并发,提高速度;
让有依赖、有副作用、有共享状态的工具严格串行或加锁;
让所有工具调用都有状态、有日志、有超时、有错误回传;
让权限和安全策略独立于 prompt;
让子 Agent、后台任务、长任务都能被追踪和恢复;
让工具结果经过压缩和结构化后再回到模型,而不是污染上下文。
最终可以用一句话概括:
Agent 调度引擎不是工具调用的 for 循环,而是一个面向 LLM 的工作流内核。
回到首页