2.8 学霸养成记:从 LangGraph 作业点评看多智能体协同的常见误区

4 阅读1分钟

直播导语:同学们好,欢迎来到我们第二周的直播课!在这一周里,我们一起探索了 LangGraph 的强大功能,并完成了 DeepResearch 这个相当有挑战性的结业项目。我看了大家提交的“作业”(我们以此前构建的 DeepResearch 项目为蓝本),发现同学们都非常有创造力,但也暴露了一些在设计复杂多智能体系统时非常典型的、共通的“误区”。本次直播,我们将采用一种新颖的形式——代码审查(Code Review)。我将选取几份匿名的“学生作业”,和大家一起分析其中的设计亮点与不足,并现场进行重构。这比单纯讲解理论更具实战意义,希望能帮助大家“从错误中学习”,避开未来开发中的“坑”,快速成长为一名优秀的“AI 架构师”。

目录

  1. 本次作业回顾:构建 DeepResearch 深度研究助手
    • 核心要求:实现 Supervisor-Worker 架构,完成“规划-并行研究-汇总”流程。
  2. 作业一(“小明同学”)点评:强耦合的“巨型”节点
    • 代码现象:将 Supervisor 的规划、所有 Worker 的调用逻辑、以及最终的聚合逻辑,全部写在一个巨大的 if-elif-else 结构的节点函数中。
    • 问题分析
      • 违反“单一职责原则”,节点功能混乱。
      • 可读性、可维护性差,难以修改或扩展。
      • 无法实现真正的并行。
    • 重构方案:将“规划”、“执行”、“聚合”拆分成独立的、职责清晰的节点,通过图的边来连接它们。
  3. 作业二(“小红同学”)点评:脆弱的“硬编码”通信
    • 代码现象:Supervisor 的 Prompt 中硬编码了 Worker 的名字("Researcher_1", "Researcher_2"),并且假设总是存在两个 Worker。在主逻辑中,通过 state['worker_outputs']['Researcher_1'] 这样硬编码的方式来获取结果。
    • 问题分析
      • 缺乏动态性:如果任务只需要一个 Worker,或者需要五个 Worker,整个系统就会出错。
      • 扩展性差:每增加一个新 Worker,都需要修改多处代码。
    • 重构方案
      • 让 Supervisor 动态决定需要多少个 Worker,并将 Worker 列表存入 State
      • 主图逻辑根据 State 中的 Worker 列表,动态地创建和分发任务。
      • 聚合节点通过遍历 state['worker_outputs'] 字典来收集结果,而不是依赖固定的 key。
  4. 作业三(“小华同学”)点评:被忽视的“状态管理艺术”
    • 代码现象AgentState 中只定义了一个 messages 字段。所有的数据,包括任务计划、中间结果、最终报告等,都以 AIMessageHumanMessage 的形式被塞进 messages 列表。
    • 问题分析
      • 语义模糊:很难从一长串消息中区分哪些是用户输入,哪些是工具结果,哪些是最终报告。
      • 效率低下:将大量结构化数据(如 JSON 格式的计划)作为普通文本放入 messages,会消耗不必要的 Token,并增加 LLM 的解析难度。
      • 状态访问困难:后续节点需要遍历和解析整个 messages 列表才能找到自己需要的信息。
    • 重构方案:精心设计 AgentState
      • 为不同类型的数据创建独立的、类型明确的字段,如 task: str, plan: List[str], research_findings: Dict[str, str], final_report: str
      • messages 字段只用来存储核心的、用于驱动 LLM 对话的历史。
      • 这种结构化的状态,使得任何节点都可以通过 state['plan']state['final_report'] 直接、高效地访问所需数据。
  5. “学霸”的作业长啥样?一个优秀架构的最佳实践
    • 模块化:将 Supervisor、Worker、图的定义等分离到不同的文件中。
    • 动态化:整个图的结构(特别是 Worker 的数量)是根据 Supervisor 的规划动态生成的。
    • 结构化状态:拥有一个设计良好、字段丰富的 AgentState
    • 错误处理:考虑了 Worker 执行失败的情况,并设计了重试或降级逻辑。
    • 持久化:集成了 Checkpointer,关键任务可断点续传。
  6. 总结:从“能跑就行”到“优雅健壮”的进阶之路

