LangGraph 多智能体 Supervisor 架构学习笔记
一、什么是 Supervisor 模式
Supervisor(主管)模式是 LangGraph 中实现多智能体协作的核心架构。它的核心思想是:
一个"主管"Agent 负责协调多个"子"Agent,根据用户任务自动分派给最合适的专家执行。
类比现实场景:一个项目经理(Supervisor)管理团队中的研究员和程序员,根据任务性质决定让谁来干。
二、create_supervisor 源码解析
2.1 没有隐藏的系统提示词
create_supervisor 不会添加任何隐藏 prompt,你传入的 prompt 参数就是唯一的系统提示词。这意味着你完全掌控 Supervisor 的行为。
workflow = create_supervisor(
[research_agent, code_agent],
model=llm,
prompt="你是一个团队主管,管理两个专家...", # ← 这就是全部系统提示词
)
2.2 真正的"魔法":自动生成的 Handoff 工具
Supervisor 的分派能力不靠提示词,而是靠框架自动生成的转移工具:
| 自动生成的工具 | 生成给谁 | 作用 |
|---|---|---|
transfer_to_research_expert | Supervisor | 路由到研究专家 |
transfer_to_code_expert | Supervisor | 路由到代码专家 |
transfer_back_to_supervisor | 每个子 Agent | 任务完成后返回主管 |
源码核心逻辑(简化):
# 框架为每个子 Agent 自动创建 handoff 工具
@tool(name="transfer_to_research_expert", description="Ask agent 'research_expert' for help")
def handoff_to_agent(state, tool_call_id):
return Command(goto="research_expert", ...) # 路由到对应 Agent
# 框架为每个子 Agent 注入返回工具
@tool(name="transfer_back_to_supervisor")
def transfer_back(state, tool_call_id):
return Command(goto="supervisor", ...) # 路由回 Supervisor
2.3 Supervisor 本身就是一个 ReAct Agent
框架内部等价于:
supervisor_agent = create_react_agent(
name="supervisor",
model=model,
tools=[transfer_to_research_expert, transfer_to_code_expert],
prompt=prompt, # 你传入的提示词
)
2.4 完整的 StateGraph 路由
2.5 一句话总结
Supervisor 的"智能分派"本质上就是:LLM 看到工具列表
[transfer_to_research_expert, transfer_to_code_expert],根据任务内容选择调用哪个工具,框架负责路由。
三、完整代码示例
3.1 定义工具
为子 Agent 定义专用工具。这里提供两个工具:计算器(calculator)和 Python 代码执行器(python_executor)。
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""计算数学表达式,如 '2 + 3 * 4',返回计算结果"""
try:
result = eval(expression, {"__builtins__": {}})
return f"{expression} = {result}"
except Exception as e:
return f"计算失败: {e}"
@tool
def python_executor(code: str) -> str:
"""执行 Python 代码并返回输出结果"""
import io, contextlib
output = io.StringIO()
try:
with contextlib.redirect_stdout(output):
exec(code, {"__builtins__": __builtins__})
result = output.getvalue()
return result if result else "(无输出)"
except Exception as e:
return f"执行错误: {e}"
3.2 创建子 Agent
每个子 Agent 有独立的模型、工具集和系统提示词。
from langgraph.prebuilt import create_react_agent
# 研究专家:纯知识回答,不使用任何工具
research_agent = create_react_agent(
model=llm,
tools=[],
name="research_expert",
prompt="你是一个研究专家,擅长分析问题、提供思路和方案。你不写代码,只负责分析和给出建议。",
)
# 代码专家:可以使用计算器和代码执行工具
code_agent = create_react_agent(
model=llm,
tools=[calculator, python_executor],
name="code_expert",
prompt="你是一个代码专家,擅长编程和计算。你可以使用 calculator 和 python_executor 工具。",
)
3.3 创建 Supervisor 并编译
from langgraph_supervisor import create_supervisor
from langgraph.checkpoint.memory import MemorySaver
# 创建 Supervisor 工作流
workflow = create_supervisor(
[research_agent, code_agent],
model=llm,
prompt=(
"你是一个团队主管,管理两个专家:\n"
"1. research_expert(研究专家):擅长分析问题、提供思路\n"
"2. code_expert(代码专家):擅长编程、计算\n"
"根据用户任务,合理分派给合适的专家。"
),
)
# 编译(带内存记忆,支持多轮对话)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
3.4 运行测试
使用 astream 流式获取每一步执行过程:
import asyncio
from langchain_core.runnables import RunnableConfig
async def run():
config = RunnableConfig(
configurable={"thread_id": "test-001"},
recursion_limit=50,
)
async for chunk in app.astream(
input={"messages": [("user", "计算 (123 + 456) * 789")]},
config=config,
):
print(chunk)
asyncio.run(run())
四、执行流程分析
以任务 "计算 (123 + 456) * 789" 为例,完整的执行流程如下:
LLM 调用次数统计:
| 步骤 | 节点 | 动作 |
|---|---|---|
| 1 | supervisor | 分析任务,决定分派给 code_expert |
| 2 | code_expert | 决定调用 calculator 工具 |
| 3 | supervisor | 收到结果,生成最终回复 |
共 3 次 LLM 调用。
五、开发踩坑记录
5.1 DeepSeek 思考模型的 reasoning_content 问题
问题: 使用 DeepSeek 思考模型(如 deepseek-v4-pro)时,多智能体消息传递中报 400 错误:
The `reasoning_content` in the thinking mode must be passed back to the API.
原因: 思考模型在回复中附带 reasoning_content(思考过程),Supervisor 把子 Agent 的消息传递给下一个 Agent 时,这个字段在 LangChain 的消息序列化过程中丢失了。
尝试过的方案:
| 方案 | 结果 |
|---|---|
model_kwargs={"reasoning": {"enabled": False}} | langchain-openai 识别为特殊参数,切换到 /responses API,404 |
extra_body={"reasoning": {"enabled": False}} | 参数传递正确但未生效,仍然返回 reasoning_content |
使用 deepseek-chat(非思考模型) | 成功 |
最终方案:
llm_no_think = ChatOpenAI(
api_key=api_key,
base_url=base_url,
model="deepseek-chat", # 非思考模型,不产生 reasoning_content
temperature=0,
)
5.2 子 Agent 回答过长导致重复调用
问题: 研究专家的回答被截断后,Supervisor 认为任务未完成,再次分派给研究专家。
解决方案:
- 增大
recursion_limit - 在子 Agent 的 prompt 中要求"回答简洁,控制在 500 字以内"
- 在 Supervisor 的 prompt 中加入"如果专家已经给出了部分回答,直接总结即可"
六、Supervisor vs 其他多智能体模式
| 模式 | 特点 | 适用场景 |
|---|---|---|
| Supervisor | 一个主管统一调度,子 Agent 不直接通信 | 任务分工明确、需要集中管控 |
| Swarm | Agent 之间可以直接互相转移 | 扁平化协作、流水线任务 |
| Hierarchical | 多层 Supervisor,主管管主管 | 大型复杂项目、多级分工 |
七、关键 API 速查
# 创建子 Agent
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(model=llm, tools=[], name="agent_name", prompt="...")
# 创建 Supervisor
from langgraph_supervisor import create_supervisor
workflow = create_supervisor([agent1, agent2], model=llm, prompt="...")
# 编译并运行
app = workflow.compile(checkpointer=MemorySaver())
result = app.invoke({"messages": [("user", "你的任务")]}, config=config)
# 流式输出
async for chunk in app.astream(input={"messages": [("user", "任务")]}, config=config):
print(chunk)
八、完整代码
import sys
import os
import asyncio
# 将项目根目录加入 sys.path,支持直接运行
_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if _project_root not in sys.path:
sys.path.insert(0, _project_root)
from langchain_core.messages import convert_to_messages
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
from langgraph_supervisor import create_supervisor
from app.common import llm
# ============================================================
# 创建专用 LLM 实例(使用非思考模型,避免多智能体传递时 reasoning_content 丢失)
# ============================================================
# DeepSeek 思考模型(如 deepseek-v4-pro)会返回 reasoning_content,
# 在 Supervisor 多智能体消息传递中这个字段会丢失,导致 API 报 400 错误。
# 解决方案:直接使用 deepseek-chat(非思考模型)。
llm_no_think = ChatOpenAI(
api_key=llm.LLM_API_KEY,
base_url=llm.LLM_BASE_URL,
model="deepseek-chat",
temperature=llm.LLM_TEMPERATURE,
)
print(f"[INFO] Supervisor 使用模型: deepseek-chat(非思考模型)")
print(f"[INFO] 当前 Base URL: {llm.LLM_BASE_URL}")
# ============================================================
# 定义工具(供子 Agent 使用)
# ============================================================
@tool
def calculator(expression: str) -> str:
"""计算数学表达式,如 '2 + 3 * 4',返回计算结果"""
try:
result = eval(expression, {"__builtins__": {}})
return f"{expression} = {result}"
except Exception as e:
return f"计算失败: {e}"
@tool
def python_executor(code: str) -> str:
"""执行 Python 代码并返回输出结果"""
import io
import contextlib
output = io.StringIO()
try:
with contextlib.redirect_stdout(output):
exec(code, {"__builtins__": __builtins__})
result = output.getvalue()
return result if result else "(无输出)"
except Exception as e:
return f"执行错误: {e}"
# ============================================================
# 日志打印函数
# ============================================================
def pretty_print_messages(update, last_message=False):
"""格式化打印 Agent 执行过程中的消息"""
is_subgraph = False
if isinstance(update, tuple):
ns, update = update
# 跳过父图的更新(只看子图)
if len(ns) == 0:
return
graph_id = ns[-1].split(":")[0]
print(f"\n [子图: {graph_id}]")
is_subgraph = True
for node_name, node_update in update.items():
update_label = f" 节点: {node_name}"
if is_subgraph:
update_label = "\t" + update_label
print(update_label)
messages = convert_to_messages(node_update["messages"])
if last_message:
messages = messages[-1:]
for m in messages:
msg_type = m.__class__.__name__
content = str(m.content)[:200] if m.content else ""
prefix = "\t" if is_subgraph else " "
if msg_type == "HumanMessage":
print(f"{prefix} 用户: {content}")
elif msg_type == "AIMessage":
if hasattr(m, 'tool_calls') and m.tool_calls:
for tc in m.tool_calls:
print(f"{prefix} 调用工具: {tc['name']}({tc['args']})")
elif content:
print(f"{prefix} AI: {content}")
elif msg_type == "ToolMessage":
print(f"{prefix} 工具结果: {content}")
else:
print(f"{prefix} [{msg_type}]: {content}")
print()
# ============================================================
# 创建子 Agent
# ============================================================
# 研究专家:擅长分析问题、信息检索、方案规划,不做任何代码
research_agent = create_react_agent(
model=llm_no_think,
tools=[], # 不使用工具,纯知识回答
name="research_expert",
prompt="你是一个研究专家,擅长分析问题、提供思路和方案。你不写代码,只负责分析问题、制定方案、给出建议。回答用中文。",
)
# 代码专家:擅长编程、计算、执行代码
code_agent = create_react_agent(
model=llm_no_think,
tools=[calculator, python_executor],
name="code_expert",
prompt="你是一个代码专家,擅长编程和计算。你可以使用 calculator 工具计算数学表达式,使用 python_executor 工具执行 Python 代码。回答用中文。",
)
# ============================================================
# 创建 Supervisor 工作流
# ============================================================
workflow = create_supervisor(
[research_agent, code_agent],
model=llm_no_think,
prompt=(
"你是一个团队主管,管理两个专家:\n"
"1. research_expert(研究专家):擅长分析问题、提供思路、制定方案\n"
"2. code_expert(代码专家):擅长编程、计算、执行代码\n"
"根据用户的任务,合理分派给合适的专家。需要分析问题时用 research_expert,"
"需要写代码或计算时用 code_expert。回答用中文。"
),
)
# 编译工作流(带内存记忆)
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)
# ============================================================
# 测试函数
# ============================================================
async def run_supervisor():
"""运行 Supervisor 多智能体测试"""
# 预定义测试任务(覆盖不同场景)
test_tasks = [
"分析一下学习 Python 的最佳路径是什么?",
"请计算 (123 + 456) * 789 的结果",
"用 Python 写一个冒泡排序算法,并测试一下",
]
config = RunnableConfig(
configurable={"thread_id": "supervisor-test-001"},
recursion_limit=50,
)
for i, task in enumerate(test_tasks, 1):
print("\n" + "=" * 60)
print(f"任务 {i}/{len(test_tasks)}: {task}")
print("=" * 60)
iteration_count = 0
async for chunk in app.astream(
input={"messages": [("user", task)]},
config=config,
):
iteration_count += 1
print(f"\n--- 第 {iteration_count} 步执行 ---")
pretty_print_messages(chunk)
print(f"--- 任务 {i} 完成,共执行 {iteration_count} 步 ---")
print("\n" + "=" * 60)
print("Supervisor 测试完成")
print("=" * 60)
# ============================================================
# 交互式对话模式(可选)
# ============================================================
async def run_interactive():
"""交互式多轮对话模式"""
config = RunnableConfig(
configurable={"thread_id": "supervisor-interactive-001"},
recursion_limit=50,
)
print("\n输入 'exit' 退出对话\n")
while True:
user_input = input("用户: ")
if user_input.lower() == "exit":
print("对话结束。")
break
print("\n--- Supervisor 正在协调工作... ---")
async for chunk in app.astream(
input={"messages": [("user", user_input)]},
config=config,
):
pretty_print_messages(chunk, last_message=True)
# ============================================================
# 运行
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("LangGraph 多智能体 Supervisor 架构测试")
print("=" * 60)
# 运行预设任务测试
asyncio.run(run_supervisor())
# 如需交互式对话,取消下方注释
# asyncio.run(run_interactive())

1015

被折叠的 条评论
为什么被折叠?



