跳到内容

多智能体系统

智能体一个使用 LLM 来决定应用程序控制流的系统。随着你开发这些系统,它们可能会随着时间的推移变得越来越复杂,使得管理和扩展变得更加困难。例如,你可能会遇到以下问题:

  • 智能体有太多工具可供使用,导致在决定下一步调用哪个工具时做出糟糕的决策
  • 上下文变得过于复杂,单个智能体难以跟踪
  • 系统中需要多个专业领域(例如,规划器、研究员、数学专家等)

为了解决这些问题,你可以考虑将你的应用程序分解为多个更小、独立的智能体,并将它们组合成一个多智能体系统。这些独立的智能体可以像一个提示和一个 LLM 调用一样简单,也可以像一个ReAct智能体一样复杂(甚至更复杂!)。

使用多智能体系统的主要好处是:

  • 模块化:独立的智能体使得开发、测试和维护智能体系统更加容易。
  • 专业化:你可以创建专注于特定领域的专家智能体,这有助于提高整个系统的性能。
  • 可控性:你可以明确控制智能体之间的通信方式(而不是依赖于函数调用)。

多智能体架构

在多智能体系统中,有几种连接智能体的方式:

  • 网络:每个智能体都可以与其他任何智能体进行通信。任何智能体都可以决定下一步调用哪个其他智能体。
  • 主管:每个智能体都与一个主管智能体进行通信。主管智能体决定下一步应该调用哪个智能体。
  • 主管(工具调用):这是主管架构的一种特殊情况。单个智能体可以被表示为工具。在这种情况下,主管智能体使用一个支持工具调用的 LLM 来决定调用哪个智能体工具,以及传递给这些智能体的参数。
  • 层级式:你可以定义一个具有主管的主管的多智能体系统。这是主管架构的一般化,允许更复杂的控制流。
  • 自定义多智能体工作流:每个智能体只与一部分智能体进行通信。部分流程是确定性的,只有一些智能体可以决定下一步调用哪个其他智能体。

交接

在多智能体架构中,智能体可以被表示为图的节点。每个智能体节点执行其步骤,并决定是结束执行还是路由到另一个智能体,包括可能路由到自身(例如,在循环中运行)。多智能体交互中的一个常见模式是移交(handoffs),即一个智能体将控制权移交给另一个智能体。移交允许你指定:

要在 LangGraph 中实现移交,智能体节点可以返回Command对象,该对象允许你同时组合控制流和状态更新:

def agent(state) -> Command[Literal["agent", "another_agent"]]:
    # the condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.
    goto = get_next_agent(...)  # 'agent' / 'another_agent'
    return Command(
        # Specify which agent to call next
        goto=goto,
        # Update the graph state
        update={"my_state_key": "my_state_value"}
    )

在一个更复杂的场景中,每个智能体节点本身就是一个图(即一个子图),其中一个智能体子图中的节点可能想要导航到另一个智能体。例如,如果你有两个智能体,alicebob(父图中的子图节点),而 alice 需要导航到 bob,你可以在 Command 对象中设置 graph=Command.PARENT

def some_node_inside_alice(state):
    return Command(
        goto="bob",
        update={"my_state_key": "my_state_value"},
        # specify which graph to navigate to (defaults to the current graph)
        graph=Command.PARENT,
    )

注意

如果你需要为使用 Command(graph=Command.PARENT) 进行通信的子图提供可视化支持,你需要将它们包装在一个带有 Command 注解的节点函数中:不要这样写:

builder.add_node(alice)

你需要这样做:

def call_alice(state) -> Command[Literal["bob"]]:
    return alice.invoke(state)

builder.add_node("alice", call_alice)

作为工具的移交

最常见的智能体类型之一是工具调用智能体。对于这类智能体,一个常见的模式是将移交包装在工具调用中:

API 参考:tool

from langchain_core.tools import tool

@tool
def transfer_to_bob():
    """Transfer to bob."""
    return Command(
        # name of the agent (node) to go to
        goto="bob",
        # data to send to the agent
        update={"my_state_key": "my_state_value"},
        # indicate to LangGraph that we need to navigate to
        # agent node in a parent graph
        graph=Command.PARENT,
    )

这是从工具更新图状态的一种特殊情况,除了状态更新外,还包含了控制流。

重要

如果你想使用返回 Command 的工具,你可以使用预构建的 create_react_agent / ToolNode 组件,或者实现你自己的逻辑:

