Skip to content

构建多智能体系统

如果一个代理需要在多个领域进行专业化或管理许多工具,它可能会遇到困难。为了解决这个问题,你可以将你的代理拆分成更小的、独立的代理,并将它们组合成多代理系统

在多代理系统中,代理之间需要相互通信。他们通过交接来实现这一点——这是一种描述将控制权交给哪个代理以及发送给该代理的有效负载的原始机制。

本指南涵盖以下内容:

要开始构建多代理系统,请查看LangGraph对两种最流行的多代理架构的预构建实现——监督器蜂群

手动交接

要在多智能体系统中设置智能体之间的通信,可以使用handoffs——一种模式,其中一个智能体将控制权*交给*另一个。Handoffs允许您指定:

  • destination: 要导航到的目标智能体(例如,要前往的LangGraph节点名称)
  • payload: 传递给该智能体的信息(例如,状态更新)

创建 handoffs

要实现 handoffs,可以从您的智能体节点或工具中返回 Command 对象:

API Reference: tool | InjectedToolCallId | create_react_agent | InjectedState | StateGraph | START | Command

from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.types import Command

def create_handoff_tool(*, agent_name: str, description: str | None = None):
    name = f"transfer_to_{agent_name}"
    description = description or f"Transfer to {agent_name}"

    @tool(name, description=description)
    def handoff_tool(
        state: Annotated[MessagesState, InjectedState], # (1)!
        tool_call_id: Annotated[str, InjectedToolCallId],
    ) -> Command:
        tool_message = {
            "role": "tool",
            "content": f"Successfully transferred to {agent_name}",
            "name": name,
            "tool_call_id": tool_call_id,
        }
        return Command(  # (2)!
            goto=agent_name,  # (3)!
            update={"messages": state["messages"] + [tool_message]},  # (4)!
            graph=Command.PARENT,  # (5)!
        )
    return handoff_tool
  1. 使用 InjectedState 注解访问调用 handoff 工具的智能体的状态。更多信息请参见 这篇指南
  2. Command 原语允许在一个操作中指定状态更新和节点转换,这使其在实现 handoffs 时非常有用。
  3. 要移交控制权的智能体或节点的名称。
  4. 将智能体的消息添加到父级的**状态**中作为交接的一部分。下一个智能体会看到父级状态。
  5. 向 LangGraph 指示我们需要在**父级**多智能体图中导航到智能体节点。

Tip

如果你想使用返回 Command 的工具,你可以使用预构建的 create_react_agent / ToolNode 组件,或者自己实现一个工具执行节点,收集由工具返回的 Command 对象并返回它们的列表,例如:

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

Important

此 handoff 实现假定以下几点:

  • 每个智能体在其输入中接收多智能体系统中的整体消息历史记录。如果你想对智能体输入有更多控制,请查看 此部分
  • 每个智能体将其内部消息历史记录输出到多智能体系统的整体消息历史记录中。如果你想对**如何添加智能体输出**有更多控制,请将智能体包装在一个单独的节点函数中:

    def call_hotel_assistant(state):
        # 返回智能体的最终响应,
        # 排除内部独白
        response = hotel_assistant.invoke(state)
        return {"messages": response["messages"][-1]}
    

控制代理输入

你可以使用 [Send()][langgraph.types.Send] 原语在交接过程中直接向工作代理发送数据。例如,你可以要求调用代理为下一个代理填充任务描述:

API Reference: tool | InjectedToolCallId | InjectedState | StateGraph | START | Command | Send

from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import InjectedState
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.types import Command, Send

def create_task_description_handoff_tool(
    *, agent_name: str, description: str | None = None
):
    name = f"transfer_to_{agent_name}"
    description = description or f"Ask {agent_name} for help."

    @tool(name, description=description)
    def handoff_tool(
        # 这个参数由调用代理填充
        task_description: Annotated[
            str,
            "描述下一个代理应该做什么,包括所有相关上下文。",
        ],
        # 这些参数被LLM忽略
        state: Annotated[MessagesState, InjectedState],
    ) -> Command:
        task_description_message = {"role": "user", "content": task_description}
        agent_input = {**state, "messages": [task_description_message]}
        return Command(
            goto=[Send(agent_name, agent_input)],
            graph=Command.PARENT,
        )

    return handoff_tool

有关在交接中使用 [Send()][langgraph.types.Send] 的完整示例,请参阅多代理 监督器 教程。

构建多代理系统

你可以在使用 LangGraph 构建的任何代理中使用交接(handoffs)。我们推荐使用预构建的 agentToolNode,因为它们原生支持返回 Command 的交接工具。下面是一个使用交接实现预订旅行的多代理系统的示例:

