2026年6月22日约 3490 字12 分钟阅读

多Agent编排:从单兵到团队

单个Agent的瓶颈在哪里?Supervisor、Swarm、Role-based三种编排模式各适合什么场景?本篇从架构原理出发,构建一个CodingAgent+ReviewAgent的Supervisor系统,带你理解上下文过滤、任务路由、终止保护的工程细节。

系列:从LLM到Agent · 第7篇 / 共10篇 阅读本文建议先了解第6篇的CodingAgent实战第4篇的Agent Prompt Engineering

上一篇我们构建了一个完整的 CodingAgent——它能读文件、写代码、跑测试、修 bug,一条龙全干了。5 轮对话,4 次工具调用,给 calculator.py 写出了通过的单元测试。

看起来很厉害。但你有没有想过一个问题:它自己写的代码,自己验证,自己说"完成了"——谁来审查它?

这就像一个程序员提交了 PR,然后自己点了 Approve。在任何正经的工程团队里,这是不允许的。

这不是假设。在实际的 Agent 评测中,单个 Agent 存在一个系统性偏差:它倾向于对自己的输出给出过高的评价。让它自己判断代码质量,它会忽略边界情况、跳过异常处理、遗漏测试覆盖——然后自信地告诉你"一切正常"。

解决方案和人类工程团队一样:分工。让写代码的 Agent 和审查代码的 Agent 分开,各司其职。

本篇我们就来构建这个多 Agent 系统。

NOTE

无框架依赖 本篇的 Supervisor 系统完全手写,不依赖 LangGraphCrewAI 或 OpenAI Agents SDK。理解底层原理后,你可以自由选择任何框架——或者不用框架。

为什么需要多 Agent?

单个 Agent 的能力天花板体现在三个层面:

注意力稀释。一个 Agent 同时承担编码、测试、审查、文档等职责时,System Prompt 必须覆盖所有角色要求。Prompt 越长,模型对每条指令的遵循度越低——第4篇讨论过的 System Prompt 设计原则在这里遇到了规模瓶颈。

工具权限冲突。CodingAgent 需要 write_fileexecute_command 来完成工作,但 ReviewAgent 如果也拥有写权限,它就可能"顺手"修复发现的问题,而不是报告给开发者。第4篇的 L2 防线(工具级权限控制)在多角色场景下要求更细粒度的隔离。

自我评估偏差。模型评估自己生成的内容时存在系统性偏差,就像人类难以发现自己文章中的错别字。独立的审查 Agent 没有"沉没成本"包袱,能给出更客观的评价。

这正是 Unix 哲学在 AI 系统中的映射:每个程序做好一件事。一个 Agent 写代码,另一个 Agent 审代码,第三个 Agent 决定任务流转。

三种编排模式

多 Agent 系统的核心问题只有一个:谁决定下一步由谁来做? 答案不同,演化出三种主要模式。

Supervisor 模式

一个中心 Supervisor 负责路由决策,所有 Sub-Agent 只与 Supervisor 通信,互相不可见。LangGraph 的多 Agent 架构就是这个模式的典型实现。

text
用户任务 → Supervisor(路由决策)
                ├── CodingAgent(写代码)
                ├── ReviewAgent(审代码)
                └── ...更多专家

优点:控制流清晰,容易加入全局策略(如安全红线、成本预算)。 代价:Supervisor 本身是单点,每轮都消耗 token

Swarm 模式

Agent 之间对等协作,任何 Agent 可以将控制权"移交"(handoff)给另一个 Agent,没有固定中心。OpenAI Agents SDK 的 handoff 机制就是这个模式1

python
# Swarm 模式的核心思路(伪代码)
class SwarmAgent:
    def run(self, messages: list[Message]) -> Message | Handoff:
        result = self.think(messages)
        if result.needs_handoff:
            return Handoff(target="reviewer", context=result.summary)
        return Message(role="assistant", content=result.content)

优点:灵活,无中心瓶颈,Agent 可以动态决定交给谁。 代价:调试困难——控制流分散在每个 Agent 的决策逻辑中,难以追踪全局状态。

Role-based 模式

为不同 Agent 分配固定角色,任务沿预定义流水线传递。CrewAI 的 crew 概念就是这个模式——每个 Agent 有 role、goal、backstory 三个字段定义身份,任务按顺序流转。

优点:可预测性强,流程固定,适合批量重复任务。 代价:灵活性最低,新增角色需要修改流水线定义。每个 Agent 的 backstory 会注入到 System Prompt 中,角色越多 token 消耗越高。