def call_tools(state):
    ...
    commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls]
    return commands

现在让我们更仔细地看看不同的多智能体架构。

网络

在这种架构中,智能体被定义为图的节点。每个智能体都可以与其他任何智能体通信(多对多连接),并可以决定下一步调用哪个智能体。这种架构适用于没有明确的智能体层级或智能体调用特定顺序的问题。

API 参考:ChatOpenAI | Command | StateGraph | START | END

from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from langgraph.graph import StateGraph, MessagesState, START, END

model = ChatOpenAI()

def agent_1(state: MessagesState) -> Command[Literal["agent_2", "agent_3", END]]:
    # you can pass relevant parts of the state to the LLM (e.g., state["messages"])
    # to determine which agent to call next. a common pattern is to call the model
    # with a structured output (e.g. force it to return an output with a "next_agent" field)
    response = model.invoke(...)
    # route to one of the agents or exit based on the LLM's decision
    # if the LLM returns "__end__", the graph will finish execution
    return Command(
        goto=response["next_agent"],
        update={"messages": [response["content"]]},
    )

def agent_2(state: MessagesState) -> Command[Literal["agent_1", "agent_3", END]]:
    response = model.invoke(...)
    return Command(
        goto=response["next_agent"],
        update={"messages": [response["content"]]},
    )

def agent_3(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]:
    ...
    return Command(
        goto=response["next_agent"],
        update={"messages": [response["content"]]},
    )

builder = StateGraph(MessagesState)
builder.add_node(agent_1)
builder.add_node(agent_2)
builder.add_node(agent_3)

builder.add_edge(START, "agent_1")
network = builder.compile()

主管型

在这种架构中,我们将智能体定义为节点,并添加一个主管节点(LLM),由它来决定接下来应调用哪些智能体节点。我们使用 Command 根据主管的决定将执行路由到适当的智能体节点。这种架构也非常适合并行运行多个智能体或使用 map-reduce 模式。

API 参考:ChatOpenAI | Command | StateGraph | START | END

from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from langgraph.graph import StateGraph, MessagesState, START, END

model = ChatOpenAI()

def supervisor(state: MessagesState) -> Command[Literal["agent_1", "agent_2", END]]:
    # you can pass relevant parts of the state to the LLM (e.g., state["messages"])
    # to determine which agent to call next. a common pattern is to call the model
    # with a structured output (e.g. force it to return an output with a "next_agent" field)
    response = model.invoke(...)
    # route to one of the agents or exit based on the supervisor's decision
    # if the supervisor returns "__end__", the graph will finish execution
    return Command(goto=response["next_agent"])

def agent_1(state: MessagesState) -> Command[Literal["supervisor"]]:
    # you can pass relevant parts of the state to the LLM (e.g., state["messages"])
    # and add any additional logic (different models, custom prompts, structured output, etc.)
    response = model.invoke(...)
    return Command(
        goto="supervisor",
        update={"messages": [response]},
    )

def agent_2(state: MessagesState) -> Command[Literal["supervisor"]]:
    response = model.invoke(...)
    return Command(
        goto="supervisor",
        update={"messages": [response]},
    )

builder = StateGraph(MessagesState)
builder.add_node(supervisor)
builder.add_node(agent_1)
builder.add_node(agent_2)

builder.add_edge(START, "supervisor")

supervisor = builder.compile()

请查看这个教程,了解主管式多智能体架构的示例。

主管(工具调用)

在这种主管架构的变体中,我们定义了一个主管智能体,它负责调用子智能体。子智能体作为工具暴露给主管,主管智能体决定下一步调用哪个工具。主管智能体遵循标准实现,即在一个 while 循环中运行一个 LLM,不断调用工具,直到它决定停止。

API 参考:ChatOpenAI | InjectedState | create_react_agent

from typing import Annotated
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import InjectedState, create_react_agent

model = ChatOpenAI()

# this is the agent function that will be called as tool
# notice that you can pass the state to the tool via InjectedState annotation
def agent_1(state: Annotated[dict, InjectedState]):
    # you can pass relevant parts of the state to the LLM (e.g., state["messages"])
    # and add any additional logic (different models, custom prompts, structured output, etc.)
    response = model.invoke(...)
    # return the LLM response as a string (expected tool response format)
    # this will be automatically turned to ToolMessage
    # by the prebuilt create_react_agent (supervisor)
    return response.content