API Reference: create_react_agent | StateGraph | START

from langgraph.prebuilt import create_react_agent
from langgraph.graph import StateGraph, START, MessagesState

def create_handoff_tool(*, agent_name: str, description: str | None = None):
    # 与上面相同的实现
    ...
    return Command(...)

# 交接
transfer_to_hotel_assistant = create_handoff_tool(agent_name="hotel_assistant")
transfer_to_flight_assistant = create_handoff_tool(agent_name="flight_assistant")

# 定义代理
flight_assistant = create_react_agent(
    model="anthropic:claude-3-5-sonnet-latest",
    tools=[..., transfer_to_hotel_assistant],
    name="flight_assistant"
)
hotel_assistant = create_react_agent(
    model="anthropic:claude-3-5-sonnet-latest",
    tools=[..., transfer_to_flight_assistant],
    name="hotel_assistant"
)

# 定义多代理图
multi_agent_graph = (
    StateGraph(MessagesState)
    .add_node(flight_assistant)
    .add_node(hotel_assistant)
    .add_edge(START, "flight_assistant")
    .compile()
)
完整示例:用于预订旅行的多代理系统
from typing import Annotated
from langchain_core.messages import convert_to_messages
from langchain_core.tools import tool, InjectedToolCallId
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.types import Command

# 我们将使用 `pretty_print_messages` 辅助函数来漂亮地渲染流式代理输出

def pretty_print_message(message, indent=False):
    pretty_message = message.pretty_repr(html=True)
    if not indent:
        print(pretty_message)
        return

    indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
    print(indented)


def pretty_print_messages(update, last_message=False):
    is_subgraph = False
    if isinstance(update, tuple):
        ns, update = update
        # 在打印中跳过父图更新
        if len(ns) == 0:
            return

        graph_id = ns[-1].split(":")[0]
        print(f"来自子图 {graph_id} 的更新:")
        print("\n")
        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)
        print("\n")

        messages = convert_to_messages(node_update["messages"])
        if last_message:
            messages = messages[-1:]

        for m in messages:
            pretty_print_message(m, indent=is_subgraph)
        print("\n")


def create_handoff_tool(*, agent_name: str, description: str | None = None):
    name = f"transfer_to_{agent_name}"
    description = description or f"Transfer to {agent_name}"

    @tool(name, description=description)
    def handoff_tool(
        state: Annotated[MessagesState, InjectedState], # (1)!
        tool_call_id: Annotated[str, InjectedToolCallId],
    ) -> Command:
        tool_message = {
            "role": "tool",
            "content": f"Successfully transferred to {agent_name}",
            "name": name,
            "tool_call_id": tool_call_id,
        }
        return Command(  # (2)!
            goto=agent_name,  # (3)!
            update={"messages": state["messages"] + [tool_message]},  # (4)!
            graph=Command.PARENT,  # (5)!
        )
    return handoff_tool

# 交接
transfer_to_hotel_assistant = create_handoff_tool(
    agent_name="hotel_assistant",
    description="Transfer user to the hotel-booking assistant.",
)
transfer_to_flight_assistant = create_handoff_tool(
    agent_name="flight_assistant",
    description="Transfer user to the flight-booking assistant.",
)

# 简单代理工具
def book_hotel(hotel_name: str):
    """Book a hotel"""
    return f"Successfully booked a stay at {hotel_name}."

def book_flight(from_airport: str, to_airport: str):
    """Book a flight"""
    return f"Successfully booked a flight from {from_airport} to {to_airport}."

# 定义代理
flight_assistant = create_react_agent(
    model="anthropic:claude-3-5-sonnet-latest",
    tools=[book_flight, transfer_to_hotel_assistant],
    prompt="You are a flight booking assistant",
    name="flight_assistant"
)
hotel_assistant = create_react_agent(
    model="anthropic:claude-3-5-sonnet-latest",
    tools=[book_hotel, transfer_to_flight_assistant],
    prompt="You are a hotel booking assistant",
    name="hotel_assistant"
)

# 定义多代理图
multi_agent_graph = (
    StateGraph(MessagesState)
    .add_node(flight_assistant)
    .add_node(hotel_assistant)
    .add_edge(START, "flight_assistant")
    .compile()
)

# 运行多代理图
for chunk in multi_agent_graph.stream(
    {
        "messages": [
            {
                "role": "user",
                "content": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
            }
        ]
    },
    subgraphs=True
):
    pretty_print_messages(chunk)
  1. 访问代理的状态
  2. Command 原语允许在一个操作中指定状态更新和节点转换,这在实现交接时非常有用。
  3. 要交接给的代理或节点的名称。
  4. 将代理的消息添加到父级的 状态 中作为交接的一部分。下一个代理将看到父级状态。
  5. 向 LangGraph 表明我们需要导航到 父级 多代理图中的代理节点。

