对比单Agent和多Agent适用场景,指导开发者在单一上下文窗口和任务分工间做权衡,明确拆分收益。
一个循环什么时候就够了——什么时候你需要一个团队
上一篇文章最后讲到了一个循环:推理、行动、观察、重复。这个循环运行在单个 Agent 内部,却能处理相当广泛的任务。但只要构建 Agent 的时间足够长,你迟早会撞上一堵墙:任务太长,无法装进一个 context window;两个子任务需要同时运行;问题的某个部分需要一位专家,但这位专家的能力放在其他环节只会产生干扰。
当你撞上这堵墙时,就需要做出选择:继续强化单个 Agent,还是将工作拆分给多个 Agent。知道该选择哪一种,以及为什么这么选,正是本文要讨论的内容。
在进行比较之前,有必要先给出准确的定义。单个 Agent,就是一个模型、一个 context window 和一套工具,运行一个循环。它对任务所掌握的一切,都存在于由思考、行动和观察不断累积而成的历史记录中。它的记忆就是它的 context。
这一约束既造就了它的简单性,也决定了它的能力边界。
单个 Agent 擅长的场景:
单个 Agent 容易失效的场景:
Multi-Agent 系统由两个或更多 Agent 协同完成任务。协同可以有许多形式:一个 Agent 创建其他 Agent;多个 Agent 并行运行并汇报结果;或者采用流水线,让每个 Agent 的输出成为下一个 Agent 的输入。它们的共同点在于,没有任何一个 Agent 独自负责整个任务。
这带来了三项关键能力:
并行处理。 如果需要同时研究五家公司,单个 Agent 只能依次研究。Multi-Agent 系统可以创建五个 subagent,一次性获得五份结果。对于 I/O 密集型任务——包括搜索、API 调用或文件读取——这是最主要的收益。
专业分工。 一个专门通过 prompt 和工具配置来编写 Python 的 Coding Agent,会比通用 Agent 更擅长编写 Python。Multi-Agent 系统允许你将子任务分配给为其量身打造的 Agent,例如规划者、研究员、程序员和评审者——每个 Agent 都配备合适的 system prompt 和工具。
Context 管理。 每个 subagent 都会获得一个全新且聚焦的 context。这样就不必让一个 Agent 不断积累来自早期步骤的 50,000 个 token 的噪声;subagent 只会接收到完成自身工作所必需的信息。orchestrator 负责总结和分派,worker 则始终保持专注。
最常见的 Multi-Agent 架构是一种两级层次结构:一个 orchestrator 和一个或多个 worker。
orchestrator 接收目标、制定计划并委派子任务。它不会亲自完成具体工作,而是决定工作内容以及由谁负责。worker 接收一个范围明确的任务,运行自己的循环,然后返回结果。它们彼此互不了解,也不知道整体目标。
def orchestrator(goal, worker_agents):
plan = llm(f"Break this goal into subtasks: {goal}")
# plan = [{"task": "...", "agent": "researcher"}, ...]
results = {}
for step in plan:
worker = worker_agents[step["agent"]]
results[step["task"]] = worker.run(step["task"])
final_answer = llm(f"Synthesise these results: {results}")
return final_answer
每个 worker.run() 都运行着上一篇文章介绍的同一种 Agent 循环——推理、行动、观察、重复——但其范围仅限于一个子任务。orchestrator 自己从不调用工具,只负责读取计划和进行分派。worker 从不接触全局视角,只负责解决分配给自己的部分。
这种职责分离很重要。它能让 orchestrator 的 context 保持精简,只包含计划和结果,而不包含工具产生的噪声;同时也能让 worker 的 context 保持聚焦,只包含一个任务、合适的工具,不掺杂其他内容。
当多个子任务相互独立时,可以并发运行 worker。在 Python 中,通常会选择 asyncio 或 ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor
def run_parallel(tasks, worker):
with ThreadPoolExecutor(max_workers=len(tasks)) as pool:
futures = {pool.submit(worker.run, task): task for task in tasks}
return {task: f.result() for f, task in
[(v, k) for k, v in futures.items()]}
这里有两件事需要注意。
第一是速率限制:五个 Agent 同时调用同一个 API,会比单个 Agent 依次调用更快触及配额限制。应该在 worker 层实现 back-off 逻辑,而不能只在 orchestrator 层处理。
第二是结果合并:并行执行的结果不会按照固定顺序返回,而且彼此之间可能存在冲突。orchestrator 的综合处理步骤需要能够应对信息缺失和相互矛盾的情况,不能假设收到的输出始终干净、统一。
Multi-Agent 系统确实存在成本,而过早采用它,是 Agent 工程中最常见的错误之一。
复杂性会叠加。 单个 Agent 发生故障时很容易诊断——只需要查看运行轨迹。多个 Agent 同时发生故障,则会变成一个分布式系统问题:究竟是哪个 Agent 出错了?orchestrator 是否错误解析了结果?worker 是否接收到了有问题的任务?每增加一次传递,就会增加一个新的故障面。
延迟可能变得更高,而不是更低。 并行处理有利于 I/O 密集型任务。但对于 CPU 密集型或受模型调用限制的工作,创建 Agent、合并结果以及额外调用 orchestrator 带来的开销,可能超过单个专注的 Agent 完成任务所需的时间。
Context 交接会丢失信息。 当 orchestrator 总结某个 worker 的结果并传递给下一个 worker 时,它必须决定保留哪些内容。这些决策必然会造成信息损失。由单个 Agent 完整执行任务,则完全不需要对自身的工作进行这种总结。
坦率地说,默认应该从单个 Agent 开始。只有遇到明确瓶颈时,才添加第二个 Agent——例如 context 溢出、能够实际测量到并行处理带来的收益,或者存在无法通过更好的 prompt 和工具解决的能力错配。
单个 Agent 并不是通往 Multi-Agent 的过渡阶段。对于很大一类任务来说,它本身就是正确的架构;在没有明确理由拆分工作之前,也应该将其作为默认选择。
只有当任务确实超出一个 context window 的承载能力、并行处理能够带来实际的速度提升,或者专业 Agent 在特定子任务上的表现明显优于通用 Agent 时,Multi-Agent 系统增加的复杂性才物有所值。
循环本身并没有改变。真正发生变化的是:循环中的每一部分由谁负责,以及各部分如何汇报结果?
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。