def agent_2(state: Annotated[dict, InjectedState]):
    response = model.invoke(...)
    return response.content

tools = [agent_1, agent_2]
# the simplest way to build a supervisor w/ tool-calling is to use prebuilt ReAct agent graph
# that consists of a tool-calling LLM node (i.e. supervisor) and a tool-executing node
supervisor = create_react_agent(model, tools)

层级式

当你向系统中添加更多智能体时,主管可能会难以管理所有智能体。主管可能会开始在决定下一步调用哪个智能体时做出糟糕的决策,或者上下文可能变得过于复杂,单个主管难以跟踪。换句话说,你最终会遇到最初促使你采用多智能体架构的同样问题。

为了解决这个问题,你可以层级式地设计你的系统。例如,你可以创建由各个主管管理的专门的智能体团队,并由一个顶级主管来管理这些团队。

API 参考:ChatOpenAI | StateGraph | START | END | Command

from typing import Literal
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.types import Command
model = ChatOpenAI()

# define team 1 (same as the single supervisor example above)

def team_1_supervisor(state: MessagesState) -> Command[Literal["team_1_agent_1", "team_1_agent_2", END]]:
    response = model.invoke(...)
    return Command(goto=response["next_agent"])

def team_1_agent_1(state: MessagesState) -> Command[Literal["team_1_supervisor"]]:
    response = model.invoke(...)
    return Command(goto="team_1_supervisor", update={"messages": [response]})

def team_1_agent_2(state: MessagesState) -> Command[Literal["team_1_supervisor"]]:
    response = model.invoke(...)
    return Command(goto="team_1_supervisor", update={"messages": [response]})

team_1_builder = StateGraph(Team1State)
team_1_builder.add_node(team_1_supervisor)
team_1_builder.add_node(team_1_agent_1)
team_1_builder.add_node(team_1_agent_2)
team_1_builder.add_edge(START, "team_1_supervisor")
team_1_graph = team_1_builder.compile()

# define team 2 (same as the single supervisor example above)
class Team2State(MessagesState):
    next: Literal["team_2_agent_1", "team_2_agent_2", "__end__"]

def team_2_supervisor(state: Team2State):
    ...

def team_2_agent_1(state: Team2State):
    ...

def team_2_agent_2(state: Team2State):
    ...

team_2_builder = StateGraph(Team2State)
...
team_2_graph = team_2_builder.compile()


# define top-level supervisor

builder = StateGraph(MessagesState)
def top_level_supervisor(state: MessagesState) -> Command[Literal["team_1_graph", "team_2_graph", END]]:
    # you can pass relevant parts of the state to the LLM (e.g., state["messages"])
    # to determine which team to call next. a common pattern is to call the model
    # with a structured output (e.g. force it to return an output with a "next_team" field)
    response = model.invoke(...)
    # route to one of the teams or exit based on the supervisor's decision
    # if the supervisor returns "__end__", the graph will finish execution
    return Command(goto=response["next_team"])

builder = StateGraph(MessagesState)
builder.add_node(top_level_supervisor)
builder.add_node("team_1_graph", team_1_graph)
builder.add_node("team_2_graph", team_2_graph)
builder.add_edge(START, "top_level_supervisor")
builder.add_edge("team_1_graph", "top_level_supervisor")
builder.add_edge("team_2_graph", "top_level_supervisor")
graph = builder.compile()

自定义多智能体工作流

在这种架构中,我们将单个智能体添加为图的节点,并预先在自定义工作流中定义智能体的调用顺序。在 LangGraph 中,工作流可以通过两种方式定义:

  • 显式控制流(普通边):LangGraph 允许你通过普通的图边来明确定义应用程序的控制流(即智能体通信的顺序)。这是上述架构中最具确定性的变体——我们总能提前知道接下来将调用哪个智能体。

  • 动态控制流(Command):在 LangGraph 中,你可以允许 LLM 决定应用程序控制流的某些部分。这可以通过使用Command来实现。这种情况的一个特例是主管工具调用架构。在这种情况下,驱动主管智能体的工具调用 LLM 将决定调用工具(智能体)的顺序。

API 参考:ChatOpenAI | StateGraph | START

from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START

model = ChatOpenAI()

def agent_1(state: MessagesState):
    response = model.invoke(...)
    return {"messages": [response]}