模式选型

维度SupervisorSwarmRole-based
控制流中心路由去中心化固定流水线
灵活性
可观测性高(路由日志集中)低(分散)
适用场景需要全局策略的协作任务对话式、需要动态切换的场景流程固定的批量任务
代表框架LangGraphOpenAI Agents SDKCrewAI

本篇选择 Supervisor 模式手写实现——它的控制流最清晰,最适合拆解教学。理解了 Supervisor 的每一行代码,你再去看 LangGraph 的 StateGraph 或 OpenAI 的 handoff,会发现它们只是在同一个核心逻辑上加了不同的 API 糖衣。

Supervisor 系统:从零构建

我们要实现的系统:用户描述任务 → Supervisor 路由给 CodingAgent → CodingAgent 产出代码 → Supervisor 路由给 ReviewAgent → ReviewAgent 审查 → 根据审查结果决定是结束还是让 CodingAgent 修改。

ReviewAgent 实现

ReviewAgent 的结构与第6篇的 CodingAgent 高度对称——都实现 Agent Protocol(run(messages) -> Message),都有 Tool Loop。关键区别在于工具权限:ReviewAgent 只拥有只读工具(read_filesearch_files),不能修改文件或执行命令。这是第4篇 L2 防线的直接应用。

python
# review_agent.py
import json
from openai import OpenAI
from shared.types import Message, ToolResult, Tool
from shared.prepare_tools import prepare_tools_for_openai

REVIEWER_SYSTEM_PROMPT = """You are a code reviewer. Your job is to review code changes
for correctness, style consistency, and potential issues.

You can read files and search code, but you cannot modify files or run commands.
This separation exists by design — your role is to evaluate, not to fix.

When reviewing, check for:
- Logic errors and edge cases
- Style inconsistencies with the existing codebase
- Missing error handling
- Test coverage gaps

After review, provide a clear verdict:
- APPROVE: Code is ready to merge
- REQUEST_CHANGES: List specific issues that need fixing"""


class ReviewAgent:

    def __init__(self, tools: list[Tool], model: str = "gpt-4o"):
        self.client = OpenAI()
        self.model = model
        self.tools = {t.name: t for t in tools}
        self.tool_schemas = prepare_tools_for_openai(tools)

    def run(self, messages: list[Message]) -> Message:
        conversation = [
            {"role": "system", "content": REVIEWER_SYSTEM_PROMPT},
            *[{"role": m.role, "content": m.content} for m in messages],
        ]

        while True:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=conversation,
                tools=self.tool_schemas if self.tool_schemas else None,
            )
            msg = response.choices[0].message

            if not msg.tool_calls:
                return Message(role="assistant", content=msg.content)

            conversation.append({
                "role": "assistant",
                "content": msg.content,
                "tool_calls": [
                    {"id": tc.id, "type": "function",
                     "function": {"name": tc.function.name,
                                  "arguments": tc.function.arguments}}
                    for tc in msg.tool_calls
                ],
            })

            for tool_call in msg.tool_calls:
                name = tool_call.function.name
                kwargs = json.loads(tool_call.function.arguments)
                result_str = self.tools[name].execute(**kwargs)
                tool_result = ToolResult(
                    content=result_str,
                    is_error=any(
                        k in result_str
                        for k in ["ERROR", "BLOCKED", "TIMEOUT"]
                    ),
                )
                conversation.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": tool_result.to_str(),
                })

对比第6篇的 CodingAgent,两者的 Tool Loop 逻辑完全一致——while True → 调用模型 → 无 tool_calls 则返回 → 有则执行。区别只在 System Prompt 和注入的工具列表。这正是 Agent Protocol 的价值:统一的 run(messages) -> Message 接口让每个 Agent 的外部行为一致,内部实现自由

TIP

递归组合 SupervisorAgent 本身实现了 Agent Protocol——它有 run(messages) -> Message 接口。这意味着你可以把一个 SupervisorAgent 作为子Agent注册到另一个 SupervisorAgent 中,构建多层级的编排树。本篇不展开实现,但这正是第4篇定义统一 Protocol 时埋下的扩展性。

SupervisorAgent 实现

Supervisor 的核心职责是两个:路由(决定下一个 Agent)和编排(管理消息流转)。

路由决策使用第3篇介绍的 Structured Output——通过 strict: Trueenum 约束,确保模型只能输出合法的 Agent 名称,彻底消除自由文本解析的不确定性。

