Agent 并行到底由谁决定?LLM 规划与程序调度的职责边界
在给 AI Agent 加入并发能力时,一个很容易被忽略的问题是:
哪些工具和任务可以并行,到底是 LLM 自己判断,还是 Java 程序判断?
答案不是二选一。
在一套合理的 Agent 系统中,通常由 LLM 负责语义规划,程序负责确定性校验和并发调度,高风险场景再交给人类审批。
简单来说:
LLM:理解任务之间的业务关系
程序:根据明确规则决定是否真正并发
人类:处理高风险、冲突和不确定情况
如果完全依赖 LLM,系统可能因为模型漏写依赖而错误并发;如果完全依赖普通程序,程序又很难从自然语言中判断“生成配置”和“启动服务”之间存在语义依赖。
真正可靠的方案是把两者结合起来。
一、为什么程序不能完全自己判断?
普通调度器擅长处理结构化信息,例如:
{
"id": "task_3",
"dependencies": ["task_1", "task_2"]
}
程序看到 dependencies 后,可以确定:
task_1、task_2 没完成
→ task_3 不能执行
但程序很难直接理解自然语言中的隐含关系。
例如:
任务 A:生成 application.yml
任务 B:根据 application.yml 启动项目
如果没有人显式告诉程序 B 依赖 A,普通线程池并不知道两者存在先后关系。
LLM 擅长理解这种语义,因此适合负责:
- 把用户目标拆成子任务;
- 判断任务输入和输出;
- 识别隐含依赖;
- 生成初步执行计划;
- 为依赖关系提供原因。
但是 LLM 的判断具有概率性。它可能漏掉依赖、误解资源范围,或者把两个有冲突的操作放进同一批。因此,LLM 的输出应该被视为“调度建议”,不能直接等同于安全规则。
二、为什么不能完全交给 LLM?
假设系统提示词告诉模型:
互不依赖的工具可以放在同一轮;
存在依赖的工具必须分多轮调用。
大多数时候模型能够遵守,但不能保证每次都正确。
例如,模型可能在同一轮返回:
write_file("config.json")
read_file("config.json")
程序如果把同一轮的工具全部并发执行,就可能出现:
read_file 先执行
→ 文件不存在或读到旧内容
write_file 后执行
→ 文件才被创建
又例如:
write_file("application.yml", contentA)
write_file("application.yml", contentB)
两个任务没有显式数据依赖,却存在写写冲突,最终文件内容取决于哪个线程最后完成。
因此,以下判断不能只依靠 LLM:
- 两个工具是否操作同一路径;
- 是否读写同一数据库记录;
- 是否操作同一个 Git 分支;
- 是否超过 API 并发限制;
- 是否需要独占资源;
- 是否属于危险操作;
- 是否需要人工审批。
这些都应该由程序使用确定性规则校验。
三、ReAct 模式:主要由 LLM 决定调用分组
ReAct 的基本循环是:
思考
→ 生成工具调用
→ 执行工具
→ 读取结果
→ 继续思考
当 LLM 在同一轮返回多个 tool_calls 时,系统可能把它们并发执行。
例如:
[
{
"id": "call_1",
"name": "read_file",
"arguments": {"path": "pom.xml"}
},
{
"id": "call_2",
"name": "read_file",
"arguments": {"path": "README.md"}
},
{
"id": "call_3",
"name": "read_file",
"arguments": {"path": "ROADMAP.md"}
}
]
三个文件读取互不依赖,适合并发:
┌→ pom.xml ────┐
Agent 工具批次 ──├→ README.md ──┼→ 结果汇总
└→ ROADMAP.md ─┘
在很多入门实现中,系统采用一个简单规则:
同一轮多个 tool_calls
→ 默认可以并行
这里实际上是 LLM 在做第一次判断。因为 LLM 决定了哪些工具被放进同一轮。
Java 程序只负责:
- 将工具调用包装成任务;
- 提交线程池;
- 限制最大并发数;
- 等待结果;
- 处理超时;
- 按调用 ID 返回结果。
这种实现简单,但存在风险:程序没有确认这些工具是否真的独立。
更安全的 ReAct 并行策略
可以采用分级规则:
多个只读工具
→ 默认允许并行
多个写工具
→ 检查目标资源后再决定
Shell、删除、部署等高风险工具
→ 默认串行或进入 HITL
例如:
| 工具组合 | 默认策略 |
|---|---|
| 读取 A + 读取 B | 并行 |
| 搜索代码 + 读取文档 | 并行 |
| 写 A + 写 B | 路径不同且无共享资源时并行 |
| 写 A + 读 A | 串行 |
| 删除目录 + 读取目录 | 串行并审批 |
| 执行两个未知 Shell 命令 | 保守串行 |
四、Plan-and-Execute:LLM 生成依赖,程序执行 DAG
Plan-and-Execute 模式会先生成一个结构化计划。
例如:
[
{
"id": "task_1",
"description": "读取 pom.xml",
"dependencies": []
},
{
"id": "task_2",
"description": "读取 README.md",
"dependencies": []
},
{
"id": "task_3",
"description": "生成项目分析报告",
"dependencies": ["task_1", "task_2"]
}
]
这里的职责分工非常清楚。
LLM 负责
- 将用户目标拆成任务;
- 判断任务需要哪些输入;
- 声明依赖关系;
- 解释为什么存在依赖。
Java 调度器负责
- 校验依赖任务是否存在;
- 检查 DAG 是否有环;
- 计算可执行批次;
- 并发执行同一批任务;
- 等待任务完成;
- 处理失败、重试和超时;
- 阻止依赖失败的下游任务。
程序计算后的批次为:
第一批:[task_1, task_2]
第二批:[task_3]
运行过程是:
task_1 ─┐
├→ task_3
task_2 ─┘
程序并没有重新理解“为什么 task_3 依赖前两个任务”,只是机械地执行 LLM 提供的 DAG。
LLM 漏写依赖怎么办?
假设 LLM 错误生成:
{
"id": "task_3",
"description": "生成项目分析报告",
"dependencies": []
}
调度器会认为 task_3 可以立即执行,于是三个任务被放进同一批。
因此,生产系统可以增加二次校验:
- 任务输入是否引用其他任务输出;
- 多个任务是否写入相同资源;
- 下游任务是否出现“基于、根据、汇总、验证”等依赖表达;
- 是否存在没有来源的输入;
- 是否存在循环依赖;
- 任务声明的读写集合是否冲突。
对于高价值计划,可以让第二个 Reviewer Agent 审核 DAG,但最终仍应由程序执行硬规则校验。
五、Multi-Agent:编排器规划,Worker 池控制执行
Multi-Agent 中通常存在:
Manager / Orchestrator
├── Worker 1
├── Worker 2
├── Worker 3
└── Reviewer
编排器负责将目标拆成多个步骤,并声明依赖:
[
{
"id": "step_1",
"description": "分析后端代码",
"dependencies": []
},
{
"id": "step_2",
"description": "分析前端代码",
"dependencies": []
},
{
"id": "step_3",
"description": "汇总前后端问题",
"dependencies": ["step_1", "step_2"]
}
]
程序发现 step_1 和 step_2 无依赖,于是将它们分配给不同 Worker:
worker-1 → step_1
worker-2 → step_2
Worker 池负责:
- 限制最多同时运行多少个 Agent;
- 确保同一个 Worker 不被两个步骤并发占用;
- 没有空闲 Worker 时让任务等待;
- Worker 完成后清理状态并归还;
- 控制总体 LLM API 并发量。
Worker 池不会理解两个任务是否存在业务冲突。它只知道:
当前有几个空闲 Worker
当前有哪些任务被标记为可执行
因此,Multi-Agent 仍然是:
LLM/编排器:拆任务和声明依赖
程序调度器:分配 Worker 和控制并发
六、程序能够确定判断什么?
普通程序擅长判断明确、结构化的条件:
- 当前有多少任务;
- 任务声明了哪些依赖;
- 依赖是否已经成功;
- 是否存在循环依赖;
- Worker 是否空闲;
- 线程池是否还有容量;
- 是否超过并发限制;
- 是否达到超时时间;
- 两个规范化路径是否相同;
- 工具是否标记为只读;
- 当前操作是否命中审批规则。
例如:
if (!completedTasks.containsAll(task.dependencies())) {
return NOT_READY;
}
或者:
if (runningWrites.contains(targetPath)) {
return RESOURCE_CONFLICT;
}
这些判断具有确定性,相同输入应该得到相同结果。
七、LLM 更适合判断什么?
LLM 擅长处理自然语言和业务语义:
- 用户目标应拆成哪些步骤;
- “生成配置”和“启动项目”之间是否有关联;
- 哪个任务需要另一个任务的结论;
- 多个搜索任务是否可以独立进行;
- 失败后应该重试还是换方案;
- 如何根据部分结果重新规划。
例如:
任务 A:调查数据库瓶颈
任务 B:调查接口响应时间
任务 C:结合调查结果提出优化方案
LLM 能够理解任务 C 应该等待 A、B,即使用户没有明确写出“依赖”。
但是,这种判断必须转换成结构化计划,交给程序验证和执行。
八、工具元数据:把并发规则写进系统
为了减少对 LLM 的依赖,可以让工具注册时声明自己的特性:
public record ToolMetadata(
boolean readOnly,
boolean parallelSafe,
boolean idempotent,
boolean requiresExclusiveAccess,
RiskLevel riskLevel
) {}
例如:
| 工具 | 只读 | 默认并行安全 | 是否独占 |
|---|---|---|---|
read_file |
是 | 是 | 否 |
list_dir |
是 | 是 | 否 |
search_code |
是 | 是 | 否 |
write_file |
否 | 需要检查路径 | 可能 |
delete_file |
否 | 否 | 是 |
execute_command |
不确定 | 默认否 | 视参数而定 |
deploy |
否 | 否 | 是 |
调度器可以先检查工具类型,再检查实际参数。
九、资源读写集合:识别真正的冲突
工具名称不同,不代表操作资源不同。
例如:
write_file("/project/config.json")
execute_command("sed -i ... /project/config.json")
虽然工具名不同,但都写入同一个文件。
可以让工具在执行前描述自己的资源效果:
public record ToolEffect(
Set<String> readResources,
Set<String> writeResources,
boolean externalSideEffect,
boolean requiresExclusiveAccess
) {}
两个任务是否能并行,可以检查:
A.write ∩ B.read ≠ 空集 → 读写冲突
A.read ∩ B.write ≠ 空集 → 读写冲突
A.write ∩ B.write ≠ 空集 → 写写冲突
伪代码:
boolean hasConflict(ToolEffect a, ToolEffect b) {
return intersects(a.writeResources(), b.readResources())
|| intersects(a.readResources(), b.writeResources())
|| intersects(a.writeResources(), b.writeResources());
}
例如:
| 操作 A | 操作 B | 是否可并行 |
|---|---|---|
读取 a.txt |
读取 b.txt |
可以 |
写入 a.txt |
读取 b.txt |
可以 |
写入 a.txt |
读取 a.txt |
不可以 |
写入 a.txt |
写入 a.txt |
不可以 |
十、三层决策模型
比较可靠的 Agent 并发系统可以采用三层判断。
第一层:LLM 语义规划
LLM 输出:
{
"id": "task_2",
"description": "启动项目",
"dependencies": ["task_1"],
"reason": "需要等待 task_1 生成配置文件",
"readResources": ["application.yml"],
"writeResources": []
}
第二层:程序确定性校验
程序验证:
- 依赖 ID 是否存在;
- DAG 是否有环;
- 读写资源是否冲突;
- 工具是否允许并发;
- 资源预算是否充足;
- 是否超过全局并发限制;
- 是否需要独占执行。
第三层:HITL 人工处理
以下情况可以交给人:
- 两个任务存在无法自动解决的写冲突;
- 操作不可逆;
- 涉及生产环境;
- LLM 与规则判断不一致;
- 依赖关系不明确;
- Agent 需要扩大原计划范围;
- 执行会影响外部用户。
例如:
检测到两个任务都将修改 application.yml:
task_1:修改数据库配置
task_2:修改服务端口
请选择:
1. 改为串行执行
2. 分别修改后自动合并
3. 取消其中一个任务
十一、并发调度器的完整决策过程
一个任务进入执行队列前,可以依次检查:
1. 所有依赖任务是否成功?
2. 是否与正在运行的任务产生资源冲突?
3. 工具是否声明为并行安全?
4. 操作是否需要独占资源?
5. 当前 Worker 和线程池是否有容量?
6. 是否超过 LLM、数据库或网络 API 限额?
7. 是否属于高风险操作?
8. 是否需要 HITL?
对应的伪代码:
ScheduleDecision evaluate(
Task task,
SchedulerState state) {
if (!state.succeededTasks()
.containsAll(task.dependencies())) {
return ScheduleDecision.waiting("依赖未完成");
}
if (state.hasResourceConflict(task)) {
return ScheduleDecision.waiting("资源冲突");
}
if (!task.parallelSafe()) {
return ScheduleDecision.exclusive();
}
if (!state.resourceBudget().hasCapacity(task)) {
return ScheduleDecision.waiting("资源不足");
}
if (task.riskLevel().requiresApproval()) {
return ScheduleDecision.approvalRequired();
}
return ScheduleDecision.runnable();
}
十二、全局并发预算
Plan-and-Execute 和 Multi-Agent 可能形成嵌套并发:
3 个任务并发
× 每个任务 4 个工具并发
= 理论上 12 个工具同时执行
如果每层只管理自己的线程池,就可能造成并发数量成倍增长。
因此应该设置全局预算:
全局 LLM 请求最多 3 个
全局工具调用最多 8 个
Shell 命令最多 2 个
生产写操作最多 1 个
可以使用共享线程池、Semaphore 或速率限制器:
Semaphore llmSlots = new Semaphore(3);
Semaphore shellSlots = new Semaphore(2);
这样即使外层有多个任务、内层又有多个工具,系统总并发量仍然可控。
十三、推荐的保守策略
实际落地时,可以先采用以下规则。
默认并行
- 读取不同文件;
- 查询不同数据源;
- 搜索不同关键词;
- 只读数据库查询;
- 独立文档摘要;
- 多个只读 Reviewer。
校验后并行
- 写入不同文件;
- 修改不同代码模块;
- 运行多个测试套件;
- 多个 Worker 开发不同功能;
- 多个 LLM 请求。
默认串行
- 写入同一个文件;
- 操作同一数据库记录;
- 使用同一个有状态 Worker;
- 使用同一个浏览器会话执行有状态操作;
- 多个未知 Shell 命令;
- 生产部署;
- 删除、支付和外部发送。
十四、一个完整示例
用户提出:
分析项目文档和依赖,然后生成报告。
LLM 生成计划
[
{
"id": "task_1",
"description": "读取 pom.xml",
"dependencies": [],
"readResources": ["pom.xml"]
},
{
"id": "task_2",
"description": "读取 README.md",
"dependencies": [],
"readResources": ["README.md"]
},
{
"id": "task_3",
"description": "生成分析报告",
"dependencies": ["task_1", "task_2"],
"writeResources": ["analysis.md"]
}
]
程序校验
task_1 与 task_2:
- 没有依赖
- 都是只读
- 读取不同资源
→ 允许并行
task_3:
- 依赖 task_1、task_2
→ 等待
实际执行
task_1 ─┐
├→ task_3
task_2 ─┘
这里既使用了 LLM 的语义能力,也使用了程序的确定性判断。
十五、最核心的职责边界
可以用下面这张表概括:
| 判断内容 | 更适合谁负责 |
|---|---|
| 用户目标如何拆分 | LLM |
| 任务之间的语义依赖 | LLM 初步判断 |
| 依赖 ID 是否有效 | 程序 |
| DAG 是否存在环 | 程序 |
| 两个路径是否相同 | 程序 |
| 是否存在读写冲突 | 程序 |
| 最大并发数 | 程序 |
| Worker 是否空闲 | 程序 |
| API 是否达到限额 | 程序 |
| 失败后如何重新规划 | LLM |
| 高风险操作是否批准 | 人类 |
总结
Agent 并行既不是完全由 LLM 决定,也不是普通线程池自己理解出来的。
更准确的架构是:
LLM 负责理解与规划
→ 输出结构化任务和依赖
→ 程序校验依赖、冲突和资源限制
→ 调度器执行可并行任务
→ 高风险与不确定场景进入 HITL
在简单 ReAct 实现中,同一轮多个工具调用通常主要依赖 LLM 判断;在 Plan-and-Execute 和 Multi-Agent 中,LLM 或编排器负责生成任务与依赖,程序根据 DAG、线程池和 Worker 池执行。
生产级系统不能只相信“LLM 把它们放在同一轮,所以一定能并行”。更可靠的做法是加入工具元数据、资源读写集合、冲突检测、全局并发预算、权限规则和人工审批。
一句话概括:
LLM 决定“从业务语义看哪些任务可能并行”,程序决定“从依赖、资源和安全规则看是否真的允许并行”。