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 的工作流内核。
分享