多轮对话

用户可能希望与一个或多个代理进行 多轮对话。为了构建能够处理这种情况的系统,您可以创建一个节点,使用 [interrupt][langgraph.types.interrupt] 来收集用户输入,并路由回 当前活动 的代理。

这些代理可以作为图中的节点实现,该图执行代理步骤并决定下一步操作:

  1. 等待用户输入 以继续对话,或者
  2. 路由到另一个代理(或返回自身,例如在循环中)通过 handoff
def human(state) -> Command[Literal["agent", "another_agent"]]:
    """用于收集用户输入的节点。"""
    user_input = interrupt(value="Ready for user input.")

    # 确定当前活动的代理。
    active_agent = ...

    ...
    return Command(
        update={
            "messages": [{
                "role": "human",
                "content": user_input,
            }]
        },
        goto=active_agent
    )

def agent(state) -> Command[Literal["agent", "another_agent", "human"]]:
    # 路由/停止的条件可以是任何内容,例如 LLM 工具调用 / 结构化输出等。
    goto = get_next_agent(...)  # 'agent' / 'another_agent'
    if goto:
        return Command(goto=goto, update={"my_state_key": "my_state_value"})
    else:
        return Command(goto="human") # 前往 human 节点
完整示例:旅行推荐的多代理系统

在这个示例中,我们将构建一个旅行助手代理团队,这些代理可以通过 handoff 相互通信。

我们将创建两个代理:

  • travel_advisor: 可以帮助推荐旅行目的地。可以请求 hotel_advisor 的帮助。
  • hotel_advisor: 可以帮助推荐酒店。可以请求 travel_advisor 的帮助。
from langchain_anthropic import ChatAnthropic
from langgraph.graph import MessagesState, StateGraph, START
from langgraph.prebuilt import create_react_agent, InjectedState
from langgraph.types import Command, interrupt
from langgraph.checkpoint.memory import MemorySaver


model = ChatAnthropic(model="claude-3-5-sonnet-latest")

class MultiAgentState(MessagesState):
    last_active_agent: str


# 定义 travel advisor 工具和 ReAct 代理
travel_advisor_tools = [
    get_travel_recommendations,
    make_handoff_tool(agent_name="hotel_advisor"),
]
travel_advisor = create_react_agent(
    model,
    travel_advisor_tools,
    prompt=(
        "你是一个通用的旅行专家,可以推荐旅行目的地(如国家、城市等)。 "
        "如果需要酒店推荐,请请求 'hotel_advisor' 的帮助。 "
        "在转移到其他代理之前,你必须包含可读性的人类响应。"
    ),
)


def call_travel_advisor(
    state: MultiAgentState,
) -> Command[Literal["hotel_advisor", "human"]]:
    # 你也可以添加额外的逻辑,比如更改代理的输入/输出等。
    # 注意:我们使用状态中的消息历史记录来调用 ReAct 代理
    response = travel_advisor.invoke(state)
    update = {**response, "last_active_agent": "travel_advisor"}
    return Command(update=update, goto="human")


# 定义 hotel advisor 工具和 ReAct 代理
hotel_advisor_tools = [
    get_hotel_recommendations,
    make_handoff_tool(agent_name="travel_advisor"),
]
hotel_advisor = create_react_agent(
    model,
    hotel_advisor_tools,
    prompt=(
        "你是一个酒店专家,可以为给定的目的地提供酒店推荐。 "
        "如果需要帮助选择旅行目的地,请请求 'travel_advisor' 的帮助。"
        "在转移到其他代理之前,你必须包含可读性的人类响应。"
    ),
)


def call_hotel_advisor(
    state: MultiAgentState,
) -> Command[Literal["travel_advisor", "human"]]:
    response = hotel_advisor.invoke(state)
    update = {**response, "last_active_agent": "hotel_advisor"}
    return Command(update=update, goto="human")


def human_node(
    state: MultiAgentState, config
) -> Command[Literal["hotel_advisor", "travel_advisor", "human"]]:
    """用于收集用户输入的节点。"""

    user_input = interrupt(value="Ready for user input.")
    active_agent = state["last_active_agent"]

    return Command(
        update={
            "messages": [
                {
                    "role": "human",
                    "content": user_input,
                }
            ]
        },
        goto=active_agent,
    )


builder = StateGraph(MultiAgentState)
builder.add_node("travel_advisor", call_travel_advisor)
builder.add_node("hotel_advisor", call_hotel_advisor)