python
# supervisor.py
import json
from openai import OpenAI
from shared.types import Message, Agent

ROUTER_PROMPT = """You are a task router that coordinates a coding team.
You have two team members:
- coder: writes and modifies code, runs tests
- reviewer: reads code and checks for issues, cannot modify files

Route the task to the appropriate team member based on the current state:
- If code needs to be written or modified → coder
- If code has been written and needs review → reviewer
- If the reviewer approved the changes → FINISH
- If the reviewer requested changes → coder (to address feedback)

Always route to reviewer before finishing. Never finish without a review."""


class SupervisorAgent:

    def __init__(self, agents: dict[str, Agent], model: str = "gpt-4o-mini"):
        self.client = OpenAI()
        self.model = model
        self.agents = agents
        self.max_rounds = 5

    def _route(self, messages: list[dict]) -> str:
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": ROUTER_PROMPT},
                *messages,
            ],
            tools=[{
                "type": "function",
                "function": {
                    "name": "route",
                    "description": "决定下一步的行动",
                    "strict": True,
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "next": {
                                "type": "string",
                                "enum": ["coder", "reviewer", "FINISH"],
                                "description": "下一个执行的Agent,或FINISH结束"
                            },
                            "reason": {
                                "type": "string",
                                "description": "路由决策的理由"
                            }
                        },
                        "required": ["next", "reason"],
                        "additionalProperties": False
                    }
                }
            }],
        )
        tool_call = response.choices[0].message.tool_calls[0]
        decision = json.loads(tool_call.function.arguments)
        return decision["next"]

    def _filter_context(self, conversation: list[dict],
                        target: str) -> list[dict]:
        filtered = []
        for msg in conversation:
            if msg["role"] == "user":
                filtered.append(msg)
            elif msg["role"] == "assistant" and "name" in msg:
                filtered.append(msg)
        return filtered

    def run(self, messages: list[Message]) -> Message:
        conversation = [
            {"role": m.role, "content": m.content} for m in messages
        ]
        round_count = 0

        while round_count < self.max_rounds:
            next_agent = self._route(conversation)
            if next_agent == "FINISH":
                break

            agent = self.agents[next_agent]
            filtered = self._filter_context(conversation, next_agent)
            result = agent.run([
                Message(role=m["role"], content=m["content"])
                for m in filtered
            ])

            conversation.append({
                "role": "assistant",
                "content": f"[{next_agent}]: {result.content}",
                "name": next_agent,
            })
            round_count += 1

        return Message(
            role="assistant",
            content=conversation[-1]["content"],
            metadata={"rounds": round_count},
        )

几个关键设计决策:

路由用小模型,执行用大模型SupervisorAgent 默认使用 gpt-4o-mini——路由决策只需要理解当前状态并选择下一步,不需要强推理能力。而 CodingAgent 和 ReviewAgent 使用 gpt-4o 做实际的编码和审查工作。这一层模型分层可以将路由成本降低一个数量级。

strict: True + enum 消除解析风险_route 方法使用 Structured Output,模型的输出被约束为 ["coder", "reviewer", "FINISH"] 三选一。不需要写正则匹配、不需要处理"我觉得应该给 coder"这种自由文本——第3篇的 Function Calling 在这里作为结构化路由器使用,additionalProperties: False 确保不会出现多余字段。

_filter_context 实现上下文过滤。这是多 Agent 系统中最容易被忽略的工程点——下一节详细讨论。

上下文过滤

多 Agent 系统面临一个单 Agent 不存在的问题:Agent A 的完整工作记录要不要传给 Agent B?

CodingAgent 在工作过程中会产生大量的工具调用日志——读了哪些文件、写了什么代码、执行了什么命令、输出了什么结果。如果把这些原封不动地传给 ReviewAgent,会产生两个问题:

  1. Token 成本爆炸。一次编码任务可能产生数千 token 的工具调用日志,ReviewAgent 并不需要这些中间过程
  2. 注意力稀释。大量无关信息降低 ReviewAgent 对代码质量问题的关注度

_filter_context 的策略很简单:只保留用户消息和各 Agent 的最终输出(带 name 标签的 assistant 消息),丢弃所有中间的工具调用细节。

python
def _filter_context(self, conversation: list[dict],
                    target: str) -> list[dict]:
    # target 参数为扩展预留——当前两Agent场景下双方互相可见即可
    filtered = []
    for msg in conversation:
        if msg["role"] == "user":
            filtered.append(msg)
        elif msg["role"] == "assistant" and "name" in msg:
            filtered.append(msg)
    return filtered

