🎯 多 Agent:Supervisor 编排

单个 Agent 做复杂任务会上下文爆炸、角色混乱。多 Agent 协作把任务拆给分工明确的多个 Agent。本章深入 Supervisor 拓扑——一个中心协调者分配任务给专家 Agent,精读 LangGraph 的子图机制和 LangChain 的 create_agent middleware 系统,并手写一个完整的 Supervisor 图。

本章目标

  • 掌握三种多 Agent 拓扑(Network / Supervisor / Hierarchical)及各自的适用场景
  • 精读 create_agent 的 middleware 系统与子图嵌入机制
  • 理解 Command(graph=Command.PARENT) 父子图通信原理
  • 用 StateGraph 手写一个完整的 Supervisor 图(路由 + 专家子图 + 状态共享)
  • 澄清本仓库不包含 create_supervisor(在独立 contrib 包)的事实
⚠️ 前置要求

本章假设你已完成 LangGraph 八章(尤其 LG1 StateGraphLG3 条件边LG8 预构建 Agent)。多 Agent 是 LangGraph 图能力的综合应用。

为什么需要多 Agent

回顾 OC5 章的核心洞察:单 Agent 跑长任务会上下文爆炸。但上下文压缩只是事后补救,更主动的方案是结构性拆分——让不同的 Agent 各管一摊:

痛点(单 Agent)多 Agent 的解法
上下文爆炸(一个 Agent 装所有信息)各 Agent 独立上下文,只共享必要信息
角色混乱(一个 Agent 既写代码又写文档又测试)专家分工,每个 Agent 专注一域
提示词臃肿(一个 prompt 装所有工具说明)每个 Agent 只看自己相关工具
难并行(串行处理慢)独立子任务可并行
错误传播(一步错全盘乱)子 Agent 失败可隔离、重试
💡 但多 Agent 不是银弹

多 Agent 也有代价:协调开销(Agent 间通信消耗 token)、复杂度上升(调试更难)、延迟增加(多一层路由)。经验法则:任务边界清晰、各子任务较重时用多 Agent;任务连贯轻量时单 Agent 更好。OpenCode 默认就是单主 Agent(build),只在需要时通过 task 工具派生子 Agent(OC5)。

三种多 Agent 拓扑

业界归纳出三种经典拓扑。理解它们的差异是设计多 Agent 系统的基础:

① Network 网络 A B C D 任意 Agent 间可通信 ② Supervisor 主管 ★ Super visor 专家1 专家2 专家3 中心协调者分发任务 ③ Hierarchical 层级 顶层 组长1 组长2 W W W W 树形层级(AD2 详讲)
图 AD1.1 · 三种多 Agent 拓扑:网络(去中心)/ 主管(星形)/ 层级(树形)
拓扑通信方式优点缺点典型场景
Network任意 Agent 间直接通信灵活、无单点协调复杂、易混乱Agent 数量少且对等
Supervisor都经中心 Supervisor清晰、可控、易调试Supervisor 是瓶颈/单点最常用,分工明确的任务
Hierarchical树形逐层下发可伸缩、隔离好层级深时延迟高大型复杂项目

Supervisor 的工作机制

Supervisor 拓扑的核心是一个协调者 Agent,它不直接干活,而是决定"把任务派给哪个专家 Agent":

Supervisor 编排流程 用户任务 Supervisor Agent LLM 决定派给谁 路由 研究 Agent 编码 Agent 测试 Agent 结果回流 Supervisor → 再决策(继续派/结束) 回流
图 AD1.2 · Supervisor 用 LLM 做路由决策,专家 Agent 执行后结果回流

这本质上是把"路由"也变成一次 LLM 调用——Supervisor 是一个特殊的 Agent,它的"工具"就是其他 Agent。注意它和 LG3 条件边的区别:条件边是硬编码的路由逻辑,Supervisor 是LLM 动态决策的路由,更灵活但更贵。

create_agent 与 middleware 系统

现代 LangChain 用 create_agent(替代旧的 create_react_agent)构建 Agent,它的扩展点是 middleware。先精读签名:

📄 langchain/agents/factory.py:787 · create_agent 签名 python
def create_agent(
    model: str | BaseChatModel,
    tools: Sequence[BaseTool | Callable | dict] | None = None,
    *,
    system_prompt: str | SystemMessage | None = None,
    middleware: Sequence[AgentMiddleware] = (),     # ★ 扩展点:中间件链
    response_format: ... = None,
    state_schema: type[AgentState] | None = None,
    context_schema: type[ContextT] | None = None,
    checkpointer: Checkpointer | None = None,
    store: BaseStore | None = None,
    interrupt_before: list[str] | None = None,
    interrupt_after: list[str] | None = None,
    name: str | None = None,                         # ★ name 用于子图嵌入
    ...
) -> CompiledStateGraph:
    """创建一个循环调用工具直到停止的 agent 图。"""