1. 本次作业回顾:构建 DeepResearch 深度研究助手

首先,我们快速回顾一下本次“大作业”的核心要求:

  • 输入:一个复杂的研究主题(字符串)。
  • 核心架构:必须体现 Supervisor-Worker 的协作模式。
    • Supervisor:负责将输入的主题分解成一个包含多个步骤的研究计划。
    • Workers:根据计划,并行地对每个步骤进行研究(调用搜索工具)。
    • Aggregator:负责将所有 Worker 的研究结果汇总成一份最终报告。
  • 输出:一份结构化的研究报告。

大部分同学都成功地让程序跑了起来,完成了基本功能,值得肯定!但是,实现功能只是第一步。如何让我们的代码更具可读性、可维护性、可扩展性,是区分“初级开发者”和“高级架构师”的关键。下面我们就来看几个典型的例子。

2. 作业一(“小明同学”)点评:强耦合的“巨型”节点

“小明同学”的思路很直接:用一个函数来处理所有事情。

代码现象

# “小明同学”的作业节选
def mega_agent_node(state):
    # 第一次进入,没有计划,认为是 Supervisor
    if not state.get('plan'):
        print("Supervisor is planning...")
        plan = create_plan(state['task'])
        # 直接在这里就调用 worker 逻辑
        outputs = {}
        for step in plan:
            # 串行调用
            outputs[step] = run_research_worker(step)
        
        # 直接在这里就聚合
        report = aggregate_outputs(outputs)
        return {"final_report": report, "plan": plan}
    else:
        # 逻辑上永远不会走到这里
        return {}

workflow = StatefulGraph(AgentState)
workflow.add_node("mega_node", mega_agent_node)
workflow.set_entry_point("mega_node")
workflow.add_edge("mega_node", END)
app = workflow.compile()

问题分析

这段代码虽然“能用”,但它完全违背了 LangGraph 的设计哲学,存在严重问题:

  1. 违反“单一职责原则”:这个 mega_agent_node 节点身兼数职,它既是 Supervisor(规划),又是 Dispatcher(分发),还是 Worker(执行),最后还是 Aggregator(聚合)。这使得节点的逻辑异常臃肿和混乱。
  2. 可读性与可维护性差:想象一下,如果 run_research_worker 的逻辑需要修改,或者你想在聚合前加入一个“审核”步骤,你都需要在这个巨大的函数内部进行修改,很容易引入新的 Bug。
  3. 丧失并行能力:最致命的是,这种写法将所有 Worker 的调用变成了一个简单的 for 串行循环,完全没有利用到多智能体并行处理以提升效率的优势。
  4. 滥用 LangGraph:它实际上只用到了 LangGraph 的 StatefulGraph 来做一个状态容器,完全没有使用到 LangGraph 最核心的“图”和“边”的编排能力。这无异于买了一辆法拉利,却只用它来听收音机。

重构方案

正确的做法是拥抱“图”思维,将不同的职责拆分到不同的节点中:

# 重构后的思路
def supervisor_node(state): ...
def worker_node(state): ...
def aggregator_node(state): ...

workflow = StatefulGraph(AgentState)
# 1. 添加职责单一的节点
workflow.add_node("supervisor", supervisor_node)
workflow.add_node("parallel_workers", worker_node) # worker_node 可以并行执行
workflow.add_node("aggregator", aggregator_node)

# 2. 用“边”来定义清晰的流程
workflow.set_entry_point("supervisor")
workflow.add_edge("supervisor", "parallel_workers")
workflow.add_edge("parallel_workers", "aggregator")
workflow.add_edge("aggregator", END)