target 参数目前没有使用——在两个 Agent 的场景下,双方互相看到对方的最终输出是合理的。但当你扩展到5个、10个 Agent 时,这个参数就变成了按需过滤的入口:可以根据 target 决定哪些 Agent 的输出对当前 Agent 可见、哪些不可见。

IMPORTANT

术语约定 本篇使用"上下文过滤"(context filtering)描述 Supervisor 模式中控制每个 Agent 可见信息的机制。它是更广义的"上下文管理"(context management)的子集——后者还包括第8篇要讨论的记忆系统中的信息筛选。

组装运行

现在把所有零件组装起来:

python
# main.py
from shared.types import Message
from coding_agent import CodingAgent
from review_agent import ReviewAgent
from supervisor import SupervisorAgent

# CodingAgent:沿用第6篇的完整实现
coder = CodingAgent(
    tools=[ReadFile(), WriteFile(), SearchCode(), RunCommand()]
)

# ReviewAgent:只有只读工具——L2 权限隔离
reviewer = ReviewAgent(
    tools=[ReadFile(), SearchCode()]
)

# Supervisor:编排两个专家
supervisor = SupervisorAgent(
    agents={"coder": coder, "reviewer": reviewer}
)

result = supervisor.run([
    Message(
        role="user",
        content="给 calculator.py 的除法函数添加除零保护,并更新测试"
    )
])
print(result.content)
print(f"总轮次: {result.metadata['rounds']}")

注意和第6篇的场景延续:第6篇 CodingAgent 处理的是 calculator.py 的浮点精度问题,这次处理除零保护——同一个文件,不同的 bug 类型,但现在有了独立的代码审查。

一次典型的执行流程:

text
Round 1 — Supervisor → coder
  CodingAgent 读取 calculator.py,发现 divide() 没有除零检查。
  添加 if divisor == 0: raise ValueError("Cannot divide by zero")。
  写入测试 test_divide_by_zero(),运行 pytest,全部通过。

Round 2 — Supervisor → reviewer
  ReviewAgent 读取修改后的 calculator.py 和测试文件。
  发现问题:测试只检查了 ValueError 的抛出,没有验证正常除法仍然正确。
  输出 REQUEST_CHANGES。

Round 3 — Supervisor → coder
  CodingAgent 收到审查反馈,补充 test_divide_normal() 测试用例。
  运行 pytest,全部通过。

Round 4 — Supervisor → reviewer
  ReviewAgent 重新审查,确认修改和测试覆盖完整。
  输出 APPROVE。

Supervisor → FINISH(总轮次: 4)

4 轮协作,2 次代码审查。如果只用第6篇的单 Agent,CodingAgent 会在 Round 1 结束后直接报告"完成"——遗漏的测试覆盖问题不会被发现。

三个工程陷阱

陷阱1:Router 吞掉你的预算

每一轮路由决策都是一次 LLM 调用。如果 Supervisor 和 Sub-Agent 使用相同的大模型,路由成本会占总成本的 30-50%——而路由决策本身不需要强推理能力。

解决方案:模型分层。Supervisor 用 gpt-4o-mini(成本约为 gpt-4o 的 1/10),Sub-Agent 用 gpt-4o。这就是为什么 SupervisorAgent.__init__ 默认 model="gpt-4o-mini" 而 CodingAgent/ReviewAgent 默认 model="gpt-4o"

python
# 成本对比
supervisor = SupervisorAgent(
    agents=agents,
    model="gpt-4o-mini",   # 路由:~$0.15/M input tokens
)
coder = CodingAgent(
    tools=tools,
    model="gpt-4o",        # 执行:~$2.50/M input tokens
)

陷阱2:上下文污染

不做上下文过滤时,每个 Agent 看到所有其他 Agent 的完整工具调用日志。3 轮协作后,conversation 列表可能膨胀到 20,000+ token——其中 80% 是与当前 Agent 无关的中间过程。

这不只是成本问题。Token 膨胀会直接降低模型的指令遵循度:当 System Prompt 只占上下文的 5% 时,模型更容易"忘记"它的角色约束。

上一节的 _filter_context 是最小化方案。在更复杂的系统中,你可能需要为每个 Agent 维护独立的上下文窗口,只在必要时注入其他 Agent 的摘要。

上下文过滤解决的是单次任务内的信息流控制——每个 Agent 在每轮推理时看到什么。但还有一个更长时间尺度的问题:跨任务之间,哪些信息值得保留、哪些应该遗忘?这就是下一篇要构建的记忆系统。