⚠️ 关键:create_agent 产物是 CompiledStateGraph

注意返回类型是 CompiledStateGraph(继承 Pregel)。这意味着每个 create_agent 的产物本身就是一个图,而图可以作为另一个图的节点——这就是多 Agent 嵌套的基础。name 参数(factory.py:893 doc 明确说)"自动用作把 agent 加进另一个图作为子图节点,对构建多 Agent 系统特别有用"。

middleware:能力增强的钩子链

middleware 不是多 Agent 编排,而是单 Agent 的能力增强。它提供一组生命周期钩子:

内置 middleware作用
HumanInTheLoopMiddleware人在回路(工具执行前批准)
SummarizationMiddleware自动摘要历史(上下文管理)
ModelFallbackMiddleware模型降级(主模型失败换备用)
ToolCallLimitMiddleware限制工具调用次数
TodoListMiddleware自动维护待办列表

钩子点(factory.py:1062):before_agent / before_model / after_model / after_agent / wrap_model_call / wrap_tool_call。理解这套钩子是写自定义 middleware 的基础。

子图与 Command.PARENT 通信

多 Agent 的核心机制是子图嵌套。一个编译好的图可以作为另一个图的节点。关键是父子图如何通信——Command(graph=Command.PARENT)

📄 langgraph/types.py:808 · Command.PARENT 常量 python
@dataclass
class Command(Generic[N], ToolOutputMixin):
    """更新图状态并发送消息的指令。"""

    graph: GraphType = None
    """发送到哪个图:
    - None:当前图
    - Command.PARENT:最近的父图  ← 子图向父图发命令"""

    update: ... = None      # 状态更新
    resume: ... = None      # 恢复中断
    goto: ... = None        # 跳转到节点

    PARENT: ClassVar[Literal["__parent__"]] = "__parent__"   # :808 常量定义
📄 graph/state.py:1747 · PARENT 命令的运行时处理 python
# 在 _control_branch 内:
if command.graph == Command.PARENT:
    raise ParentCommand(command)
    # 子图无法自己处理 PARENT 命令,所以【抛异常】
    # 异常会冒泡到父图,由父图的 _get_root (state.py:1780/1791) 接收处理
    # 这就是子→父通信的底层机制
父图 父节点 子图(作为节点嵌入) 子节点执行 Command.PARENT ↑ 异常冒泡到父图,父图接收处理
图 AD1.3 · 子图通过 Command.PARENT 向父图发命令(异常冒泡机制)

重要澄清:create_supervisor 不在主仓库

⚠️ 源码事实核对

很多教程提到 from langgraph.prebuilt import create_supervisor——但本仓库的 prebuilt 不包含它prebuilt/__init__.py:14__all__ 只有 create_react_agent / ToolNode 等)。create_supervisor / create_swarm / create_handoff独立的 contrib 包langgraph-supervisorlanggraph-swarm,需 pip install)。

本教程选择手写 Supervisor,这样你能真正理解机制,而不是调一个黑盒函数。理解原理后,用 contrib 包只是语法糖。

实战:手写一个 Supervisor 图

🚀 完整可运行的 Supervisor

pip install langgraph langchain-openai。这是一个研究-编码-测试三专家协作的 Supervisor 系统。

📄 ad1_supervisor.py · 手写 Supervisor 编排(完整可运行) python
# pip install langgraph langchain-openai
# export OPENAI_API_KEY="sk-..."
import operator
from typing import Annotated, Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4o-mini")

# ============ ① 定义共享状态 ============
class State(TypedDict):
    messages: Annotated[list, operator.add]   # 消息累积(各 Agent 的输出追加)
    next: str                                  # Supervisor 决定的下一个 Agent

# ============ ② 定义专家 Agent(用 create_react_agent 构建)============
# 每个 Agent 有自己的工具和系统提示,专注一域
research_agent = create_react_agent(
    model, tools=[],  # 实际可加搜索工具
    prompt="你是研究专家。负责调研技术方案、收集信息。简洁回答。",
)
coding_agent = create_react_agent(
    model, tools=[],  # 实际可加文件读写工具
    prompt="你是编码专家。负责写代码实现。只输出关键代码。",
)

def research_node(state: State) -> dict:
    """把专家 Agent 包成图节点。"""
    result = research_agent.invoke({"messages": state["messages"]})
    # 只取最后一条 AI 消息,标注来源
    last = result["messages"][-1]
    last.content = f"[研究专家] {last.content}"
    return {"messages": [last]}

def coding_node(state: State) -> dict:
    result = coding_agent.invoke({"messages": state["messages"]})
    last = result["messages"][-1]
    last.content = f"[编码专家] {last.content}"
    return {"messages": [last]}

# ============ ③ Supervisor 节点(用 LLM 做路由决策)============
from langchain_core.messages import HumanMessage, SystemMessage

