📘 《深入理解 AI Agent》多 Agent 编排系列 · 第1篇
记忆系统解决了单个 Agent 的"remembering"问题,但任务复杂度一旦超过单 Agent 的能力边界,就需要多 Agent 协作------分工、协调、汇总。
多 Agent 编排要解决的核心问题:怎么让这些 Agent 按正确的顺序、正确的方式协同工作?
网上讲编排的文章大多是"串行、并行、条件、竞争"四种模式的概念罗列。但在 Dream-SaaS 落地的经验是:模式本身不复杂,复杂的是每种模式背后的坑。这篇直接聊问题。
1. 为什么单 Agent 不够用?
给一个 Agent 塞 10 个工具、写 2000 字 system prompt,当任务涉及"搜索 → 分析 → 写报告 → 生成图表 → 排版发布"这种多步骤流程时,单 Agent 会碰到三个硬伤:
🔴 问题一:Prompt 膨胀
工具越多,Prompt 越长,模型注意力分散,指令遵循能力下降。实测中,当工具数从 3 个增加到 10 个时,指令遵循准确率从 92% 降至 71%。
🔴 问题二:错误放大
单 Agent 链路中,一步错步步错,没有"回退"或"降级"的机会。上游 Agent 输出的微小偏差会在下游被逐步放大。
🔴 问题三:无法并行
所有步骤串行执行,总延迟 = 各步骤延迟之和,用户体验差。5 个串行步骤,每步 3 秒,总延迟就是 15 秒。
解法是把任务拆分给多个专业 Agent,每个只负责一小块,通过编排层协调执行顺序和数据流。
2. 四种编排模式:每种模式的坑在哪
四种基础模式:串行、并行、条件分支、竞争。定义不展开,直接聊每种模式的坑点。
2.1 模式一:串行(Sequential)
bash
Agent A → Agent B → Agent C → 结果
A 的输出作为 B 的输入,像流水线一样。
✅ 优势:逻辑清晰,每步输入输出可追踪。
⚠️ 坑点
- 延迟累积:如果 A 耗时 2s、B 耗时 3s、C 耗时 5s,总延迟 = 10s。用户等不了。
- 单点脆弱:任何一个环节失败,整条链路断裂。B 挂了,A 和 C 的结果全部作废。
- 错误传播:A 输出一个小错误,经过 B 放大,到 C 已经面目全非------就像"传话游戏"。
工程建议 :串行链路中,每个节点都应有超时控制 和输出校验。不要假设上游一定返回正确数据。Dream-SaaS 的内容生成链路中,每个节点之间加了一层轻量级校验:输出为空或格式异常时,直接触发降级逻辑,而不是让错误继续传播。
实际代码思路(Java):
bash
public class SequentialPipeline {
private final List<AgentNode> nodes;
private final Duration timeoutPerNode;
public String execute(String input) {
String current = input;
for (AgentNode node : nodes) {
try {
current = node.execute(current)
.orTimeout(timeoutPerNode.toSeconds(), TimeUnit.SECONDS)
.join();
// 输出校验:防止错误传播
if (current == null || current.isBlank()) {
log.warn("节点 {} 返回空结果,触发降级", node.getName());
current = node.fallback(current);
}
} catch (CompletionException e) {
log.error("节点 {} 执行失败: {}", node.getName(), e.getMessage());
throw new PipelineException(node.getName(), e);
}
}
return current;
}
}
2.2 模式二:并行(Parallel)
bash
┌→ Agent A ─┐
输入 ─┼→ Agent B ─┼→ 合并 → 输出
└→ Agent C ─┘
多个 Agent 同时执行,最后合并结果。
✅ 优势:吞吐高、延迟低。
⚠️ 坑点
- 资源争用:5 个 Agent 同时调用 LLM API,触发限流,反而比串行还慢。
- 结果不一致:A 说"答案是 X",B 说"答案是 Y",怎么合并?简单拼接?投票?让 LLM 仲裁?
- 部分失败:3 个 Agent 中 1 个失败了,是返回 2 个的部分结果,还是整体回滚?
真实踩坑 :Dream-SaaS 的"多源知识检索"功能,最初设计 4 个 Agent 并行查询不同数据源。上线后发现,并发数超过 3 时,DeepSeek API 的限流导致大量 429 错误。最终方案是引入信号量控制并发数 + 指数退避重试 ,并在合并层做容错处理------允许部分 Agent 失败,只要核心数据源成功即可。
并发控制示例:
bash
public class ParallelOrchestrator {
private final Semaphore concurrencyLimit;
private final ExecutorService executor;
public ParallelResult execute(List<AgentTask> tasks) {
List<CompletableFuture<AgentResult>> futures = tasks.stream()
.map(task -> CompletableFuture.supplyAsync(() -> {
try {
concurrencyLimit.acquire(); // 控制并发数
return executeWithRetry(task, 3); // 指数退避重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return AgentResult.failed(e.getMessage());
} finally {
concurrencyLimit.release();
}
}, executor))
.toList();
// 容错合并:只要核心数据源成功即可
List<AgentResult> results = futures.stream()
.map(CompletableFuture::join)
.toList();
long coreSuccess = results.stream()
.filter(r -> r.isCoreSource() && r.isSuccess())
.count();
if (coreSuccess == 0) {
throw new OrchestrationException("所有核心数据源均失败");
}
return mergeResults(results);
}
private AgentResult executeWithRetry(AgentTask task, int maxRetries) {
for (int i = 0; i <= maxRetries; i++) {
try {
return task.agent().execute(task.input());
} catch (RateLimitException e) {
if (i == maxRetries) throw e;
long backoff = (long) Math.pow(2, i) * 1000; // 指数退避
sleep(backoff);
}
}
throw new UnreachableException();
}
}
2.3 模式三:条件分支(Conditional)
bash
┌→ Agent A(路径 1)
输入 → 路由器 ─┼→ Agent B(路径 2)
└→ Agent C(路径 3)
根据条件判断路由到不同 Agent。典型场景:意图识别后分发。
✅ 优势:职责分离,Prompt 精准。
⚠️ 坑点
- LLM 的不确定性:路由器用 LLM 实现时,同一个问题问两次可能路由到不同 Agent。例如用户问"帮我查一下文档里的代码示例",第一次路由到 RAG Agent(关键词命中"文档"),第二次可能路由到 Code Review Agent(关键词命中"代码")。
- 边界模糊:用户的问题可能同时涉及多个分支,需要设计优先级或组合路由策略。
- 无默认路径:路由条件未覆盖所有情况时,请求会"掉到地上"。必须有兜底的 default route。
工程建议:路由层的设计原则是**"能不调 LLM 就不调"**。Dream-SaaS 的 Supervisor 设计采用三层判定策略:
第一层:规则匹配 (关键词命中,延迟 < 1ms)→ 如包含"代码审查"直接走 Review Agent
第二层:启发式判断 (基于特征,延迟 < 50ms)→ 如检测到代码片段 + 长度 > 50 行
第三层:LLM 分类(兜底,延迟 500ms+)→ 只有前两层无法确定时才调用 LLM
这种设计让 80% 的请求在前两层完成路由,显著降低延迟和不确定性。
三层路由实现:
bash
public class ThreeLayerRouter {
private final List<RuleMatcher> rules; // 第一层
private final List<HeuristicMatcher> heuristics; // 第二层
private final LLMAgent llmClassifier; // 第三层
public AgentRoute route(String input) {
// 第一层:规则匹配
for (RuleMatcher rule : rules) {
if (rule.matches(input)) {
log.debug("规则命中: {} -> {}", rule.pattern(), rule.targetAgent());
return rule.targetAgent();
}
}
// 第二层:启发式判断
for (HeuristicMatcher h : heuristics) {
if (h.matches(input)) {
log.debug("启发式命中: {} -> {}", h.feature(), h.targetAgent());
return h.targetAgent();
}
}
// 第三层:LLM 分类(兜底)
try {
String classification = llmClassifier.classify(input);
return AgentRegistry.resolve(classification);
} catch (Exception e) {
// 必须有 default route,不能让请求"掉到地上"
log.warn("LLM 分类失败,使用默认路由: {}", e.getMessage());
return AgentRegistry.defaultAgent();
}
}
}
2.4 模式四:竞争(Race)
bash
┌→ Agent A ─┐
输入 ─┼→ Agent B ─┼→ 取最快/最优 → 输出
└→ Agent C ─┘
多个 Agent 同时执行相同任务,取最快或最优的结果。
✅ 优势:低延迟、高可靠、可 A/B 测试。
⚠️ 坑点
- 资源浪费:3 个 Agent 都在跑,但只有 1 个的结果会被用到,其他 2 个的算力和 API 调用费都浪费了。
- 败者清理:取到最快结果后,其余 Agent 还在跑吗?要不要主动取消?如何取消?
- 结果质量:最快的不一定是最好的。如果 Agent A 100ms 返回了一个"不知道",Agent B 2s 后返回了正确答案,你取哪个?
容易被忽略的问题 :竞争模式中的**"败者清理"。Java 中用
CompletableFuture.anyOf()拿到最快返回的 Future,但其他 Future 还在后台执行,继续占用线程和 LLM 资源。Dream-SaaS 实现了一个 竞争清理器**:胜者确定后,遍历所有败者 Future,调用cancel(true)并记录取消原因到日志,方便后续分析。
bash
public class RaceOrchestrator {
@SuppressWarnings("unchecked")
public AgentResult race(List<AgentTask> tasks) {
CompletableFuture<AgentResult>[] futures = tasks.stream()
.map(t -> CompletableFuture.supplyAsync(
() -> t.agent().execute(t.input()), executor))
.toArray(CompletableFuture[]::new);
try {
// 取最快结果
AgentResult winner = CompletableFuture.anyOf(futures).join();
// 败者清理:取消所有未完成的 Future
for (CompletableFuture<AgentResult> future : futures) {
if (!future.isDone()) {
boolean cancelled = future.cancel(true);
log.info("取消败者任务: cancelled={}", cancelled);
}
}
// 质量校验:最快的结果可能质量不够
if (!winner.meetsQualityThreshold()) {
log.warn("最快结果质量不达标,等待次优结果");
return waitForQualityResult(futures, winner);
}
return winner;
} catch (CompletionException e) {
throw new RaceException("所有竞争任务均失败", e);
}
}
}
3. DAG 调度:编排模式的"集大成者"
实际项目中更常见的是混合编排 ------先条件路由,再并行执行,最后串行汇总。描述这种复杂拓扑的最佳方式是 DAG(有向无环图)。
bash
┌→ Agent A ─┐
START → 路由 ─┼→ Agent B ─┼→ 合并 → Agent D → END
└→ Agent C ─┘
DAG 的好处是声明式:你只需要定义"谁依赖谁",调度器自动计算执行顺序。但有几个问题值得注意:
3.1 静态 DAG vs 动态 DAG
传统的 DAG 是编译时确定的------你画好图,执行时就按这个拓扑走。但在 Agent 场景中,图的结构可能需要在运行时动态决定。
举个例子:Agent A 分析用户需求后,可能决定需要调用 Agent B 和 C,也可能只需要 B,甚至可能发现需要新增一个 Agent D。这种动态图生成的需求,传统 DAG 调度器搞不定。
主流框架的解法:
| 框架 | 策略 | 特点 |
|---|---|---|
| LangGraph | 有状态的有向图,允许循环 | 节点可根据状态动态决定下一节点(条件边),甚至回退。代价是需要处理"无限循环"------必须设置最大步数或收敛检测 |
| Spring AI Alibaba StateGraph | DAG + 条件边 | 保留 DAG 结构约束,但通过条件边(Conditional Edge)支持运行时动态路由。"静态拓扑 + 动态路由"的折中,约束更强但可预测性更好 |
LangGraph 的循环机制:
bash
from langgraph.graph import StateGraph, END
workflow = StateGraph(AgentState)
workflow.add_node("analyze", analyze_node)
workflow.add_node("execute", execute_node)
workflow.add_node("review", review_node)
# 条件边:review 节点可以回退到 execute
workflow.add_conditional_edges(
"review",
lambda state: "execute" if state["needs_revision"] else END
)
# 必须设置最大步数防止无限循环
app = workflow.compile()
result = app.invoke(input, config={"recursion_limit": 10})
Spring AI Alibaba StateGraph 的条件边:
bash
StateGraph graph = new StateGraph("supervisor", stateFactory)
.addNode("intent", intentRouterNode)
.addNode("rag", ragAgentNode)
.addNode("code_review", codeReviewNode)
// 条件边:根据运行时状态动态路由
.addConditionalEdges("intent", state -> {
String intent = state.get("intent");
return switch (intent) {
case "knowledge" -> "rag";
case "code" -> "code_review";
default -> "rag"; // 默认路由
};
})
.addEdge("rag", END)
.addEdge("code_review", END);
两者的核心区别:LangGraph 允许 review → execute → review 这种循环(本质是有向图),而 StateGraph 要求拓扑无环,只能通过条件边在"同一层的不同节点"之间做选择。前者表达力更强,后者更容易推理和调试。
3.2 状态传递
DAG 中节点之间的状态传递有两种方式:
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 显式传递 | 节点输出作为下一个节点的输入参数 | 串行链路、简单 DAG |
| 共享状态 | 所有节点读写同一个全局状态对象 | 复杂 DAG、需要跨分支共享数据 |
显式传递更安全,但不够灵活;共享状态更灵活,但容易出竞态条件------两个并行节点同时修改同一个状态字段,结果不可预测。
3.3 竞态条件及解法
当并行分支共享同一个状态对象时,两个节点可能同时读写同一字段,导致数据不一致。
三种常见解法:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 不可变状态 | 每次更新返回新对象,不修改原对象(Redux reducer 模式) | 天然线程安全 | 内存开销大,状态对象多时 GC 压力上升 |
| 细粒度锁 | 对状态字段加 ReentrantLock 或 synchronized |
简单直接 | 可能成为性能瓶颈,死锁风险 |
| 原子更新 | AtomicReference<Map> 或 ConcurrentHashMap 原子操作 |
性能好 | 实现复杂,复合操作需要 CAS 循环 |
Dream-SaaS 的方案是共享状态 + 增量更新 + 键隔离:
bash
// 每个节点返回状态增量,而非修改全局状态
public Map<String, Object> execute(Map<String, Object> sharedState) {
Map<String, Object> delta = new HashMap<>();
delta.put("analysisResult", performAnalysis(sharedState.get("input")));
delta.put("confidence", 0.95);
return delta; // 框架负责合并
}
// 并行分支的约束:只写各自隔离的状态键
// Agent A 只写 "rag_result" 键
// Agent B 只写 "code_result" 键
// 合并阶段由汇总节点统一处理,避免并行写入冲突
Spring AI Alibaba 的 StateGraph 采用的就是这种模式:每个节点返回一个 Map<String, Object> 作为状态增量,框架负责合并到全局状态。Dream-SaaS 在此基础上增加了约束:并行分支只写各自隔离的状态键,从设计上杜绝了竞态条件的发生。
4. 选型决策框架
面对具体任务,怎么选编排模式?下面是一个从实际项目中总结的决策框架:
bash
第一步:任务可拆分吗?
├─ 不可拆分 → 单 Agent,不需要编排
└─ 可拆分 → 进入第二步
第二步:子任务之间有依赖吗?
├─ 无依赖 → 并行模式
└─ 有依赖 → 进入第三步
第三步:依赖是线性的还是分支的?
├─ 线性(A → B → C)→ 串行模式
└─ 分支(A 的输出决定走 B 还是 C)→ 条件分支模式
第四步:需要冗余或低延迟吗?
├─ 需要(如多模型择优、容灾)→ 竞争模式
└─ 不需要 → 用前面的组合即可
核心逻辑:从简单到复杂,能用简单模式就不上复杂模式。编排越复杂,调试越困难,故障点越多。
反模式警告 :不要为了"架构感"而过度编排。见过有项目把简单的"检索 → 生成"流程搞成 5 个 Agent 的 DAG,每个 Agent 都要调 LLM,总延迟从 3s 变成 12s,用户直接流失。编排是为了解决问题,不是炫技。
5. 总结
| 模式 | 适用场景 | 主要风险 | 缓解措施 |
|---|---|---|---|
| 串行 | 线性流程,步骤间有强依赖 | 延迟累积、错误传播 | 超时控制 + 输出校验 + 降级逻辑 |
| 并行 | 无依赖的独立子任务 | 资源争用、结果不一致 | 信号量限流 + 容错合并 |
| 条件分支 | 意图路由、任务分发 | LLM 不确定性、边界模糊 | 三层路由(规则→启发式→LLM) |
| 竞争 | 低延迟要求、多模型择优 | 资源浪费、败者未清理 | 竞争清理器 + 质量阈值 |
| DAG 混合 | 复杂多步骤任务 | 动态图、竞态条件 | 条件边 + 键隔离 + 增量状态 |
关键结论:
- 编排模式没有银弹,每种都有适用场景和对应的坑
- LLM 的不确定性会沿着编排链路传染,路由层、判断层都需要兜底机制
- 动态 DAG 的表达力强,但实现复杂度不容小觑
- 从简单到复杂,编排是手段不是目的
下篇预告:《深入理解 AI Agent(六):Agent 之间怎么"说话"------通信、状态与冲突解决》
📘 深入理解 AI Agent · 编排篇(上)
作者:宋哥 | Java 后端 → AI Agent 工程师
项目:Dream-SaaS · 多 Agent 协作平台
有问题评论区见,欢迎交流~
