跳转至

多智能体系统

智能体一种使用大语言模型(LLM)决定应用程序控制流的系统。在开发这些系统的过程中,它们可能会随着时间推移变得越来越复杂,从而更难管理和扩展。例如,你可能会遇到以下问题:

  • 智能体可使用的工具过多,导致其难以做出下一步调用哪个工具的正确决策
  • 上下文过于复杂,单个智能体无法有效跟踪
  • 系统需要多个专业领域分工(例如:规划者、研究员、数学专家等)

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

使用多智能体系统的主要优势包括:

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

多智能体架构

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

  • 网络型:每个智能体都可以与其他所有智能体通信。任何智能体都可以决定下一步调用哪个其他智能体。
  • 监督型:每个智能体都与一个监督者智能体通信。监督者智能体决定下一步应调用哪个智能体。
  • 监督型(工具调用):这是监督型架构的一种特殊情况。各个智能体可以被表示为工具。此时,监督者智能体使用支持工具调用的LLM来决定调用哪个智能体工具,以及传递给这些智能体的参数。
  • 层级型:你可以定义一个具有监督者之监督者的多智能体系统。这是监督型架构的泛化形式,允许更复杂的控制流。
  • 自定义多智能体工作流:每个智能体仅与部分其他智能体通信。流程中的某些部分是确定性的,只有部分智能体可以决定下一步调用哪些其他智能体。

交接(Handoffs)

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

在 LangGraph 中实现交接时,智能体节点可以返回 Command 对象,从而结合控制流和状态更新:

def agent(state) -> Command[Literal["agent", "another_agent"]]:
    # 路由/停止的条件可以是任意内容,例如 LLM 工具调用 / 结构化输出等。
    goto = get_next_agent(...)  # 'agent' / 'another_agent'
    return Command(
        # 指定下一步调用哪个智能体
        goto=goto,
        # 更新图状态
        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"},
        # 指定要导航到哪个图(默认为当前图)
        graph=Command.PARENT,
    )

Note

如果你需要支持使用 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 Reference: tool

from langchain_core.tools import tool

@tool
def transfer_to_bob():
    """转交给 bob。"""
    return Command(
        # 要前往的智能体(节点)名称
        goto="bob",
        # 发送给该智能体的数据
        update={"my_state_key": "my_state_value"},
        # 向 LangGraph 表明我们需要导航到
        # 父图中的智能体节点
        graph=Command.PARENT,
    )

这是从工具更新图状态的一个特例,除了状态更新外,还包括了控制流信息。

Important

如果你想使用返回 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 Reference: 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]]:
    # 你可以将状态的相关部分传递给LLM(例如 state["messages"])
    # 以决定下一步调用哪个智能体。常见模式是使用结构化输出调用模型
    # (例如强制模型返回包含 "next_agent" 字段的输出)
    response = model.invoke(...)
    # 根据LLM的决策路由到某个智能体或退出
    # 如果LLM返回 "__end__",图将结束执行
    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 Reference: 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]]:
    # 你可以将状态的相关部分传递给LLM(例如 state["messages"])
    # 以决定下一步调用哪个智能体。常见模式是使用结构化输出调用模型
    # (例如强制模型返回包含 "next_agent" 字段的输出)
    response = model.invoke(...)
    # 根据监督者的决策路由到某个智能体或退出
    # 如果监督者返回 "__end__",图将结束执行
    return Command(goto=response["next_agent"])

def agent_1(state: MessagesState) -> Command[Literal["supervisor"]]:
    # 你可以将状态的相关部分传递给LLM(例如 state["messages"])
    # 并添加任何额外逻辑(不同模型、自定义提示、结构化输出等)
    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()

请查看这个教程,了解监督型多智能体架构的示例。

监督型(工具调用)

在这一监督型架构的变体中,我们定义了一个智能体作为监督者,负责调用子智能体。子智能体以工具的形式暴露给监督者,监督者智能体决定下一步调用哪个工具。监督者智能体遵循标准实现,即LLM在while循环中调用工具,直到决定停止。

API Reference: ChatOpenAI | InjectedState | create_react_agent

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

model = ChatOpenAI()

# 这是将作为工具调用的智能体函数
# 注意:你可以通过 InjectedState 注解将状态传递给工具
def agent_1(state: Annotated[dict, InjectedState]):
    # 你可以将状态的相关部分传递给LLM(例如 state["messages"])
    # 并添加任何额外逻辑(不同模型、自定义提示、结构化输出等)
    response = model.invoke(...)
    # 将LLM响应作为字符串返回(预期的工具响应格式)
    # 预构建的 create_react_agent(监督者)会自动将其转换为 ToolMessage
    return response.content

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

tools = [agent_1, agent_2]
# 构建带工具调用的监督者的最简单方法是使用预构建的 ReAct 智能体图
# 它由一个工具调用LLM节点(即监督者)和一个工具执行节点组成
supervisor = create_react_agent(model, tools)

层级型

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

为了解决这个问题,你可以**分层**设计你的系统。例如,你可以创建由个别监督者管理的独立专业化智能体团队,以及一个顶层监督者来管理这些团队。

API Reference: 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()

# 定义团队1(同上述单一监督者示例)

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()

# 定义团队2(同上述单一监督者示例)
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()


# 定义顶层监督者

builder = StateGraph(MessagesState)
def top_level_supervisor(state: MessagesState) -> Command[Literal["team_1_graph", "team_2_graph", END]]:
    # 你可以将状态的相关部分传递给LLM(例如 state["messages"])
    # 以决定下一步调用哪个团队。常见模式是使用结构化输出调用模型
    # (例如强制模型返回包含 "next_team" 字段的输出)
    response = model.invoke(...)
    # 根据监督者的决策路由到某个团队或退出
    # 如果监督者返回 "__end__",图将结束执行
    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 Reference: 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)
# 显式定义流程
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中有两种方法可以实现这一点:

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