LangGraph实现多代理任务

LangGraph实现多代理任务


任务背景

假设用户现有一问题:"今年 2025 年,获取过去 5 年中国统计人口(亿),然后绘制一条折线图。"

我们希望这个问题送入大模型中能够返回的结果如下:

这是中国近五年人口统计的折线图,显示出人口的变化趋势。根据数据,过去五年人口在 2020 年达到最高点之后略有下降,2024 年预计保持稳定。 如果你有任何其他问题或需要进一步的信息,请随时告诉我!

问题拆解:

  1. 怎么获取过去 5 年中国的人口
  2. 如果拿到数据之后如何绘制折线图

Multi-Agent

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

  • Agent 拥有太多的工具可供使用,工具越多,调用决策会越糟糕
  • 上下文过于复杂,以至于单个 Agent 无法跟踪和回溯
  • 系统中需要多个专业领域(例如规划者、研究员、数学专家等)。

为解决这些问题,你可能考虑将应用程序拆分成多个更小、独立的代理,并将它们组合成一个多 Agent 系统。这些独立的 Agent 可以简单到一个提示和一个 LLM 调用,或者复杂到像一个 ReactAgent(甚至更多!)。

优势

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

Multi-Agent 结构

Single Agent

一个 Agent 相当于在扮演一个拥有特殊职能的角色,不同的角色会有不同的工具来帮助完成特殊的动作,而 LLM 作为角色的主脑来思考需求和决策我应该使用哪一个工具以及返回最终结果。

Network

每个 Agent 都可与其他 Agent 通信。任何 Agent 都可以决定接下来调用哪个其他 Agent

这种架构中,Agent 被定义为图节点。每个 Agent 都可以与每个其他 Agent 通信(多对多连接),并且可以决定接下来调用哪个 Agent。虽然非常灵活,但随着 Agent 数量的增加,这种架构扩展性并不好:

  • 很难强制执行接下来应该调用哪个 Agent
  • 很难确定应该在 Agent 之间传递多少信息

建议生产避免使用这架构。

Supervisor

每个 Agent 与一个监督者 Agent 通信。监督者 Agent 决定接下来应该调用哪个 Agent。扩展性强,可以更好地控制执行流程,推荐使用。

Supervisor(As Tools)

这是监督者架构的一个特殊情况。个别 Agent 可以被表示为工具。在这种情况下,监督者 Agent 使用一个工具调用 LLM 来决定调用哪个 Agent 工具,以及传递哪些参数给这些 Agent。

Hierarchical(层次)

可以定义一个有监督者的多 Agent 系统,每个系统也可以作为一个 Agent 单位组成一个新的监督者多 Agent 结构系统。

Custom(自定义多 Agent 工作流)

每个 Agent 只与 Agent 子集中的其他 Agent 通信。流程的部分是确定性的,只有一些 Agent 可以决定接下来调用哪个其他 Agent。

常用结构应用

监督者

定义 Agent 为节点,并添加一个监督者节点(LLM),它决定接下来应该调用哪个 Agent 节点。使用条件边根据监督者的决策将执行路由到适当的 Agent 节点。这种架构也适用于并行运行多个 Agent 或使用 map-reduce 模式

python 复制代码
from typing import Literal

from dotenv import load_dotenv, find_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START

load_dotenv(find_dotenv())
model = ChatOpenAI()

class AgentState(MessagesState):
    next: Literal["agent_1", "agent_2"]

def supervisor(state: AgentState):
    response = model.invoke(...)
    return {"next": response["next_agent"]}

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

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

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

builder.add_edge(START, "supervisor")
# 根据监督者的决策路由到Agent之一或退出
builder.add_conditional_edges("supervisor", lambda state: state["next"])
builder.add_edge("agent_1", "supervisor")
builder.add_edge("agent_2", "supervisor")

supervisor = builder.compile()

graph_png = supervisor.get_graph().draw_mermaid_png()
with open("multi_agent_demo.png", "wb") as f:
    f.write(graph_png)

以下是监督者系统的结构图:

监督者(工具调用)

python 复制代码
from audioop import findfactor
from typing import Annotated

from dotenv import load_dotenv, find_dotenv
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import InjectedState, create_react_agent

load_dotenv(find_dotenv())
model = ChatOpenAI()

def agent_1(state: Annotated[dict, InjectedState]):
    """This is the Agent Tool A"""
    tool_message = ...
    return {"messages": [tool_message]}

def agent_2(state: Annotated[dict, InjectedState]):
    """This is the Agent Tool B"""
    tool_message = ...
    return {"messages": [tool_message]}

tools = [agent_1, agent_2]
supervisor = create_react_agent(model, tools)
graph_png = supervisor.get_graph().draw_mermaid_png()
with open("superviser_as_tool.png", "wb") as f:
    f.write(graph_png)

以下是监督者(工具调用)系统的结构图:

任务实战

初始化代理和工具

  1. 这里使用动态构建 Agent 的方法构建:

    python 复制代码
    # 定义Agent创建方法
    def agent_template(llm, tools: list, system_message: str):
        prompt = ChatPromptTemplate.from_messages([
            ("system", "你是一个有帮助的AI助手,与其他助手合作。"
                       " 使用提供的工具来推进问题的回答。"
                       " 如果你不能完全回答,没关系,另一个拥有不同工具的助手"
                       " 会接着你的位置继续帮助。执行你能做的以取得进展。"
                       " 如果你或其他助手有最终答案或交付物,"
                       " 在你的回答前加上FINAL ANSWER,以便团队知道停止。"
                       " 你可以使用以下工具: {tool_names}。\n{system_message}"),
            MessagesPlaceholder(variable_name="messages")
        ])
        # 将系统消息和工具名称传递给prompt
        prompt = prompt.partial(system_message=system_message)
        prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
    
        return prompt | llm.bind_tools(tools)
  2. 定义工具方法:

    python 复制代码
    # 定义工具方法
    # 1. 定义python代码执行器工具
    @tool
    def python_repl(code: Annotated[str, "要执行已生成图标的Python代码。"]):
        """使用这个工具来执行Python代码。如果你想查看某个值的输出,
           应该使用print(...)。这个输出对用户可见。"""
        repl = PythonREPL()
        try:
            result = repl.run(code)
        except Exception as e:
            return f"Error: {e}"
    
        result_str = f"执行成功:\n‍```python\n{code}\n‍```\nStdout:{result}"
        return result_str + "\n\n如果你已完成所有任务,请回复FINAL ANSWER"
    
    
    # 2. 定义搜索工具
    tavily_tool = TavilySearchResults(max_results=5)

状态定义

后期在定义节点和条件边时需要获取状态信息

python 复制代码
# 创建langgraph 状态,用于在图中各节点之间传递
class AgentState(TypedDict):
    # messages 字段用于存储消息序列,,并通过Annotated定义消息的类型和处理方法
    messages: Annotated[Sequence[BaseMessage], operator.add]
    # sender 字段用于标识消息的发送者
    sender: str

定义节点和条件边

  1. 节点定义,这里也使用动态定义的方法:

    python 复制代码
    # 为每个代理创建不同的节点
    def create_node(state: AgentState, agent, name: str):
        result = agent.invoke(state)
        if isinstance(result, ToolMessage):
            pass
        else:
            result = AIMessage(**result.model_dump(exclude={"type", "name"}), name=name)
        return {
            "messages": [result],
            "sender": name
        }
  2. 条件边方法定义:

    python 复制代码
    # 3. 定义路由节点方法
    def route(state: AgentState):
        last_message = state["messages"][-1]
        # 判断是否结束,如果已返回最终答案则返回结束节点
        if "FINAL ANSWER" in last_message.content:
            return "__end__"
        # 如果有工具调用则返回调用工具节点
        elif last_message.tool_calls:
            return "call_tool"
        # 否则返回继续节点,这个节点参考边的path_map定义
        else:
            return "continue"

流程图构建