def agent_2(state: MessagesState):
    response = model.invoke(...)
    return {"messages": [response]}

builder = StateGraph(MessagesState)
builder.add_node(agent_1)
builder.add_node(agent_2)
# define the flow explicitly
builder.add_edge(START, "agent_1")
builder.add_edge("agent_1", "agent_2")

通信和状态管理

在构建多智能体系统时,最重要的事情是弄清楚智能体如何通信。

智能体通信的一种常见、通用的方式是通过消息列表。这引出了以下问题:

此外,如果你正在处理更复杂的智能体,或者希望将单个智能体的状态与多智能体系统的状态分开,你可能需要使用不同的状态模式

移交 vs 工具调用

在智能体之间传递的“负载”是什么?在上面讨论的大多数架构中,智能体通过移交进行通信,并将图状态作为移交负载的一部分传递。具体来说,智能体将消息列表作为图状态的一部分传递。在带工具调用的主管的情况下,负载是工具调用的参数。

智能体之间的消息传递

智能体最常见的通信方式是通过一个共享的状态通道,通常是一个消息列表。这假设在状态中至少有一个由智能体共享的通道(键)(例如,messages)。当通过共享消息列表进行通信时,还有一个额外的考虑:智能体应该共享其思考过程的完整历史还是只共享最终结果

共享完整思考过程

智能体可以与所有其他智能体共享其思考过程的完整历史(即“草稿板”)。这个“草稿板”通常看起来像一个消息列表。共享完整思考过程的好处是,它可能有助于其他智能体做出更好的决策,并提高整个系统的推理能力。缺点是,随着智能体数量和其复杂性的增长,“草稿板”会迅速增长,并可能需要额外的内存管理策略。

仅共享最终结果

智能体可以有自己的私有“草稿板”,并只与其余智能体共享最终结果。对于拥有许多智能体或更复杂的智能体的系统,这种方法可能效果更好。在这种情况下,你需要为智能体定义不同的状态模式

对于作为工具被调用的智能体,主管根据工具模式确定输入。此外,LangGraph 允许在运行时向单个工具传递状态,因此下级智能体可以在需要时访问父级状态。

在消息中指明智能体名称

指明某条 AI 消息来自哪个智能体可能会很有帮助,特别是对于长消息历史。一些 LLM 提供商(如 OpenAI)支持向消息添加 name 参数——你可以用它来将智能体名称附加到消息上。如果不支持,你可以考虑手动将智能体名称注入到消息内容中,例如,<agent>alice</agent><message>来自 alice 的消息</message>

在消息历史中表示移交

移交通常是通过 LLM 调用一个专用的移交工具来完成的。这被表示为一个带有工具调用的AI 消息,该消息被传递给下一个智能体(LLM)。大多数 LLM 提供商不支持在没有相应工具消息的情况下接收带有工具调用的 AI 消息。

因此,你有两个选择:

  1. 向消息列表添加一条额外的工具消息,例如,“成功转移到智能体 X”
  2. 移除带有工具调用的 AI 消息

在实践中,我们看到大多数开发者选择选项(1)。

子智能体的状态管理

一个常见的做法是让多个智能体在一个共享的消息列表上进行通信,但只将其最终消息添加到列表中。这意味着任何中间消息(例如,工具调用)都不会保存在此列表中。

如果你确实想保存这些消息,以便将来调用这个特定的子智能体时可以将其传递回去,该怎么办?

实现这一点有两种高级方法:

  1. 将这些消息存储在共享消息列表中,但在将其传递给子智能体 LLM 之前过滤该列表。例如,你可以选择过滤掉来自其他智能体的所有工具调用。
  2. 在子智能体的图状态中为每个智能体存储一个单独的消息列表(例如,alice_messages)。这将是它们对消息历史的“视图”。

使用不同的状态模式

一个智能体可能需要有与其他智能体不同的状态模式。例如,一个搜索智能体可能只需要跟踪查询和检索到的文档。在 LangGraph 中有两种方法可以实现这一点:

  • 定义具有独立状态模式的子图智能体。如果子图和父图之间没有共享的状态键(通道),重要的是要添加入/输出转换,以便父图知道如何与子图通信。
  • 定义具有私有输入状态模式的智能体节点函数,该模式与整个图状态模式不同。这允许传递仅在执行该特定智能体时需要的信息。