app = workflow.compile()

核心启示节点应该是小的、职责单一的;流程的复杂性应该体现在图的拓扑结构(边)上,而不是单个节点的内部逻辑。

3. 作业二(“小红同学”)点评:脆弱的“硬编码”通信

“小红同学”已经掌握了拆分节点的基本思想,但她在节点间的通信和协作方式上,采用了大量的“硬编码”。

代码现象

# “小红同学”的 Supervisor Prompt 节选
SUPERVISOR_PROMPT = """
...你的任务是创建一份包含两个步骤的研究计划。
Step 1 将由 Researcher_1 执行。
Step 2 将由 Researcher_2 执行。
"""

# “小红同学”的聚合节点节选
def aggregator_node(state):
    output_1 = state['worker_outputs']['Researcher_1']
    output_2 = state['worker_outputs']['Researcher_2']
    
    final_report = f"Report part 1: {output_1}\nReport part 2: {output_2}"
    return {"final_report": final_report}

问题分析

这种硬编码的方式使得整个系统非常脆弱和僵化

  1. 缺乏动态性:Supervisor 被“焊死”,永远只能生成两个步骤的计划,分配给两个固定的 Worker。如果用户的任务很简单,只需要一个步骤,怎么办?如果任务很复杂,需要五个步骤,又该怎么办?
  2. 扩展性极差:假设你想为团队增加一个新的 Worker——Researcher_3。你需要去修改 Supervisor 的 Prompt,修改聚合节点的代码,甚至可能需要修改图的结构。每增加一个成员,都需要对系统的多个部分进行“手术”。

重构方案

我们应该追求一种动态的、数据驱动的架构。Worker 的数量和身份不应该由代码写死,而应该由 Supervisor 在运行时动态决定,并存入 State 中。

# 重构后的 Supervisor Prompt
SUPERVISOR_PROMPT = """
...根据用户的任务,生成一个合适的研究步骤列表。步骤的数量应该根据任务的复杂度决定,可以是 1 到 5 步。
"""

# 重构后的 Supervisor Node
def supervisor_node(state):
    plan: Plan = supervisor_runnable.invoke({"task": state['task']})
    # 动态生成 team_members 列表
    team_members = [f"Researcher_{i+1}" for i in range(len(plan.steps))]
    
    # 将动态信息存入 State
    return {"plan": plan.steps, "team_members": team_members}

# 重构后的 Aggregator Node
def aggregator_node(state):
    # 不再关心具体的 key 是什么,直接遍历字典
    all_outputs = "\n\n".join(state['worker_outputs'].values())
    # ... 后续逻辑

同时,主图的逻辑也应该根据 state['team_members'] 来动态地分发任务,而不是连接到写死的节点上。

核心启示用“状态”来传递配置和通信,而不是用“代码”来硬编码。让你的系统由数据(State)驱动,而不是由结构驱动。

4. 作业三(“小华同学”)点评:被忽视的“状态管理艺术”

“小华同学”的代码在图结构上非常清晰,但她在 AgentState 的设计上犯了懒。

代码现象

# “小华同学”的 AgentState
class AgentState(TypedDict):
    # 只有孤零零一个 messages
    messages: Annotated[List[BaseMessage], operator.add]

# “小华同学”的 Supervisor 节点
def supervisor_node(state):
    # ...
    plan_str = "1. First step...\n2. Second step..."
    # 把 plan 用 AIMessage 包装起来
    return {"messages": [AIMessage(content=plan_str, name="Supervisor")]}

# “小华同学”的 Worker 节点
def worker_node(state):
    # 需要从 messages 列表里找到 supervisor 的那条消息来获取计划
    plan = parse_plan_from_messages(state['messages'])
    # ...

问题分析