# 这会添加一个节点来收集人类输入,它将路由
# 回到当前活动的代理。
builder.add_node("human", human_node)

# 我们总是从通用的 travel advisor 开始。
builder.add_edge(START, "travel_advisor")


checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)

让我们测试一下这个应用程序的多轮对话。

import uuid

thread_config = {"configurable": {"thread_id": str(uuid.uuid4())}}

inputs = [
    # 第一轮对话,
    {
        "messages": [
            {"role": "user", "content": "i wanna go somewhere warm in the caribbean"}
        ]
    },
    # 由于我们使用了 `interrupt`,我们需要使用 Command 原语来恢复。
    # 第二轮对话,
    Command(
        resume="could you recommend a nice hotel in one of the areas and tell me which area it is."
    ),
    # 第三轮对话,
    Command(
        resume="i like the first one. could you recommend something to do near the hotel?"
    ),
]

for idx, user_input in enumerate(inputs):
    print()
    print(f"--- Conversation Turn {idx + 1} ---")
    print()
    print(f"User: {user_input}")
    print()
    for update in graph.stream(
        user_input,
        config=thread_config,
        stream_mode="updates",
    ):
        for node_id, value in update.items():
            if isinstance(value, dict) and value.get("messages", []):
                last_message = value["messages"][-1]
                if isinstance(last_message, dict) or last_message.type != "ai":
                    continue
                print(f"{node_id}: {last_message.content}")
--- Conversation Turn 1 ---

User: {'messages': [{'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean'}]}

travel_advisor: 根据推荐,阿鲁巴将是您加勒比之旅的一个绝佳选择!阿鲁巴被称为“快乐岛”,提供以下特色:
- 全年温暖天气,气温常年保持在约82°F(28°C)
- 美丽的白色沙滩,如鹰滩和棕榈滩
- 清澈的蓝绿色海水,非常适合游泳和浮潜
- 降雨量少且位于飓风带之外
- 加勒比和荷兰文化的融合
- 丰富的餐饮选择和夜生活
- 各种水上运动和活动

您是否需要我为您在阿鲁巴推荐一些具体的酒店?我可以将您转介给我们的酒店顾问,他们可以帮助您安排住宿。

--- Conversation Turn 2 ---

User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')

hotel_advisor: 根据推荐,我可以建议两个非常棒的选择:

1. 阿鲁巴丽思卡尔顿酒店 - 位于棕榈滩
- 这家豪华度假酒店位于热闹的棕榈滩地区
- 以其卓越的服务和设施而闻名
- 如果您想靠近餐饮、购物和娱乐场所,这是个绝佳选择
- 提供多家餐厅、赌场和世界级水疗中心
- 位于棕榈滩一片纯净的沙滩上

2. Bucuti & Tara 海滩度假村 - 位于鹰滩
- 一家仅限成人入住的精品度假村,位于鹰滩
- 以更加亲密和宁静著称
- 因其可持续发展实践而获奖
- 适合浪漫之旅或宁静假期
- 位于加勒比海最美丽的海滩之一

您是否需要更多关于这两处房产及其位置的具体信息?

--- Conversation Turn 3 ---

User: Command(resume='i like the first one. could you recommend something to do near the hotel?')

travel_advisor: 在棕榈滩的丽思卡尔顿酒店附近,有以下高度推荐的活动:

1. 参观棕榈滩广场购物中心 - 距离酒店步行几分钟即可到达,提供购物、餐饮和娱乐
2. 尝试在丽思卡尔顿内部的 Stellaris 博彩馆试试手气
3. 乘坐日落帆船游 - 很多船只从附近的码头出发
4. 参观加利福尼亚灯塔 - 位于棕榈滩北边的一个风景名胜
5. 在棕榈滩享受水上运动:
   - 水上摩托艇
   - 滑翔伞
   - 潜水
   - 站立式划桨

您是否需要了解更多关于这些活动的信息,或者想知道该地区还有哪些其他选择?

预构建实现

LangGraph 提供了两种最流行的多智能体架构的预构建实现:

  • supervisor — 由一个中心监督智能体协调各个智能体。监督智能体控制所有通信流和任务委托,根据当前上下文和任务需求决定调用哪个智能体。你可以使用 langgraph-supervisor 库来创建一个监督型多智能体系统。
  • swarm — 智能体根据各自的专长动态地将控制权交给其他智能体。系统会记住最后一个活跃的智能体,确保后续交互中对话能从该智能体继续进行。你可以使用 langgraph-swarm 库来创建一个群体型多智能体系统。