【Agent的革命之路——LangGraph】多个agent的多轮对话如此方便

577 阅读7分钟

我们今天来讲述一下多轮对话,我们将编写一个程序,让用户可以和一个和多个 agent 进行多轮对话。我们还是使用 interrupt 来获得用户的输入然后回到 active agent (活动代理)的节点。 一个 agent 代理可作为 graph 图中的节点,执行 agent 代理步骤并确定下一步操作:首先等待用户的输入继续对话,然后通过切换导航到另一个代理(或返回到自身,例如在循环中)。

代码如下

def human(state: MessagesState) -> Command[Literal["agent", "another_agent"]]:
    """获得用户的输入."""
    user_input = interrupt(value="Ready for user input.")

    # 决定这个活跃的agent
    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") 

定义agent

在我们新写的例子中,我们要建立一个旅行助理代理团队,他们可以通过交流相互沟通。 下面我们先创建两个 agenttravel_advisor agent:可以帮助推荐旅行目的地。可以向hotel_advisor agent寻求帮助。 hotel_advisor agent:可以帮助推荐酒店。可以向travel_advisor agent寻求帮助。 我们将使用 create_react_agent 这种方式来构建 agent ,因为我们的每个 agent 都将拥有特定于其专业领域的工具以及用于移交到另一个agent 的特殊工具。然后下面开始定义我们的工具:

import random
from typing import Annotated, Literal

from langchain_core.tools import tool
from langchain_core.tools.base import InjectedToolCallId
from langgraph.prebuilt import InjectedState


@tool
def get_travel_recommendations():
	""" 获得旅游城市推荐 """
    return random.choice(["北京", "上海"])


@tool
def get_hotel_recommendations(location: Literal["北京", "上海"]):
	""" 获得酒店推荐 """
    return {
        "北京": [
            "宝格丽"
            "希尔顿"
        ],
        "上海": ["外滩18号", "帝豪"],
    }[location]

# 构建传输tool
def make_handoff_tool(*, agent_name: str):
    tool_name = f"transfer_to_{agent_name}"

    @tool(tool_name)
    def handoff_to_agent(
        state: Annotated[dict, InjectedState],
        tool_call_id: Annotated[str, InjectedToolCallId],
    ):
        """Ask another agent for help."""
        tool_message = {
            "role": "tool",
            "content": f"Successfully transferred to {agent_name}",
            "name": tool_name,
            "tool_call_id": tool_call_id,
        }
        return Command(
            goto=agent_name,
            graph=Command.PARENT,
            
			# 这是 agent_name 代理被调用时将看到的状态更新
			# 我们传递代理的完整内部消息历史记录,并添加一个工具消息
			# 以确保生成的聊天历史是有效的
            update={"messages": state["messages"] + [tool_message]},
        )

    return handoff_to_agent

我们现在来设置一个对话系统,其中包含多个AI代理。当这些代理完成对话后,系统会自动切换回人类用户的节点,让用户可以继续输入。这种设置确保了对话流程的顺序性和交互性。每个代理的响应都被设计成在完成后自动转向人类用户,这是通过特定的命令结构来实现的。 大概的步骤:

  1. 首先要使用预构建的 create_react_agent 函数来创建代理
  2. 然后需要定义一个专门的 human 节点,这个节点带有 interrupt 功能
  3. 在代理给出最终回应后,系统会路由到这个 human 节点
  4. 为了实现这个功能,每个代理的调用都被封装在一个单独的节点函数中
  5. 这个节点函数会返回一个带有 goto="human" 参数的 Command 命令
# 定义旅行顾问工具和ReAct代理
travel_advisor_tools = [
    get_travel_recommendations,
    make_handoff_tool(agent_name="hotel_advisor"),
]
travel_advisor = create_react_agent(
    model,
    travel_advisor_tools,
    state_modifier=(
            "您是一位可以推荐旅行目的地(例如国家、城市等)的普通旅行专家。"
            "如果您需要酒店推荐,请向 'hotel_advisor' 寻求帮助。"
            "在转移到另一个代理之前,您必须包含人类能够阅读的响应"
    ),
)