python 复制代码
def build_graph(llm, agent_info: dict, tools: list):
    # 1. 创建图
    graph_builder = StateGraph(AgentState)
    # 获取节点列表
    llm = llm.bind_tools(tools)

    # 2. 创建节点
    # 2.1 添加Agent节点
    for name, info in agent_info.items():
        agent = agent_template(llm, info["tool"], info["description"])
        node = functools.partial(create_node, agent=agent, name=name)
        graph_builder.add_node(name, node)

    # 2.3 添加工具节点
    graph_builder.add_node("call_tool", ToolNode(tools))

    # 3. 添加边
    # 添加开始节点
    graph_builder.add_edge(START, "tavily_search")
    graph_builder.add_conditional_edges(
        "tavily_search",
        route,
        {
            "call_tool": "call_tool",
            "continue": "chart_generator",
            "__end__": END
        }
    )
    graph_builder.add_conditional_edges(
        "chart_generator",
        route,
        {
            "call_tool": "call_tool",
            "continue": "tavily_search",
            "__end__": END
        }
    )
    graph_builder.add_conditional_edges(
        "call_tool",
        lambda state: state["sender"],
        {
            "chart_generator": "chart_generator",
            "tavily_search": "tavily_search",
        }
    )
    return graph_builder.compile()

最终调用

python 复制代码
if __name__ == '__main__':
    tavily_tool = TavilySearchResults(max_results=5)
    tool_list = [python_repl, tavily_tool]

    agent_info = {
        "chart_generator": {
            "description": "你执行展示的任何图表都将对用户可见。",
            "tool": [python_repl]
        },
        "tavily_search": {
            "description": "你应该提供准确的数据供chart_generator使用。",
            "tool": [tavily_tool]
        }
    }

    llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    graph = build_graph(llm, agent_info, tool_list)

    # 绘制图形化流程
    graph_png = graph.get_graph().draw_mermaid_png()
    with open("multi_agent_demo.png", "wb") as f:
        f.write(graph_png)

    events = graph.stream({
        "messages": [
            ("user", "今年2025年,获取过去5年中国统计人口(亿),然后绘制一条折线图。")
        ]
    }, {"recursion_limit": 150})

    for s in events:
        print(s)
        print("-" * 10)

执行结果:

python 复制代码
...
{'call_tool': {'messages': [ToolMessage(content="执行成功:\n```python\nimport matplotlib.pyplot as plt\n\n# 年份和人口数据\nyears = [2020, 2021, 2022, 2023, 2024]\npopulation = [14.04, 14.09, 14.01, 14.10, 14.11]\n\n# 创建折线图\nplt.figure(figsize=(10, 5))\nplt.plot(years, population, marker='o')\nplt.title('中国人口变化(2020-2024)')\nplt.xlabel('年份')\nplt.ylabel('人口(亿)')\nplt.xticks(years)  # 设置 x 轴刻度\nplt.grid()  # 添加网格\nplt.savefig('china_population_trend.png')  # 保存图形为图片文件\n```\nStdout:\n\n如果你已完成所有任务,请回复FINAL ANSWER", name='python_repl', tool_call_id='call_pbvDN2QOcsiewO9UDaT0JnSN')]}}
----------
{'chart_generator': {'messages': [AIMessage(content='以下是中国从2020年到2024年人口变化的折线图:\n\n![中国人口变化(2020-2024)](attachment://china_population_trend.png)\n\n- 2020年:14.04亿\n- 2021年:14.09亿\n- 2022年:14.01亿\n- 2023年:14.10亿\n- 2024年(预测):14.11亿\n\n这些数据显示了中国总人口在过去5年中的变化趋势。\n\nFINAL ANSWER', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 113, 'prompt_tokens': 50846, 'total_tokens': 50959, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_5154047bf2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-be6c8356-6d6a-4046-b242-006ae9e7bb2e-0', usage_metadata={'input_tokens': 50846, 'output_tokens': 113, 'total_tokens': 50959, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})], 'sender': 'chart_generator'}}
----------

图表生成: