多智能体系统¶
智能体是“使用大语言模型(LLM)来决定应用程序控制流的系统”。在开发这些系统时,随着时间的推移,它们可能会变得更加复杂,从而更难管理和扩展。例如,你可能会遇到以下问题:
- 智能体可使用的工具过多,在决定接下来调用哪个工具时做出糟糕的决策
- 上下文变得过于复杂,单个智能体难以跟踪
- 系统中需要多个专业领域(例如规划者、研究者、数学专家等)
为了解决这些问题,你可以考虑将应用程序拆分为多个更小的、独立的智能体,并将它们组合成一个**多智能体系统**。这些独立的智能体可以简单到只是一个提示和一次大语言模型调用,也可以复杂到像一个 ReAct 智能体(甚至更复杂!)。
使用多智能体系统的主要好处有:
- 模块化:独立的智能体使开发、测试和维护智能体系统变得更加容易。
- 专业化:你可以创建专注于特定领域的专家智能体,这有助于提高整个系统的性能。
- 可控性:你可以明确控制智能体之间的通信方式(而不是依赖函数调用)。
多智能体架构¶
在多智能体系统中,有几种连接智能体的方式:
- 网络:每个智能体都可以与其他所有智能体进行通信。任何智能体都可以决定接下来调用哪个其他智能体。
- 监督者:每个智能体都与单个监督者智能体进行通信。监督者智能体决定接下来应该调用哪个智能体。
- 监督者(工具调用):这是监督者架构的一种特殊情况。单个智能体可以表示为工具。在这种情况下,监督者智能体使用支持工具调用的大语言模型(LLM)来决定调用哪个智能体工具,以及传递给这些智能体的参数。
- 分层:你可以定义一个具有监督者的监督者的多智能体系统。这是监督者架构的一种泛化,允许更复杂的控制流。
- 自定义多智能体工作流:每个智能体仅与一部分智能体进行通信。部分流程是确定性的,只有一些智能体可以决定接下来调用哪个其他智能体。
交接¶
在多智能体架构中,智能体可以表示为图节点。每个智能体节点执行其步骤,并决定是结束执行还是路由到另一个智能体,包括可能路由到自身(例如,在循环中运行)。多智能体交互中的一个常见模式是交接,即一个智能体将控制权交给另一个智能体。交接允许你指定:
- 目的地:要导航到的目标智能体(例如,要前往的节点名称)
- 有效负载:要传递给该智能体的信息(例如,状态更新)
要在 LangGraph 中实现交接,智能体节点可以返回 Command
对象,该对象允许你将控制流和状态更新结合起来:
def agent(state) -> Command[Literal["agent", "another_agent"]]:
# 路由/停止的条件可以是任何条件,例如大语言模型工具调用/结构化输出等。
goto = get_next_agent(...) # 'agent' / 'another_agent'
return Command(
# 指定接下来要调用的智能体
goto=goto,
# 更新图状态
update={"my_state_key": "my_state_value"}
)
在更复杂的场景中,每个智能体节点本身就是一个图(即,一个子图),某个智能体子图中的一个节点可能想要导航到不同的智能体。例如,如果你有两个智能体 alice
和 bob
(父图中的子图节点),并且 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
注解的节点函数来包装它们,例如,不要这样做:
你需要这样做:
将交接作为工具¶
最常见的智能体类型之一是 ReAct 风格的工具调用智能体。对于这些类型的智能体,一种常见的模式是将交接包装在工具调用中,例如:
def transfer_to_bob(state):
"""转移到 bob。"""
return Command(
goto="bob",
update={"my_state_key": "my_state_value"},
graph=Command.PARENT,
)
这是从工具更新图状态的一种特殊情况,除了状态更新之外,还包括控制流。
Important
如果你想使用返回 Command
的工具,你可以使用预构建的 create_react_agent
/ ToolNode
组件,或者实现你自己的工具执行节点,该节点收集工具返回的 Command
对象并返回一个列表,例如:
现在让我们更详细地了解不同的多智能体架构。
网络¶
在这种架构中,智能体被定义为图节点。每个智能体都可以与其他所有智能体进行通信(多对多连接),并可以决定接下来调用哪个智能体。这种架构适用于没有明确的智能体层次结构或没有特定的智能体调用顺序的问题。
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]]:
# 你可以将状态的相关部分传递给大语言模型(例如,state["messages"])
# 以确定接下来要调用哪个智能体。一种常见的模式是使用结构化输出调用模型
# (例如,强制它返回一个包含 "next_agent" 字段的输出)
response = model.invoke(...)
# 根据大语言模型的决策路由到其中一个智能体或退出
# 如果大语言模型返回 "__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()
监督者¶
在这种架构中,我们将智能体定义为节点,并添加一个监督者节点(大语言模型),该节点决定接下来应该调用哪个智能体节点。我们使用 Command
根据监督者的决策将执行路由到适当的智能体节点。这种架构也非常适合并行运行多个智能体或使用映射-归约模式。
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]]:
# 你可以将状态的相关部分传递给大语言模型(例如,state["messages"])
# 以确定接下来要调用哪个智能体。一种常见的模式是使用结构化输出调用模型
# (例如,强制它返回一个包含 "next_agent" 字段的输出)
response = model.invoke(...)
# 根据监督者的决策路由到其中一个智能体或退出
# 如果监督者返回 "__end__",图将结束执行
return Command(goto=response["next_agent"])
def agent_1(state: MessagesState) -> Command[Literal["supervisor"]]:
# 你可以将状态的相关部分传递给大语言模型(例如,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()
查看此教程,了解监督者多智能体架构的示例。
监督者(工具调用)¶
在监督者架构的这种变体中,我们将单个智能体定义为**工具**,并在监督者节点中使用支持工具调用的大语言模型。这可以实现为一个具有两个节点的 ReAct 风格的智能体 —— 一个大语言模型节点(监督者)和一个执行工具(在这种情况下是智能体)的工具调用节点。
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]):
# 你可以将状态的相关部分传递给大语言模型(例如,state["messages"])
# 并添加任何额外的逻辑(不同的模型、自定义提示、结构化输出等)
response = model.invoke(...)
# 将大语言模型的响应作为字符串返回(预期的工具响应格式)
# 这将由预构建的 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 智能体图
# 该图由一个工具调用大语言模型节点(即监督者)和一个工具执行节点组成
supervisor = create_react_agent(model, tools)
分层¶
当你向系统中添加更多智能体时,监督者可能会难以管理所有智能体。监督者可能会开始在决定接下来调用哪个智能体时做出糟糕的决策,或者上下文可能会变得过于复杂,以至于单个监督者无法跟踪。换句话说,你最终会遇到最初促使采用多智能体架构的相同问题。
为了解决这个问题,你可以对系统进行_分层_设计。例如,你可以创建由各个监督者管理的独立、专业化的智能体团队,以及一个顶级监督者来管理这些团队。
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]]:
# 你可以将状态的相关部分传递给大语言模型(例如,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 中,你可以允许大语言模型决定应用程序控制流的部分内容。这可以通过使用
Command
来实现。这种情况的一个特殊例子是监督者工具调用架构。在这种情况下,为监督者智能体提供支持的工具调用大语言模型将决定工具(智能体)的调用顺序。
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")
智能体之间的通信¶
构建多智能体系统时,最重要的事情是弄清楚智能体之间如何进行通信。有几个不同的方面需要考虑:
- 智能体是通过图状态还是通过工具调用进行通信?
- 如果两个智能体有不同的状态模式,该怎么办?
- 如何通过共享消息列表进行通信?
图状态与工具调用¶
在智能体之间传递的“有效负载”是什么?在上述大多数架构中,智能体通过图状态进行通信。在带有工具调用的监督者的情况下,有效负载是工具调用参数。
图状态¶
要通过图状态进行通信,单个智能体需要被定义为图节点。这些可以作为函数或整个子图添加。在图执行的每一步,智能体节点接收图的当前状态,执行智能体代码,然后将更新后的状态传递给下一个节点。
通常,智能体节点共享单个状态模式。但是,你可能希望设计具有不同状态模式的智能体节点。
不同的状态模式¶
一个智能体可能需要与其他智能体具有不同的状态模式。例如,一个搜索智能体可能只需要跟踪查询和检索到的文档。在 LangGraph 中有两种方法可以实现这一点:
- 定义具有单独状态模式的子图智能体。如果子图和父图之间没有共享的状态键(通道),那么添加输入/输出转换就很重要,这样父图就知道如何与子图进行通信。
- 定义具有私有输入状态模式的智能体节点函数,该模式与整个图的状态模式不同。这允许传递仅执行该特定智能体所需的信息。
共享消息列表¶
智能体之间最常见的通信方式是通过共享状态通道,通常是一个消息列表。这假设状态中始终至少有一个通道(键)是智能体共享的。通过共享消息列表进行通信时,还有一个额外的考虑因素:智能体应该共享其思考过程的完整历史记录还是只共享最终结果?
共享完整历史记录¶
智能体可以与所有其他智能体**共享其思考过程的完整历史记录**(即“草稿本”)。这个“草稿本”通常看起来像一个消息列表。共享完整思考过程的好处是,它可能有助于其他智能体做出更好的决策,并提高整个系统的推理能力。缺点是,随着智能体数量及其复杂性的增加,“草稿本”会迅速增长,可能需要额外的内存管理策略。
共享最终结果¶
智能体可以有自己的私有“草稿本”,并且只与其他智能体**共享最终结果**。这种方法可能更适用于具有许多智能体或更复杂智能体的系统。在这种情况下,你需要定义具有不同状态模式的智能体。
对于作为工具调用的智能体,监督者根据工具模式确定输入。此外,LangGraph 允许在运行时将状态传递给各个工具,因此如果需要,从属智能体可以访问父状态。