陷阱3:无限循环

如果 CodingAgent 的修改始终无法满足 ReviewAgent 的要求,两者会陷入无限"修改-拒绝"循环。这和第6篇的 BLOCKED_PATTERNS 是同一类问题——缺少硬性终止条件。

self.max_rounds = 5 就是那个安全阀:

python
while round_count < self.max_rounds:
    next_agent = self._route(conversation)
    if next_agent == "FINISH":
        break
    # ...
    round_count += 1

# 超过 max_rounds 自动退出,返回最后一条消息
return Message(
    role="assistant",
    content=conversation[-1]["content"],
    metadata={"rounds": round_count},
)

metadata["rounds"] 让调用方知道是正常结束(FINISH)还是超时退出(rounds == max_rounds)。在生产环境中,超时退出应该触发告警而不是静默返回——但状态持久化和告警机制属于第8篇的内容,本篇保持内存态的简洁实现。

WARNING

生产环境必须加终止保护 max_rounds 是保底措施,不是设计目标。如果你的系统经常触达 max_rounds,说明 ROUTER_PROMPT 的路由逻辑或 Sub-Agent 的能力存在问题,需要从根因解决。

什么时候不该用多 Agent?

多 Agent 系统有真实的工程成本:更多的 LLM 调用、更复杂的调试、更难以预测的行为。不是所有任务都需要多个 Agent。

Li(2026) 的研究直接挑战了"多 Agent 万能"的假设:在多个基准测试中,单个 Agent 配合技能库(skill library)可以替代多 Agent 系统,同时保持甚至超过后者的性能2。核心发现是——很多时候你以为需要"多个专家协作",实际上只是需要"一个专家配合更多工具"。

在决定是否引入多 Agent 之前,问自己三个问题:

1. 角色之间是否需要权限隔离? 如果所有 Agent 需要相同的工具和权限,多 Agent 只是增加了通信开销。CodingAgent + ReviewAgent 有效,正是因为 reviewer 不能修改文件——这个约束在单 Agent 中无法实现。

2. 任务是否存在自然的检查点? "写代码然后审查"有明确的阶段划分。但"回答一个技术问题"没有——强行拆成"搜索 Agent"和"总结 Agent"往往比单个 Agent 更差。

3. 你能接受延迟和成本的增加吗? 每增加一个 Agent,至少增加一次 LLM 调用。4 轮 Supervisor 协作意味着至少 8 次 API 调用(4 次路由 + 4 次执行)。如果用户需要实时响应,多 Agent 可能不是正确的选择。

如果三个问题的答案都是"否",用第6篇的单 Agent + 更好的工具集就够了。

TIP

A2A:跨进程的 Agent 协作 本篇的多 Agent 系统运行在同一进程内,Agent 之间通过 Python 函数调用通信。但当 Agent 需要跨服务、跨组织协作时,需要标准化的通信协议。Google 提出的 A2A(Agent-to-Agent)协议正在解决这个问题——它与 MCP(Agent-to-Tool)互补,都已加入 Linux Foundation 的 AAIF。第10篇会展开讨论跨进程 Agent 协作的工程实现。

总结与下一步

本篇我们构建了一个完整的 Supervisor 多 Agent 系统:

  • 三种编排模式:Supervisor(中心路由)、Swarm(去中心化移交)、Role-based(固定流水线),各有适用场景
  • 手写 Supervisor:路由用 Structured Output 约束输出空间,_filter_context 控制信息流,max_rounds 防止无限循环
  • CodingAgent + ReviewAgent 协作:写代码和审代码分离,L2 权限隔离让 ReviewAgent 只能读不能写
  • 工程陷阱:模型分层控制成本、上下文过滤防止注意力稀释、终止保护避免无限循环
  • 决策框架:不是所有场景都需要多 Agent,单 Agent + 技能库在很多任务上已经足够

到目前为止,我们构建的所有 Agent(第1篇到第7篇)都是内存态系统——对话历史存在 Python 列表中,进程结束即消失。Supervisor 的路由状态、Sub-Agent 的执行结果、审查的反馈记录——全部随进程退出而丢失。

下一篇,我们让 Agent 拥有记忆

参考文献

  1. Building Effective AgentsErik Schluntz, Barry Zhang · Anthropic Blog, 2024 · 博客
  2. When Single-Agent with Skills Replace Multi-Agent Systems and When They FailX. Li · arXiv, 2026 · 论文