分而治之——Supervisor 多 Agent 模式与子图 — LangGraph 实战——构建跨平台爆款图文 Agent 第6篇
第6章:分而治之——Supervisor 多 Agent 模式与子图
本章目标
读完本章你会:
- 能解释为什么单个巨型 Agent 不如多个专业 Agent 协作
- 能用 Supervisor 模式 设计一个中央调度器 + N 个专业子 Agent 的架构
- 能用子图(Subgraph) 把独立 Agent 封装为可复用的图模块
- 能处理好父图与子图之间的 State 共享
- 能将 LocalTrend 拆分为 Researcher → Analyst → Writer → Publisher 四角色协作系统
知识讲解
从一个生活例子开始
一支交响乐团正在演奏。台上没有一个人包办所有乐器——指挥站在中央,弦乐组在左边,管乐组在右边,打击乐组在后面。
指挥的工作不是自己拉小提琴或吹长号。他的工作是:
- 读懂总谱,知道每个乐器组在什么时候该干什么
- 给弦乐组一个手势——"你们进"
- 听弦乐组的表现,决定"够了,管乐组接上"
- 调整各组的强弱、速度、情绪
每个乐器组内部有自己的协作方式——弦乐组的首席小提琴会协调组内弓法。指挥不需要关心这些细节——他只管"弦乐组,第三乐章,起"。
这就是 Supervisor 多 Agent 模式:
| 交响乐团 | LangGraph |
|---|---|
| 指挥 | Supervisor Agent——中央调度器,决定"下一个该谁" |
| 弦乐组 | Researcher Agent——一个独立的子图,负责搜索和收集 |
| 管乐组 | Analyst Agent——分析搜索结果,提炼规律 |
| 打击乐组 | Writer Agent——根据分析结果撰写内容 |
| 总谱 | 共享 State——所有子 Agent 读写同一份状态 |
关键洞察:指挥不需要会吹长号,Researcher 不需要会写文章。 每个子 Agent 只做自己擅长的事,Supervisor 负责把它们串成一个完整的作品。
工作原理
为什么需要多 Agent?
看看你现在的 LocalTrend——第 5 章的 build_multi_platform_graph() 把所有逻辑塞在一个图里:搜索、分析、生成、审核全混在一起。随着功能增长:
- State 膨胀:
platform_reports、generated_content、review_decision、published……20 多个字段,没人记得住 - 节点膨胀:10+ 个节点,每个节点要理解整个 State 的上下文
- 修改困难:想给"搜索"加个重试逻辑?你得在图中间插入新节点和边——牵一发动全身
多 Agent 架构解决的就是这个问题:把大图拆成小图,每个小图只关心自己领域的状态子集。
Supervisor 模式的结构
┌─────────────┐
│ Supervisor │ ← 中央调度器(路由 Agent)
└──┬──┬──┬───┘
│ │ │
┌────────────┘ │ └────────────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────────┐
│Researcher│ │ Analyst │ │ Writer │ ← 专业子 Agent
│ (子图) │ │ (子图) │ │ (子图) │ 每个是独立编译的图
└──────────┘ └──────────┘ └──────────────┘
↓ ↓ ↓
└───────────────┴───────────────┘
↓
共享 State(所有子图读写同一份)
Supervisor 本身通常是一个带 LLM 的路由节点——它分析当前 State,决定"下一步该调用哪个子 Agent"。它的路由函数类似:
def supervisor_router(state) -> str:
# 检查当前进度,决定下一步
if 还没搜索 → "researcher"
elif 搜索完了还没分析 → "analyst"
elif 分析完了还没写 → "writer"
elif 写完了还没发布 → "publisher"
else → END
子图与父图的 State 关系
这是一个容易踩坑的地方。LangGraph 中子图和父图的 State 处理规则:
- 子图必须使用与父图相同的 State schema——至少包含父图 State 中它需要读写的字段
- 子图可以直接读取和修改共享 State——子图的节点就像父图节点一样读写 State
- 子图内部的状态变更在子图完成后才对父图可见——子图执行期间,父图看到的是子图开始前的快照
⚠️ 常见坑:初学者容易给子图定义一个"精简版"State(只包含自己需要的字段),然后发现父图传给子图时丢失了其他字段。解决方式:所有子图使用和父图完全相同的 State 类,或确保子图 State 是父图 State 的完整超集。
思考一下: 如果 Researcher 子图在搜索时改了 messages,Analyst 子图能看到这些消息吗?能——因为它们共享同一个 State。那为什么还要用子图?因为子图把"搜索相关的节点和边"封装成一个可理解的单元——Analyst 不需要知道 Researcher 内部有几个节点、怎么路由的,只需要知道"Researcher 会在 messages 里追加搜索结果"。
代码实战
基础版:两子 Agent + Supervisor 的最简模式
先从最简单的 Supervisor 开始——一个 MathAgent 做计算,一个 TextAgent 做文本处理,Supervisor 判断该找谁。新建文件 chapter06_supervisor_basics.py:
"""
第 6 章 基础演示:Supervisor 多 Agent 最简模式
一个 Supervisor 调度两个子 Agent——MathAgent 和 TextAgent
"""
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.messages import HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
import os
# ============================================================
# 共享 State——所有子图 + 父图使用同一个 schema
# ============================================================
class TeamState(TypedDict):
messages: Annotated[list, add_messages]
next_agent: str # Supervisor 决定下一个调谁
task_done: bool # 任务完成标记
# ============================================================
# 子图 1:MathAgent——一个独立的 StateGraph
# ============================================================
def create_math_agent():
"""处理数学计算的专业 Agent。对外部来说它就是一个黑盒节点。"""
def math_solver(state: TeamState) -> dict:
"""计算节点——内部可能有 own LLM 调用、工具调用等复杂逻辑"""
last_msg = state["messages"][-1]
content = last_msg.content if hasattr(last_msg, "content") else str(last_msg)
# 模拟数学计算(实际项目这里会调 LLM + 计算器工具)
print(f" [MathAgent] 处理计算请求: {content[:60]}")
# 简单计算演示
result = "计算结果:根据数据,线性回归斜率约为 2.35,R² = 0.87。"
return {
"messages": [AIMessage(content=f" MathAgent: {result}")],
"task_done": True,
}
builder = StateGraph(TeamState)
builder.add_node("math_solver", math_solver)
builder.add_edge(START, "math_solver")
builder.add_edge("math_solver", END)
return builder.compile()
# ============================================================
# 子图 2:TextAgent——另一个独立 StateGraph
# ============================================================
def create_text_agent():
"""处理文本分析的专业 Agent"""
def text_analyzer(state: TeamState) -> dict:
last_msg = state["messages"][-1]
content = last_msg.content if hasattr(last_msg, "content") else str(last_msg)
print(f" [TextAgent] 处理文本请求: {content[:60]}")
result = "文本分析结果:该段落的情感倾向为正面(置信度 0.92),关键词包括'增长'、'创新'、'突破'。"
return {
"messages": [AIMessage(content=f" TextAgent: {result}")],
"task_done": True,
}
builder = StateGraph(TeamState)
builder.add_node("text_analyzer", text_analyzer)
builder.add_edge(START, "text_analyzer")
builder.add_edge("text_analyzer", END)
return builder.compile()
# ============================================================
# 父图:Supervisor + 子图
# ============================================================
def build_supervisor_graph():
builder = StateGraph(TeamState)
# 把编译好的子图作为节点注册到父图中
builder.add_node("math_agent", create_math_agent())
builder.add_node("text_agent", create_text_agent())
# Supervisor 节点——用 LLM 决定路由
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY", "your-api-key-here"),
base_url="https://api.deepseek.com",
temperature=0,
)
def supervisor(state: TeamState) -> dict:
"""中央调度器:分析请求,决定调哪个 Agent"""
last_msg = state["messages"][-1]
content = last_msg.content if hasattr(last_msg, "content") else str(last_msg)
print(f"\n[Supervisor] 分析请求: {content[:80]}")
# 让 LLM 判断该找谁
response = llm.invoke([
HumanMessage(content=(
f"用户的请求是:\n{content}\n\n"
f"你需要决定调用哪个 Agent:\n"
f"- 如果请求涉及数学计算、数据分析、数值统计 → 回复 'math'\n"
f"- 如果请求涉及文本分析、情感分析、关键词提取 → 回复 'text'\n"
f"- 如果已经处理完了 → 回复 'done'\n\n"
f"只回复一个词: math 或 text 或 done。"
))
])
choice = response.content.strip().lower()
print(f" ↳ Supervisor 决策: {choice}")
return {"next_agent": choice}
def supervisor_router(state: TeamState) -> Literal["math_agent", "text_agent", "__end__"]:
"""条件路由:根据 supervisor 的决策分发"""
choice = state.get("next_agent", "")
if choice == "math":
return "math_agent"
elif choice == "text":
return "text_agent"
return END
builder.add_node("supervisor", supervisor)
builder.add_edge(START, "supervisor")
# 条件路由到不同子图
builder.add_conditional_edges(
"supervisor", supervisor_router,
{"math_agent": "math_agent", "text_agent": "text_agent", END: END}
)
# 子图完成后回到 supervisor,让它决定"下一步"
builder.add_edge("math_agent", "supervisor")
builder.add_edge("text_agent", "supervisor")
return builder.compile()
# ============================================================
# 运行
# ============================================================
if __name__ == "__main__":
graph = build_supervisor_graph()
print("=" * 60)
print("测试 1:数学问题")
print("=" * 60)
result = graph.invoke({
"messages": [HumanMessage(content="帮我分析这组数据: [12, 18, 25, 33, 41],拟合线性回归")],
"next_agent": "",
"task_done": False,
})
# 打印所有消息,观察 MathAgent 和 TextAgent 各被调了几次
for msg in result["messages"]:
role = type(msg).__name__
content = str(msg.content)[:80]
print(f" [{role}] {content}")
print("\n" + "=" * 60)
print("测试 2:文本问题")
print("=" * 60)
result2 = graph.invoke({
"messages": [HumanMessage(content="分析这段文字的情感:产品增长迅猛,用户反馈积极。")],
"next_agent": "",
"task_done": False,
})
for msg in result2["messages"]:
print(f" [{type(msg).__name__}] {str(msg.content)[:80]}")
逐行解析
builder.add_node("math_agent", create_math_agent())
这就是子图的关键用法。create_math_agent() 返回一个 compile() 过的图对象——这个对象可以像普通节点函数一样被 add_node 注册。当 Supervisor 路由到 "math_agent" 时,LangGraph 会把整个子图作为一个"超级节点"来执行:START → math_solver → END,然后返回到父图的边。
子图和父图使用相同的 State 类 (TeamState)
这是 State 兼容性的保证。子图的所有节点都读写 TeamState,和父图共享同一份数据。MathAgent 在 messages 里追加的内容,TextAgent 和 Supervisor 都能看到。
子图完成后回到 Supervisor
builder.add_edge("math_agent", "supervisor") 和 builder.add_edge("text_agent", "supervisor") 是关键设计——每个子 Agent 完成工作后,控制权交回 Supervisor。Supervisor 再次评估当前 State,决定"下一步"——可能调用另一个 Agent,也可能结束。
这形成了一个循环:Supervisor → 子 Agent → Supervisor → 子 Agent → ... → END。 不同于第 2 章的 Agent ↔ Tool 循环,这里的循环粒度更大——每次循环是一个完整的子任务,而非单个工具调用。
扩展版:LocalTrend 四 Agent 团队
现在把 LocalTrend 正式升级为多 Agent 架构。四个专业子 Agent:
- Researcher:接收话题,搜索平台趋势
- Analyst:分析搜索结果,提炼爆款规律
- Writer:基于规律撰写爆款内容
- Publisher:审核(interrupt)并发布
新建文件 chapter06_localtrend_team.py:
"""
第 6 章 扩展演示:LocalTrend 多 Agent 团队
Researcher → Analyst → Writer → Publisher 四角色协作
"""
import os
import sqlite3
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
# ============================================================
# 配置
# ============================================================
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY", "your-api-key-here"),
base_url="https://api.deepseek.com",
temperature=0.7,
)
# ============================================================
# 共享 State——所有子 Agent 使用此 schema
# ============================================================
class TeamState(TypedDict):
# 消息历史(所有 Agent 共享)
messages: Annotated[list, add_messages]
# Supervisor 控制
next_agent: str
# Researcher 产出
research_results: str
# Analyst 产出
trend_patterns: str
# Writer 产出
generated_content: str
# Publisher 状态
review_decision: str
review_feedback: str
published: bool
# ============================================================
# 工具(仅 Researcher 使用)
# ============================================================
@tool
def search_trending(topic: str) -> str:
"""搜索热门话题趋势"""
print(f" [工具] 搜索: {topic}")
knowledge = {
"AI": "AI爆款:Agent开发(CTR 12%)、RAG实战(完读率45%)、LLM应用架构(收藏率高)。标题公式:'从零'/'实战'/'2026最新'。",
"职场": "职场爆款:'AI时代核心竞争力'(情绪共鸣强)、'副业月入复盘'(转化率高)、'中年转型'(评论互动多)。",
}
for key, val in knowledge.items():
if key in topic:
return val
return f"关于'{topic}'的趋势分析数据。"
# ============================================================
# 子图 1: Researcher Agent
# ============================================================
def create_researcher():
"""搜索 Agent——负责搜索和收集原始数据"""
llm_with_tools = llm.bind_tools([search_trending])
def research_node(state: TeamState) -> dict:
print("\n [Researcher] 开始搜索趋势...")
# 从消息历史中提取用户的原始话题
user_requests = [
m for m in state["messages"]
if isinstance(m, HumanMessage)
]
topic = user_requests[-1].content if user_requests else "AI"
response = llm_with_tools.invoke([
HumanMessage(content=f"搜索'{topic}'的爆款趋势。使用搜索工具。")
])
results = []
if hasattr(response, "tool_calls") and response.tool_calls:
for tc in response.tool_calls:
result = search_trending.invoke(tc["args"])
results.append(result)
research_summary = "\n".join(results) if results else response.content
print(f" ✅ [Researcher] 完成,收集到 {len(results)} 条结果")
return {
"messages": [response],
"research_results": research_summary,
}
builder = StateGraph(TeamState)
builder.add_node("research_node", research_node)
builder.add_edge(START, "research_node")
builder.add_edge("research_node", END)
return builder.compile()
# ============================================================
# 子图 2: Analyst Agent
# ============================================================
def create_analyst():
"""分析 Agent——从原始数据中提炼爆款规律"""
def analyze_node(state: TeamState) -> dict:
print("\n [Analyst] 分析搜索结果,提炼规律...")
research = state.get("research_results", "")
if not research:
research = "无搜索结果"
response = llm.invoke([
HumanMessage(content=(
f"基于以下搜索数据,提炼 3 条跨平台爆款规律:\n\n{research}\n\n"
f"每条规律包含:标题模式、内容结构、互动设计"
))
])
print(f" ✅ [Analyst] 分析完成")
return {
"messages": [AIMessage(content=f" 分析结果:\n{response.content}")],
"trend_patterns": response.content,
}
builder = StateGraph(TeamState)
builder.add_node("analyze_node", analyze_node)
builder.add_edge(START, "analyze_node")
builder.add_edge("analyze_node", END)
return builder.compile()
# ============================================================
# 子图 3: Writer Agent
# ============================================================
def create_writer():
"""写作 Agent——根据规律生成爆款内容"""
def write_node(state: TeamState) -> dict:
print("\n ✍️ [Writer] 基于规律撰写内容...")
patterns = state.get("trend_patterns", "")
if not patterns:
patterns = "通用爆款规律:痛点切入+分点论述+案例支撑+互动引导"
# 如果有驳回意见,加入上下文
feedback = state.get("review_feedback", "")
feedback_context = f"上一版被驳回,意见:{feedback}。请根据意见改进。" if feedback else ""
response = llm.invoke([
HumanMessage(content=(
f"根据以下爆款规律,为微信公众号撰写一篇 200 字的短文:\n\n{patterns}\n\n"
f"{feedback_context}\n"
f"要求:标题含数字+情感词、开头痛点切入、结尾互动引导。"
))
])
print(f" ✅ [Writer] 内容生成完成({len(response.content)} 字)")
return {
"messages": [AIMessage(content=f"✍️ 生成内容:\n{response.content}")],
"generated_content": response.content,
"review_feedback": "", # 清除旧驳回意见
}
builder = StateGraph(TeamState)
builder.add_node("write_node", write_node)
builder.add_edge(START, "write_node")
builder.add_edge("write_node", END)
return builder.compile()
# ============================================================
# 子图 4: Publisher Agent
# ============================================================
def create_publisher():
"""发布 Agent——审核+发布(含 Human-in-the-Loop)"""
def review_node(state: TeamState) -> dict:
print("\n [Publisher] 内容审核...")
content = state.get("generated_content", "")
print(f" {'='*50}")
print(f" {content[:200]}")
print(f" {'='*50}")
decision = interrupt("请审核:approve(批准) / 输入修改意见")
if decision.lower() in ("approve", "批准"):
print(" ✅ 审核通过,准备发布")
return {
"review_decision": "approved",
"published": True,
}
else:
print(f" ❌ 驳回,意见: {decision}")
return {
"review_decision": "rejected",
"review_feedback": decision,
"published": False,
}
builder = StateGraph(TeamState)
builder.add_node("review_node", review_node)
builder.add_edge(START, "review_node")
builder.add_edge("review_node", END)
return builder.compile()
# ============================================================
# 父图:Supervisor + 四个子 Agent
# ============================================================
def build_localtrend_team(db_path="localtrend_team.db"):
builder = StateGraph(TeamState)
# 注册四个子图
builder.add_node("researcher", create_researcher())
builder.add_node("analyst", create_analyst())
builder.add_node("writer", create_writer())
builder.add_node("publisher", create_publisher())
# Supervisor 节点
supervisor_llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY", "your-api-key-here"),
base_url="https://api.deepseek.com",
temperature=0,
)
def supervisor(state: TeamState) -> dict:
"""中央调度器:根据当前进度决定下一步"""
research = state.get("research_results", "")
patterns = state.get("trend_patterns", "")
content = state.get("generated_content", "")
published = state.get("published", False)
# 构建状态摘要给 LLM
status = f"""
当前任务进度:
- 搜索完成: {'是' if research else '否'}
- 分析完成: {'是' if patterns else '否'}
- 内容生成: {'是' if content else '否'}
- 已发布: {'是' if published else '否'}
- 最近驳回意见: {state.get('review_feedback', '无')}
请决定下一步调用哪个 Agent:
- 搜索未完成 → researcher
- 搜索完成、分析未完成 → analyst
- 分析完成、内容未生成 → writer
- 内容生成但驳回(review_feedback 非空)→ writer(重写)
- 内容生成且有驳回意见 → writer(根据意见重写)
- 内容生成且审核通过 → finish
- 内容生成但未审核 → publisher
"""
response = supervisor_llm.invoke([
HumanMessage(content=(
f"{status}\n只回复一个词: researcher / analyst / writer / publisher / finish"
))
])
choice = response.content.strip().lower()
print(f"\n [Supervisor] → {choice}")
return {"next_agent": choice}
def supervisor_router(state: TeamState) -> Literal[
"researcher", "analyst", "writer", "publisher", "__end__"
]:
choice = state.get("next_agent", "")
if choice == "researcher":
return "researcher"
elif choice == "analyst":
return "analyst"
elif choice == "writer":
return "writer"
elif choice == "publisher":
return "publisher"
return END
builder.add_node("supervisor", supervisor)
builder.add_edge(START, "supervisor")
builder.add_conditional_edges(
"supervisor", supervisor_router,
{
"researcher": "researcher",
"analyst": "analyst",
"writer": "writer",
"publisher": "publisher",
END: END,
}
)
# 所有子 Agent 完成后回到 Supervisor
builder.add_edge("researcher", "supervisor")
builder.add_edge("analyst", "supervisor")
builder.add_edge("writer", "supervisor")
builder.add_edge("publisher", "supervisor")
conn = sqlite3.connect(db_path, check_same_thread=False)
return builder.compile(checkpointer=SqliteSaver(conn))
# ============================================================
# 运行演练
# ============================================================
if __name__ == "__main__":
graph = build_localtrend_team()
config = {"configurable": {"thread_id": "team-demo-1"}}
print("=" * 60)
print(" LocalTrend 团队启动")
print("=" * 60)
# 启动:Supervisor → Researcher → Analyst → Writer → Publisher(中断)
result = graph.invoke(
{
"messages": [HumanMessage(content="帮我分析 AI 领域的爆款规律,并写一篇公众号文章。")],
"next_agent": "",
"research_results": "",
"trend_patterns": "",
"generated_content": "",
"review_decision": "",
"review_feedback": "",
"published": False,
},
config
)
# 在 publisher 中断了——审核
print("\n⏸️ 等待审核...")
# 模拟场景 A:批准
print("\n--- 批准 ---")
final = graph.invoke(Command(resume="approve"), config)
print(f"\n✅ 发布状态: {'已发布' if final['published'] else '未发布'}")
# 模拟场景 B:驳回重写
print("\n\n" + "=" * 60)
print("场景 B:驳回 → Writer 重写 → 再审核 → 批准")
print("=" * 60)
config_b = {"configurable": {"thread_id": "team-demo-2"}}
graph.invoke(
{
"messages": [HumanMessage(content="分析 AI 爆款规律,写一篇小红书笔记。")],
"next_agent": "",
"research_results": "",
"trend_patterns": "",
"generated_content": "",
"review_decision": "",
"review_feedback": "",
"published": False,
},
config_b
)
print("⏸️ 审核暂停")
# 驳回
result_b = graph.invoke(
Command(resume="标题不够吸引人,加 emoji,用更短的分句"),
config_b
)
# 驳回后 Writer 重新生成 → 又到 Publisher 中断
print("⏸️ 再次暂停(重写后)")
# 批准
final_b = graph.invoke(Command(resume="approve"), config_b)
print(f"✅ {'已发布' if final_b['published'] else '未发布'}")
运行后的关键观察
- Supervisor 像一个智能调度器:它读取 State 中的进度标记(
research_results、trend_patterns等),决定"下一步该谁" - 驳回后的重写是自动的:Supervisor 看到
review_feedback非空,自动路由回writer——不需要手动控制 - 每个子 Agent 的代码清晰独立:
create_researcher()的代码不超过 30 行,职责单一。修改 Researcher 的逻辑不会影响 Analyst
本章小结
- Supervisor 模式 = 中央调度器 + N 个专业子 Agent——每个子 Agent 是独立编译的图,负责一个明确的任务域。
- 子图作为节点注册:
builder.add_node("name", create_subgraph())——子图对外表现为一个"超级节点"。 - 父图和子图必须共享相同的 State schema——否则字段会丢失或冲突。
- 子 Agent 完成后回到 Supervisor——循环决策直到任务完成。这是 ReAct 循环的高层次版本。
- 各子 Agent 可以使用不同的 LLM 模型——根据任务需求(成本、质量、速度)灵活选择。
- 子图内部可以使用 LangGraph 全部特性——Send 并行、interrupt 审核、条件边,都可以在子图内部使用。
关键术语
| 术语 | 释义 |
|---|---|
| Supervisor 模式 | 一个中央调度 Agent 根据当前进度决定调用哪个子 Agent 的多 Agent 架构 |
| 子图(Subgraph) | 一个独立编译的 StateGraph,作为父图的节点使用 |
| Agent 团队 | 多个专业子 Agent 通过共享 State 协作完成复杂任务的架构 |
| Agent 路由 | Supervisor 根据 State 中的进度标记(research_results、trend_patterns 等)决定下一个调谁 |
| 共享 State | 父图和所有子图使用相同的 State schema,确保数据无缝流转 |

浙公网安备 33010602011771号