def supervisor_node(state: State) -> dict:
    """Supervisor:用 LLM 决定下一步派给谁,或结束。"""
    decision = model.invoke([
        SystemMessage(content=(
            "你是项目主管,协调研究专家和编码专家。"
            "根据当前进度,决定下一步:\n"
            "- 需要调研 → 回复 FINISH 或 RESEARCH 或 CODE\n"
            "- 需要编码 → 回复 CODE\n"
            "- 任务完成 → 回复 FINISH\n"
            "只回复一个词。"
        )),
        *state["messages"],
    ])
    choice = decision.content.strip().upper()
    # 映射到节点名
    next_node = {"RESEARCH": "research", "CODE": "coding", "FINISH": FINISH}.get(choice, FINISH)
    return {"next": next_node}   # 条件边会读这个字段

# ============ ④ 构建图 ============
graph = StateGraph(State)
graph.add_node("supervisor", supervisor_node)
graph.add_node("research", research_node)
graph.add_node("coding", coding_node)

# 入口:先问 Supervisor
graph.add_edge(START, "supervisor")

# 条件边:Supervisor 根据 next 字段路由
def route_from_supervisor(state: State) -> str:
    return state["next"]   # 返回 "research" / "coding" / FINISH

graph.add_conditional_edges(
    "supervisor", route_from_supervisor,
    {"research": "research", "coding": "coding", "FINISH": END},
)
# 专家执行完回到 Supervisor 继续决策(形成循环)
graph.add_edge("research", "supervisor")
graph.add_edge("coding", "supervisor")

app = graph.compile()

# ============ ⑤ 运行 ============
result = app.invoke({
    "messages": [HumanMessage(content="帮我做一个简单的待办事项 CLI 工具")],
    "next": "",
})
print("\n===== 协作过程 =====")
for m in result["messages"]:
    print(f"  {m.content[:80]}")
💡 理解这个图的结构

核心是 supervisor ↔ {research, coding} 的循环:Supervisor 决策 → 专家执行 → 回到 Supervisor 再决策,直到 Supervisor 说 FINISH。这就是 LG8 agent↔tools 循环的升级版——把"工具"换成了"专家 Agent"。每个专家有自己的上下文和工具集,互不干扰。

用 contrib 包简化(了解)

理解原理后,可以用 contrib 包简化上面的代码:

📄 用 langgraph-supervisor 简化(需额外安装) python
# pip install langgraph-supervisor  ← 独立包,不在主仓库
from langgraph_supervisor import create_supervisor
# create_supervisor 把上面的手写逻辑封装成一行
# 但理解了手写版,你才知道它在做什么

进阶模式

1. 共享状态 vs 私有状态

上面例子所有 Agent 共享 messages。生产中常需要私有状态(每个 Agent 有自己的中间变量)。用子图 schema隔离:

📄 子图的独立状态 schema python
# 父图状态
class ParentState(TypedDict):
    messages: Annotated[list, operator.add]
    next: str

# 子图状态(可以和父图不同!只暴露必要字段)
class ResearchState(TypedDict):
    messages: Annotated[list, operator.add]
    research_notes: list   # ← 私有字段,不回传父图

# 子图编译时,只共享 schema 重叠的字段(messages)

2. 并行派发多个专家

用 LG3 的 Send 让 Supervisor 一次派给多个专家并行:

📄 并行派发(Supervisor + Send) python
def supervisor_with_parallel(state):
    """Supervisor 决定并行派给多个专家。"""
    return [
        Send("research", {"messages": state["messages"]}),
        Send("coding", {"messages": state["messages"]}),
    ]   # 两个专家并发执行,结果由 operator.add reducer 聚合

与生产实践对照

Supervisor 概念OpenCode 对应
Supervisor 路由决策主 build agent 用 LLM 决定调 task 工具(OC2 runLoop)
专家 Agent 子图explore/general 子 agent(agent.ts:182/196)
create_agent 产物=图每个内置 agent 是一套配置(不是图,是 runLoop 参数)
Command.PARENT 子→父task 工具结果回流主会话(task.ts:199)
middleware 增强compaction/permission/subagent 等横切逻辑
🧭 OpenCode 是"隐式 Supervisor"

OpenCode 的 build agent 本质就是 Supervisor——它自己不直接探索代码库,而是调用 task 工具派生 explore 子 agent去探索(OC5)。区别是:OpenCode 的"专家 Agent"是通过工具调用隐式触发的(LLM 决定调 task),而本节的 Supervisor 是显式图结构。两种风格各有优劣——图结构更可控可观测,工具调用更灵活自然。

小结

下一章

下一章 AD2 · 多 Agent:层级树 + Handoff:深入层级拓扑与 Handoff(控制权移交)语义,精读 OpenCode task 工具如何建立父子会话树、防递归、回流结果。