def call_travel_advisor(
    state: MessagesState,
) -> Command[Literal["hotel_advisor", "human"]]:
    # 还可以添加其他逻辑,如更改代理的输入/代理的输出等。
    # #注意:我们正在调用ReAct代理,其中包含完整消息的历史记录
    response = travel_advisor.invoke(state)
    return Command(update=response, goto="human")


# 定义酒店顾问工具和ReAct代理
hotel_advisor_tools = [
    get_hotel_recommendations,
    make_handoff_tool(agent_name="travel_advisor"),
]
hotel_advisor = create_react_agent(
    model,
    hotel_advisor_tools,
    state_modifier=(
        "您是一位酒店专家,可以为特定旅游目的地提供酒店推荐。"
        "如果您在选择旅行目的地时需要帮助,请向travel_visor寻求帮助。"
        "在转移到另一个代理之前,您必须包含人类能够阅读的响应。"
    ),
)

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

# 定义一个人类节点,通过 langgraph_triggers 来追踪最后一个与用户交互的顾问,确保对话能够正确地继续进行。
def human_node(
    state: MessagesState, config
) -> Command[Literal["hotel_advisor", "travel_advisor", "human"]]:
    """A node for collecting user input."""
    user_input = interrupt(value="等待用户的输入.")

    # 从配置中获取触发器信息
    langgraph_triggers = config["metadata"]["langgraph_triggers"]

    # 确保只有一个触发器
    if len(langgraph_triggers) != 1:
        raise AssertionError("Expected exactly 1 trigger in human node")

    # 获取最新一个活跃的 agent 名称(通过分割触发器字符串)
    active_agent = langgraph_triggers[0].split(":")[1]
    return Command(
        update={
            "messages": [
                {
                    "role": "human",
                    "content": user_input,
                }
            ]
        },
        goto=active_agent,
    )

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

# 这行代码将之前定义的 human_node 函数添加为图中的一个节点,用于处理用户输入。这个节点会将用户的输入路由回活跃的代理(agent)。
builder.add_node("human", human_node)

# We'll always start with a general travel advisor.
builder.add_edge(START, "travel_advisor")


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

from IPython.display import display, Image
display(Image(graph.get_graph().draw_mermaid_png()))

我们现在来看下它的图形: 在这里插入图片描述

测试多轮对话

让我们用这个应用程序来测试一下多轮对话。我们自己来创建三轮对话:

import uuid

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

inputs = [
    {
        "messages": [
            {"role": "user", "content": "我想去三亚温暖的地方"}
        ]
    },
    Command(
        resume="你能在其中一个地区推荐一家不错的酒店吗? 告诉我它在哪个地区."
    ),
    Command(
        resume="我喜欢第一个推荐。你能推荐一些在酒店附近的其他东西吗?"
    ),
]

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}")

看看我们的结果: 在这里插入图片描述 神奇吧,当然我们子此多轮对话的基础上还可以衍生出许多功能来增强系统的能力和用户体验。例如,扩展目的地和酒店推荐的范围,增加更多城市和酒店选项,甚至根据用户偏好(如预算、旅行类型)进行个性化推荐。还可以集成外部API,如航班信息、天气预报、景点推荐等,提供更全面的旅行规划服务。

为了支持多语言用户,可以增加多语言支持功能。此外,系统可以引入用户偏好和历史记录功能,根据用户的历史选择提供更精准的推荐。对话管理方面,可以增强上下文记忆和状态恢复能力,确保长时间对话的连贯性。如果需要更复杂的协作,可以增加更多代理(如景点推荐代理、交通代理等),实现多代理协同工作。用户反馈机制也可以加入,允许用户对推荐结果进行评分或反馈,从而优化推荐算法。为了提升自然语言理解能力,可以使用更先进的模型来更好地理解用户意图。最后,可以开发一个可视化界面(如Web或移动应用),方便用户与系统交互,甚至集成支付和预订功能,使用户能够直接通过系统完成酒店或航班的预订。这些功能的加入将使系统更加智能、全面和用户友好,提供更优质的旅行规划服务。