上一篇讨论了什么情况下,一个任务适合交给多个 Agent 协作。

完成职责划分后,接下来要解决的是 Multi-Agent 系统如何运行,也就是控制流(Control Flow)的问题。

比如,研究 Agent 已经返回结果,下一步交给写作 Agent,还是先让审校 Agent 检查资料?如果结果不完整,是重新研究,还是继续往下执行?整个任务又应该在什么条件下结束?

这和初学编程时控制流很相似。普通程序通常由开发者提前写好执行路径:什么时候进入分支,什么时候循环,下一步调用哪个函数。Multi-Agent 系统在此基础上多了一层变化:部分路径可以由模型根据当前状态动态决定。

因此,设计 Multi-Agent 系统时,关键要明确两件事:任务如何从一个执行单元流向下一个执行单元,以及每一次路径选择由谁来决定。

顺序、条件、并行、计划和移交,都是围绕这两个问题形成的不同控制方式。

一、控制流的设计对象

普通程序中的控制流很容易识别。

if 创建分支,for 创建循环,程序写完后,其执行路径也随之确定。

Multi-Agent 系统依然需要这些规则,只是“下一步做什么”多了一种可能:交给模型在运行时判断。

例如,一个线上故障排查系统包含日志分析、指标分析、代码检查和修复验证四个 Agent。

最简单的方式是固定执行:

代码片段Text
日志分析 → 指标分析 → 代码检查 → 修复验证

另一种方式,是增加一个负责调度的 Agent。

数据库延迟升高时,它先检查慢查询;而上线后不久刚好出现故障,它可能优先检查代码变更;证据不足时,还可以增加新的调查步骤。

以上两种设计中,Agent 的功能可能完全相同,但运行方式却不同。

前一种方案中,路径主要由代码决定;后一种方案中,模型开始参与路径选择。

所以,设计 Multi-Agent 时,应考虑一个重要问题:每一次路由决定,由代码、规则、人工,还是模型负责?

二、显式控制的工作流

工作流(Workflow)由开发者预先定义主要执行路径。

一种常见实现方式,是把工作流表示成计算图。比如 LangGraph 中的设计。其中:

  • 节点(Node):负责执行具体任务;
  • 边(Edge):规定节点之间怎样流转;
  • 条件边(Conditional Edge):根据当前状态选择后续节点。

显式工作流路径确定,适合步骤稳定、依赖清楚,或者对审计和安全要求较高的任务。

它的问题也很明显:如果实际执行路线经常变化,条件分支会越来越多,最后可能形成一张很难维护的流程网。

三、三类基础工作流

顺序、条件和并行,是最常见的三种显式控制方式。

顺序工作流

顺序工作流按照:

代码片段Text
A → B → C

依次执行,前一个节点的结果成为后一个节点的输入。

例如故障处理可以设计成:

代码片段Text
收集信息 → 诊断问题 → 生成方案 → 验证结果

验证阶段依赖前面的诊断和修改结果,因此没有必要提前执行。

这种结构路径清楚,失败后也容易知道任务停在哪里。

缺点是延迟会累加。前面某一步执行较慢,后面的节点都要等待。

条件工作流

条件工作流根据当前状态选择分支。

条件可以来自普通代码,例如错误码、请求类型和风险等级,也可以来自模型生成的分类结果。

例如监控系统可以把告警交给数据库、网络或应用 Agent。

即使分类由模型完成,只要模型只能在几个预先声明的节点中选择,整体控制权仍然属于工作流。

下面是一个 LangGraph 示例:

代码片段Python
from typing import Literal
from typing_extensions import TypedDict
from langgraph.graph import END, START, StateGraph
 
 
class IncidentState(TypedDict):
    alert_type: str
    result: str
 
 
def route_alert(
    state: IncidentState,
) -> Literal["database_agent", "network_agent"]:
    if state["alert_type"] == "database":
        return "database_agent"
    return "network_agent"
 
 
def database_agent(state: IncidentState) -> dict:
    return {"result": "检查慢查询和连接池"}
 
 
def network_agent(state: IncidentState) -> dict:
    return {"result": "检查丢包率和网关延迟"}
 
 
builder = StateGraph(IncidentState)
 
