Appearance
多智能体协作平台
本实战构建一个 Supervisor + 多 Worker 的多智能体协作平台。场景是"内容创作团队":策划出题、研究找资料、写作成稿、审校把关,四个角色各司其职,由 Supervisor 智能体统一调度。这是 Supervisor 模式 在真实业务里的落地。
一、需求分析
内容创作是个典型的多角色协作流程:
- 策划(Planner):把用户主题拆成创作大纲和要点。
- 研究(Researcher):按大纲搜集资料。
- 写作(Writer):综合资料写成初稿。
- 审校(Reviewer):检查质量,不合格打回重写。
这些角色如果让一个智能体全干,prompt 会臃肿、容易顾此失彼。拆成多个 Worker,由一个 Supervisor 决定"下一步交给谁",每个 Worker 专注自己的事——这就是 Supervisor 模式的价值。
二、架构设计
mermaid
flowchart TD
U([用户主题]) --> SUP[Supervisor 调度器]
SUP -- 交给策划 --> P[Planner]
SUP -- 交给研究 --> R[Researcher]
SUP -- 交给写作 --> W[Writer]
SUP -- 交给审校 --> RV[Reviewer]
P --> SUP
R --> SUP
W --> SUP
RV --> SUP
SUP -- 完成 --> E([END])Supervisor 是中枢:每次收到某个 Worker 的产出后,决定下一步交给谁,直到任务完成。所有 Worker 共享一个状态,Supervisor 根据状态里的"当前进度"做路由决策。
对 Java/LangGraph4j 同学:这相当于一个调度中心 + 多个职能服务,调度中心根据当前业务状态路由到不同服务,跟工作流引擎的"人工节点路由"类似。
三、状态设计
共享状态是协作的枢纽。设计原则:只放所有 Worker 都需要读写的字段,避免职责污染。
python
from typing import TypedDict, List, Annotated, Optional
def append_str(left: str, right: str) -> str:
"""字符串追加 reducer:每个 worker 把自己的产出追加到 outputs"""
if not right:
return left or ""
return (left or "") + "\n---\n" + right
class TeamState(TypedDict):
task: str # 用户原始主题/任务
outline: str # 策划产出的大纲
materials: str # 研究产出的资料
draft: str # 写作产出的初稿
review: str # 审校产出的意见
outputs: Annotated[str, append_str] # 所有角色产出的日志(追加)
current_worker: str # Supervisor 决定的下一个 worker
done: bool # 任务是否完成
step_count: int # 步骤计数,防死循环
messages: Annotated[list, "add_messages"] # 可选:对话历史注意 outputs: Annotated[str, append_str]——每个 Worker 把自己的产出追加进来,形成完整执行日志。step_count 是死循环刹车。
四、Worker 实现
每个 Worker 是一个普通节点函数,读自己关心的状态字段,写自己的产出字段。为演示清晰,这里用 Mock 核心能力(真实项目里 Worker 可以是子图或 create_react_agent)。
1. 策划 Worker
python
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def planner(state: TeamState) -> dict:
"""策划:把主题拆成大纲"""
task = state["task"]
outline = llm.invoke(
f"你是内容策划。把下面主题拆成一份创作大纲,含3-5个要点和写作方向:\n主题:{task}"
).content
return {
"outline": outline,
"outputs": f"[策划] 已产出大纲",
"step_count": state.get("step_count", 0) + 1,
}2. 研究 Worker
python
def researcher(state: TeamState) -> dict:
"""研究:根据大纲搜集资料(Mock)"""
outline = state.get("outline", "")
# Mock 资料搜集;真实项目接向量库或 web 搜索
materials = llm.invoke(
f"你是研究员。根据大纲搜集相关资料要点,3-5条简明要点:\n大纲:{outline}"
).content
return {
"materials": materials,
"outputs": f"[研究] 已产出资料",
"step_count": state.get("step_count", 0) + 1,
}3. 写作 Worker
python
def writer(state: TeamState) -> dict:
"""写作:综合大纲和资料写初稿"""
outline = state.get("outline", "")
materials = state.get("materials", "")
draft = llm.invoke(
f"你是写手。根据大纲和资料写一篇800字左右的初稿:\n"
f"大纲:{outline}\n\n资料:{materials}"
).content
return {
"draft": draft,
"outputs": f"[写作] 已产出初稿",
"step_count": state.get("step_count", 0) + 1,
}4. 审校 Worker
python
def reviewer(state: TeamState) -> dict:
"""审校:检查初稿质量,给通过/打回意见"""
draft = state.get("draft", "")
review = llm.invoke(
f"你是审校。检查初稿质量,输出'PASS'或'REWRITE: 具体问题':\n初稿:{draft}"
).content
return {
"review": review,
"outputs": f"[审校] {review[:30]}",
"step_count": state.get("step_count", 0) + 1,
}五、Supervisor 实现
Supervisor 是整个平台的大脑。它用 LLM 根据当前状态决定"下一步交给谁",输出一个 worker 名字或 END。
python
from pydantic import BaseModel, Field
class Route(BaseModel):
next_worker: str = Field(description="下一个 worker:planner/researcher/writer/reviewer/FINISH")
route_llm = llm.with_structured_output(Route)
WORKERS = ["planner", "researcher", "writer", "reviewer"]
def supervisor(state: TeamState) -> dict:
"""调度器:根据当前进度决定下一步交给谁"""
# 死循环刹车:超过 8 步强制结束
if state.get("step_count", 0) >= 8:
return {"current_worker": "FINISH", "done": True}
task = state["task"]
# 把各阶段产出状态汇总给 Supervisor 判断
status = (
f"任务:{task}\n"
f"大纲:{'有' if state.get('outline') else '无'}\n"
f"资料:{'有' if state.get('materials') else '无'}\n"
f"初稿:{'有' if state.get('draft') else '无'}\n"
f"审校意见:{state.get('review', '无')}\n"
)
prompt = (
"你是创作团队调度员。根据当前进度决定下一步交给谁。\n"
"可选:planner(策划) / researcher(研究) / writer(写作) / reviewer(审校) / FINISH(完成)\n"
"正常流程:planner → researcher → writer → reviewer;"
"若审校结果是 REWRITE,回到 writer;若 PASS,FINISH。\n\n"
f"{status}\n只输出下一个 worker 名字。"
)
result = route_llm.invoke(prompt)
nxt = result.next_worker.strip()
if nxt not in WORKERS + ["FINISH"]:
nxt = "FINISH" # 兜底
return {"current_worker": nxt, "done": nxt == "FINISH"}Supervisor 的 prompt 是协作质量的关键——它要懂"正常流程"和"打回重做"两种路径。本例把审校的 PASS/REWRITE 信号传给它,让它能据此路由。
六、条件边与路由
Supervisor 之后用条件边把控制权交给被选中的 Worker;Worker 执行完回到 Supervisor,形成循环。
python
def route_from_supervisor(state: TeamState) -> str:
"""根据 Supervisor 决定路由到哪个 worker,或结束"""
nxt = state["current_worker"]
if nxt == "FINISH" or state.get("done"):
return END
return nxt七、组装图
python
from langgraph.graph import StateGraph, START, END
gb = StateGraph(TeamState)
gb.add_node("supervisor", supervisor)
gb.add_node("planner", planner)
gb.add_node("researcher", researcher)
gb.add_node("writer", writer)
gb.add_node("reviewer", reviewer)
gb.add_edge(START, "supervisor")
# Supervisor → 某个 worker 或 END
gb.add_conditional_edges("supervisor", route_from_supervisor, {
"planner": "planner",
"researcher": "researcher",
"writer": "writer",
"reviewer": "reviewer",
END: END,
})
# 每个 worker 执行完回到 supervisor
for w in WORKERS:
gb.add_edge(w, "supervisor")
team_app = gb.compile()完整协作图:
mermaid
flowchart TD
S([START]) --> SUP[supervisor]
SUP -- planner --> P[planner]
SUP -- researcher --> R[researcher]
SUP -- writer --> W[writer]
SUP -- reviewer --> RV[reviewer]
SUP -- FINISH --> E([END])
P --> SUP
R --> SUP
W --> SUP
RV --> SUP八、完整可运行示例
python
import os
from typing import TypedDict, List, Annotated
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ---- 状态 ----
def append_str(left, right):
if not right: return left or ""
return (left or "") + "\n---\n" + right
class TeamState(TypedDict):
task: str
outline: str
materials: str
draft: str
review: str
outputs: Annotated[str, append_str]
current_worker: str
done: bool
step_count: int
# ---- 结构化路由 ----
class Route(BaseModel):
next_worker: str
WORKERS = ["planner", "researcher", "writer", "reviewer"]
# ---- Workers ----
def planner(state):
o = llm.invoke(f"你是策划。把主题拆成大纲含3-5要点:\n{state['task']}").content
return {"outline": o, "outputs": "[策划]大纲完成", "step_count": state.get("step_count",0)+1}
def researcher(state):
m = llm.invoke(f"你是研究员。根据大纲搜3-5条资料要点:\n{state.get('outline','')}").content
return {"materials": m, "outputs": "[研究]资料完成", "step_count": state.get("step_count",0)+1}
def writer(state):
d = llm.invoke(f"你是写手。根据大纲和资料写800字初稿:\n大纲:{state.get('outline','')}\n资料:{state.get('materials','')}").content
return {"draft": d, "outputs": "[写作]初稿完成", "step_count": state.get("step_count",0)+1}
def reviewer(state):
r = llm.invoke(f"你是审校。检查初稿,输出'PASS'或'REWRITE:问题':\n{state.get('draft','')}").content
return {"review": r, "outputs": f"[审校]{r[:30]}", "step_count": state.get("step_count",0)+1}
# ---- Supervisor ----
def supervisor(state):
if state.get("step_count",0) >= 8:
return {"current_worker":"FINISH","done":True}
status = (f"任务:{state['task']}\n大纲:{'有' if state.get('outline') else '无'}"
f"\n资料:{'有' if state.get('materials') else '无'}"
f"\n初稿:{'有' if state.get('draft') else '无'}"
f"\n审校:{state.get('review','无')}")
r = llm.with_structured_output(Route).invoke(
f"你是调度员。决定下一步交给谁(planner/researcher/writer/reviewer/FINISH)。"
f"正常流:planner→researcher→writer→reviewer;REWRITE回writer;PASS则FINISH。\n{status}\n只输出名字。")
nxt = r.next_worker.strip()
if nxt not in WORKERS+["FINISH"]: nxt = "FINISH"
return {"current_worker": nxt, "done": nxt=="FINISH"}
# ---- 路由 ----
def route(state):
if state["current_worker"]=="FINISH" or state.get("done"): return END
return state["current_worker"]
# ---- 建图 ----
gb = StateGraph(TeamState)
gb.add_node("supervisor", supervisor)
for n,f in [("planner",planner),("researcher",researcher),("writer",writer),("reviewer",reviewer)]:
gb.add_node(n,f)
gb.add_edge(START,"supervisor")
gb.add_conditional_edges("supervisor", route,
{"planner":"planner","researcher":"researcher","writer":"writer","reviewer":"reviewer", END:END})
for w in WORKERS:
gb.add_edge(w, "supervisor")
app = gb.compile()
if __name__ == "__main__":
task = "写一篇面向新手的《为什么用 LangGraph 构建智能体》科普文"
result = app.invoke({"task": task, "step_count": 0, "outputs": ""})
print("=== 最终初稿 ===")
print(result.get("draft", "(无初稿)"))
print("\n=== 执行日志 ===")
print(result.get("outputs", ""))
print("\n=== 审校意见 ===")
print(result.get("review", ""))运行后,Supervisor 会依次调度 planner → researcher → writer → reviewer,如果审校说 REWRITE 就回到 writer 重写,PASS 就结束。outputs 字段记录了完整执行链路。
九、用 stream 观察调度过程
python
for chunk in app.stream({"task": task, "step_count": 0, "outputs": ""}, stream_mode="updates"):
for node, update in chunk.items():
print(f"[{node}] -> {update.get('outputs','')}")输出大致是:
text
[supervisor] -> (决定下一个: planner)
[planner] -> [策划]大纲完成
[supervisor] -> (决定下一个: researcher)
[researcher] -> [研究]资料完成
[supervisor] -> (决定下一个: writer)
[writer] -> [写作]初稿完成
[supervisor] -> (决定下一个: reviewer)
[reviewer] -> [审校]REWRITE: 结尾太仓促
[supervisor] -> (决定下一个: writer)
[writer] -> [写作]初稿完成
[supervisor] -> (决定下一个: reviewer)
[reviewer] -> [审校]PASS
[supervisor] -> (FINISH)十、扩展点
1. 加新 Worker
比如加一个"配图师":定义 illustrator 节点函数,加到 WORKERS 列表,在 Supervisor prompt 里说明何时调用,建图时 add_node + add_edge(illustrator, "supervisor")。无需改动其他 Worker,这是 Supervisor 模式的扩展性优势。
2. Worker 升级为子图或 ReAct 智能体
本例 Worker 是单次 LLM 调用。真实项目里:
- Researcher 可以是 深度研究 Agent 子图,多轮搜索。
- Writer 可以是带
retrieve_kb工具的 ReAct 智能体。 - 把子图作为节点加入:
gb.add_node("researcher", research_subgraph),详见 子图 Subgraph。
3. 人工审批节点
关键环节加 HITL:在 writer 之后插一个 human_review 节点,用 interrupt 暂停等人工批注,人工通过后 Command(resume=...) 恢复。详见 智能客服系统 的 handoff 实现和 HITL 章节。
4. 并行 Worker
研究阶段可以拆成多个子主题,用 Send 让多个 Researcher 并行(参考 深度研究 Agent 的并行搜索)。Supervisor 决定"扇出研究",并行完成后汇总回 Supervisor。
十一、常见踩坑
1. 职责边界模糊
Worker 之间职责重叠(比如 Writer 也去查资料、Researcher 也写两句)会导致产出互相覆盖、Supervisor 路由混乱。解决:
- 每个 Worker 只写自己专属的字段(planner 写 outline、researcher 写 materials...),不碰别人的。
- prompt 里明确角色边界:"只做 X,不做 Y"。
- 用
outputs日志字段追溯谁产出了什么,便于排查。
2. 死循环
Supervisor 可能在 writer ↔ reviewer 之间反复横跳,或一直不判 FINISH。本例用 step_count >= 8 强制结束兜底。其他手段:
- 限制 REWRITE 次数(如最多 2 次),超过强制 PASS。
- Supervisor prompt 里强调"质量达标即可 FINISH,不要过度追求完美"。
- 监控单次任务总 token,超预算强制结束。
3. 状态字段污染
共享状态字段一多,就容易"这个 Worker 改了那个 Worker 依赖的字段"。本例 outline/materials/draft/review 各管一块,互不覆盖。如果某个 Worker 需要改别人的字段(比如 Reviewer 直接改 draft),要明确这是"打回重写"还是"直接修改",并在 prompt 里约束。建议Reviewer 只产出意见,不改 draft,由 Writer 根据意见重写,职责更清晰。
4. Supervisor 路由决策不稳
Supervisor 用 LLM 决策,可能输出不存在的 worker 名字或乱跳。缓解:
- 用
with_structured_output(Route)强制结构化输出。 - 兜底:不在白名单里的输出统一转 FINISH 或回 planner。
- Supervisor prompt 里给完整流程示例和"打回"逻辑,减少自由发挥。
5. 共享状态里塞了不该塞的东西
新手容易把"对话历史""中间思考"全塞进共享状态,导致状态膨胀、Worker 之间互相干扰。原则:共享状态只放跨 Worker 协作必需的字段,每个 Worker 的内部思考用局部变量或独立子图状态,不污染顶层。
十二、小结
- 多智能体协作平台 = Supervisor(调度)+ 多 Worker(执行),共享一个 TeamState。
- Supervisor 用 LLM + 结构化输出决定下一步交给谁,Worker 执行完回 Supervisor 形成循环。
- 状态设计原则:每个 Worker 只写自己的产出字段,避免职责污染。
- 必加
step_count死循环刹车,并限制 REWRITE 次数。 - 扩展性强:加 Worker 只需加节点+更新 Supervisor prompt;Worker 可升级为子图或 ReAct 智能体。
- 关键环节可加
interrupt人工审批,实现 HITL 协作。