将所有信息,无论类型和用途,都一股脑地塞进 messages 列表,是一种非常常见的“坏味道”(Bad Smell)。

  1. 语义模糊messages 列表的语义应该是“用于驱动 LLM 对话的历史记录”。将计划、研究结果、最终报告等结构化数据伪装成“消息”存入其中,会污染其原始语义。
  2. 效率低下:LLM 的上下文窗口是宝贵的资源。将一个可能很长的、包含大量非对话信息的计划或报告全文,在每一次 LLM 调用时都重复传入,是巨大的 Token 浪费。LLM 也不得不从一堆“聊天记录”中费力地提取出它需要的结构化信息。
  3. 状态访问困难:任何一个节点想要获取之前某个节点产生的数据(比如计划),都必须去遍历整个 messages 列表,并进行复杂的解析。这既不高效,也不可靠。

重构方案

精心设计你的 AgentState!把它当作一个数据库的表结构来设计。

# 重构后的 AgentState
class AgentState(TypedDict):
    # 核心任务
    task: str
    # 用于 LLM 对话的核心历史
    messages: Annotated[List[BaseMessage], operator.add]
    
    # Supervisor 产生的结构化数据
    plan: Optional[List[str]]
    
    # Workers 产生的结构化数据
    worker_outputs: Optional[Dict[str, str]]
    
    # Aggregator 产生的最终结果
    final_report: Optional[str]

这种设计的好处是显而易见的:

  • 职责清晰messages 只负责对话。plan 只负责存储计划。各司其职。
  • 访问高效:任何节点想获取计划,只需 state['plan'] 即可,简单、直接、高效。
  • Token 优化:在调用某个节点(比如 Worker)时,我们可以只把 state['task']state['plan'] 中分配给它的那一部分传给它,而不需要传入完整的、包含无关信息的 messages 历史。

核心启示AgentState 是你多智能体系统的“单一事实来源”(Single Source of Truth)。花时间精心设计它,是构建一个健壮、高效系统的最佳投资。

5. “学霸”的作业长啥样?一个优秀架构的最佳实践

综合以上分析,一个堪称“学霸”级别的 DeepResearch 项目,应该具备以下特质:

  1. 模块化(Modularity):代码被清晰地组织在不同文件中。main.py 负责组装和运行,graph.py 负责定义图的结构,nodes.py 负责实现所有节点函数,state.py 负责定义 AgentState
  2. 动态化(Dynamism):Supervisor 能够根据任务动态生成计划,图的执行逻辑(特别是并行 Worker 的数量和调用)能够自适应地根据 State 中的计划进行调整。
  3. 结构化状态(Structured State)AgentState 设计得非常完善,包含了任务、计划、中间产物、最终结果等多个专用字段,而不仅仅是一个 messages 列表。
  4. 健壮性(Robustness):代码考虑了异常情况。例如,如果某个 Worker 执行失败,它不会导致整个系统崩溃,而是会将错误信息存入 State,Supervisor 在下一轮决策中可以看到这个错误,并可能决定重试或将任务重新分配给其他 Worker。
  5. 持久化(Persistence):集成了 Checkpointer 机制,使得任何一个长时间的研究任务都可以被中断和恢复。

6. 总结:从“能跑就行”到“优雅健壮”的进阶之路

通过这次“作业点评”,我们看到了从一个功能“可用”的程序,到一个架构“优秀”的系统之间存在的巨大鸿沟。

初学者往往追求“能跑就行”,容易写出功能耦合、硬编码、状态管理混乱的代码。而一名优秀的 AI 架构师,则会始终思考:

  • 我的节点职责是否单一?
  • 我的协作方式是否足够动态?
  • 我的状态管理是否足够清晰?
  • 我的系统是否考虑了异常和扩展?

希望这次 Code Review 能帮助大家在未来的项目中,少走弯trouble,多一些架构师的深度思考,真正从“代码实现者”蜕变为“系统设计者”。