builder.add_node("route_alert", lambda state: {})
builder.add_node("database_agent", database_agent)
builder.add_node("network_agent", network_agent)
 
builder.add_edge(START, "route_alert")
builder.add_conditional_edges("route_alert", route_alert)
builder.add_edge("database_agent", END)
builder.add_edge("network_agent", END)
 
workflow = builder.compile()
 
result = workflow.invoke(
    {"alert_type": "database", "result": ""}
)
 
print(result["result"])

StateGraph 创建状态图,add_node() 注册节点,add_edge() 添加固定边,add_conditional_edges() 则根据路由函数的返回值选择后续节点。

运行结果如下:

代码片段Text
检查慢查询和连接池

因为 alert_type 为 database,工作流会进入 database_agent。

如果把 route_alert 换成模型分类,模型可以参与判断,但它仍只能选择预先允许的节点。

这类设计的优点比较明显:把难以硬编码的判断交给模型,同时保留清楚的执行边界。

并行工作流

如果多个任务之间没有依赖,就可以同时执行,再统一合并结果。

这就是常见的 Fan-out / Fan-in。

例如排查线上延迟时,可以同时检查日志、监控指标和最近的发布记录,完成后再由汇总 Agent 对照分析。

并行可以缩短整体等待时间,但也会增加协调问题:

某个分支超时怎么办?汇合节点需要等待哪些结果?多个 Agent 同时写入状态时如何处理冲突?

因此,判断是否适合并行,关键要看数据依赖和可能影响 Agent 运行的异常情况,而不是 Agent 的数量。

四、运行时产生的路径

有些任务很难提前确定完整流程。

例如用户只提出:

找出这次性能下降的原因,并给出修复建议。

系统可能先查看监控指标,再检查日志和版本变更;调查过程中,也可能发现问题其实来自第三方服务。

这类任务无法把完整执行路径提前写死。模型需要根据当前目标、已有证据和工具返回结果,在运行过程中决定下一步:

代码片段Text
下一步调查什么
↓
调用哪个 Agent 或工具
↓
是否继续调查

因此,任务的实际路径是在运行时逐步形成的。这类方式通常可以称为动态控制流(Dynamic Control Flow),在 Multi-Agent 系统中也常表现为动态编排(Dynamic Orchestration)。

如果多个 Agent 能够自主协作,整体执行路径并非由某个中心控制器明确规划,而是从 Agent 之间的局部决策和交互中逐渐形成,也可以进一步称为涌现式控制(Emergent Control)。

不过,自主并不意味着没有限制。

生产系统通常仍然要设定:

  • 可以调用哪些 Agent 和工具;
  • 最大执行轮数;
  • Token 或调用预算;
  • 超时规则;
  • 哪些操作必须人工确认。

一个实用原则是:模型负责难以提前确定的判断,程序负责安全边界和资源上限。

例如模型可以决定是否继续调查,但最多执行十轮;它可以提出修改数据库配置,但真正执行前必须经过人工审批。

自主程度提高以后,测试方式也要改变。

此时不能只检查是否经过某条固定路径,还要验证最终结果、异常行为、调用次数和资源消耗。

五、计划式集中编排

计划式编排(Plan-Based Orchestration)通常由一个编排 Agent 管理整个任务。

典型过程是:

代码片段Text
理解目标
↓
生成计划
↓
分配 Agent
↓
检查结果
↓
调整计划
↓
判断结束

例如故障原因未知时,编排 Agent 可以先安排日志和指标调查。

如果指标 Agent 发现数据库连接等待异常,它可以暂停原来的代码检查,增加慢查询分析,修复后再安排验证。

这种方式的优势是:动态执行仍然保留了明确的任务状态。

系统可以知道哪些步骤已经完成、哪些仍在等待,以及为什么增加或取消了某个任务。

计划式编排也有利于控制上下文。

编排 Agent 掌握完整任务状态,而数据库 Agent 只接收当前工作真正需要的内容,不必读取此前所有对话。

这样可以减少干扰,也能降低 Token 消耗。

风险同样集中在编排 Agent。

如果它最开始判断错了方向,后续专业 Agent 很可能沿着错误路线继续工作。

