4444 字
约 14 分钟
4
Agent 并行到底由谁决定?LLM 规划与程序调度的职责边界

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_1step_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 决定“从业务语义看哪些任务可能并行”,程序决定“从依赖、资源和安全规则看是否真的允许并行”。

Agent 并行到底由谁决定?LLM 规划与程序调度的职责边界
http://clxhxhhr.top/posts/459/
作者
clxstart
发布于
2026-09-04
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。