因此,计划式编排更适合目标明确,但解决步骤需要动态产生的任务。

如果流程本来就固定,直接使用工作流通常更简单。

六、移交式局部协作

移交(Handoff)让当前 Agent 判断自己的工作是否结束,并把任务交给另一个 Agent。

例如:

代码片段Text
一线运维 Agent
    ↓
数据库 Agent
    ↓
基础设施 Agent

一线运维 Agent 判断问题来自数据库,于是交给数据库 Agent;数据库 Agent 又发现问题来自云基础设施,再继续移交。

这种方式适合职责边界清楚、处理过程需要连续交互的场景。

不过,Handoff 至少需要明确三个问题:

  1. 什么情况下移交;
  2. 可以移交给谁;
  3. 需要传递哪些状态。

否则很容易形成:

代码片段Text
Agent A → Agent B → Agent A → Agent B

另一个常见问题是上下文丢失。

因此,移交时通常应该携带:

  • 当前任务目标;
  • 已确认事实;
  • 已执行操作;
  • 尚未解决的问题;
  • 希望接收方完成什么。

没有必要复制全部聊天记录,否则既增加 Token 成本,也会引入无关信息。

七、对话式共同协作

对话式协作(Conversation-Driven Pattern)让多个 Agent 共享一段会话,通过轮流发言完成任务。

这种模式也常被称为 Group Chat。

发言顺序可以固定:

代码片段Text
研究 Agent → 分析 Agent → 审校 Agent

也可以由模型根据当前内容决定下一位发言者。

例如审校 Agent 发现某条结论缺少来源,可以重新调用研究 Agent,而不需要提前写出固定回路。

这种模式搭建简单,也方便观察讨论过程,适合方案探索、技术评审和多角色讨论。

问题主要来自成本和控制。

随着消息越来越多,每个 Agent 读取的历史也会越来越长,Token 消耗持续增加。

如果固定轮询,还可能出现无效调用;如果让模型选择下一位发言者,又增加了路由判断的不确定性。

因此,它适合开放探索,且必须有明确的停止条件。

八、模式选择与组合

下表展示了上述所有模式以及适用场景。

模式下一步由谁决定适合场景主要代价
顺序工作流固定边步骤和依赖稳定串行延迟
条件工作流规则或受限路由分支已知分支维护
并行工作流预定义依赖子任务独立状态合并
计划式编排编排 Agent目标明确、路径未知编排错误
移交式协作当前 Agent职责边界清楚循环移交
对话式协作规则或模型开放探索Token 增长

实际系统很少只使用一种模式。

例如故障处理可以设计成:

代码片段Text
告警分类
↓
并行检查日志、指标和版本
↓
计划式调查根因
↓
固定审批
↓
执行变更
↓
固定验证

规则明确的部分交给工作流,未知程度高的部分再增加模型决策。

一个复杂 Agent 内部也可以继续包含另一套控制模式。比如,外层工作流中的“故障调查”节点,可以运行计划式 Multi-Agent;其中的“安全审查”又可以采用固定检查流程。

总结来说,选择控制方式时,我们可以从几个方面判断。

如果下一步能够提前明确,优先使用固定工作流,执行路径更稳定,也更容易测试和维护。

如果子任务之间彼此独立,可以并行执行;存在明显前后依赖时,则更适合保持顺序。

还要看一次动态决策需要多少上下文。如果决策依赖完整任务状态,集中编排通常更合适;如果只涉及相邻角色之间的职责转交,可以考虑使用 Handoff。

最后要考虑系统能够接受多大的路径变化。模型拥有的控制权越多,执行路径越灵活,但测试、可观测性、成本控制和异常恢复的要求也会随之提高。

最后,回答本文开头的问题。

研究 Agent 完成任务以后,如果资料检查属于固定步骤,可以直接进入审校节点;如果下一步取决于研究结果,可以交给编排 Agent 判断;如果任务已经进入另一个明确职责范围,则适合 Handoff。

Multi-Agent 架构中有多少个 Agent,并不能说明系统是怎样运行的。真正决定系统行为的,是谁拥有控制权,依据什么状态决定下一步,以及异常发生后如何停止和恢复。