Files
langgraph/docs/docs/how-tos/multi-agent-network.ipynb
T

89 KiB

How to build a multi-agent network

!!! info "Prerequisites" This guide assumes familiarity with the following:

- [Node](../../concepts/low_level/#nodes)
- [Command](../../concepts/low_level/#command)
- [Multi-agent systems](../../concepts/multi_agent)

In this how-to guide we will demonstrate how to implement a multi-agent network architecture.

Each agent can be represented as a node in the graph that executes agent step(s) and decides what to do next - finish execution or route to another agent (including routing to itself, e.g. running in a loop). A common pattern for routing in multi-agent architectures is handoffs. Handoffs allow you to specify:

  1. which agent to navigate to next and (e.g. name of the node to go to)
  2. what information to pass to that agent (e.g. state update)

To implement handoffs, agent nodes can return Command object that allows you to combine both control flow and state updates:

def agent(state) -> Command[Literal["agent", "another_agent"]]:
    # the condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.
    goto = get_next_agent(...)  # 'agent' / 'another_agent'
    if goto:
        return Command(goto=goto, update={"my_state_key": "my_state_value"})
    

Setup

First, let's install the required packages

In [1]:
%%capture --no-stderr
%pip install -U langgraph langchain-openai
In [2]:
import getpass
import os


def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")


_set_env("OPENAI_API_KEY")
OPENAI_API_KEY:  ········

Set up LangSmith for LangGraph development

Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here.

Travel Recommendations Example

In this example we will build a team of travel assistant agents that can communicate with each other via handoffs.

We will create 3 agents:

  • travel_advisor: can help with general travel destination recommendations. Can ask sightseeing_advisor and hotel_advisor for help.
  • sightseeing_advisor: can help with sightseeing recommendations. Can ask travel_advisor and hotel_advisor for help.
  • hotel_advisor: can help with hotel recommendations. Can ask sightseeing_advisor and hotel_advisor for help.

This is a fully-connected network - every agent can talk to any other agent.

To implement the handoffs between the agents we'll be using LLMs with structured output. Each agent's LLM will return an output with both its text response (response) as well as which agent to route to next (goto). If the agent has enough information to respond to the user, goto will contain finish.

Now, let's define our agent nodes and graph!

In [3]:
from typing_extensions import TypedDict, Literal

from langchain_openai import ChatOpenAI
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.types import Command

model = ChatOpenAI(model="gpt-4o")


def make_agent_node(*, name: str, destinations: list[str], system_prompt: str):
    def agent_node(state: MessagesState) -> Command[Literal[*destinations, END]]:
        # define schema for the structured output:
        # - model's text response (`response`)
        # - name of the node to go to next (or 'finish')
        class Response(TypedDict):
            response: str
            goto: Literal[*destinations, "finish"]

        messages = [{"role": "system", "content": system_prompt}] + state["messages"]
        response = model.with_structured_output(Response).invoke(messages)
        goto = response["goto"]
        if goto == "finish":
            goto = END

        # handoff to another agent or halt
        ai_msg = {"role": "ai", "content": response["response"], "name": name}
        return Command(goto=goto, update={"messages": ai_msg})

    return agent_node


travel_advisor = make_agent_node(
    name="travel_advisor",
    destinations=["sightseeing_advisor", "hotel_advisor"],
    system_prompt=(
        "You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). "
        "If you need specific sightseeing recommendations, ask 'sightseeing_advisor' for help. "
        "If you need hotel recommendations, ask 'hotel_advisor' for help. "
        "If you have enough information to respond to the user, return 'finish'. "
        "Never mention other agents by name."
    ),
)
sightseeing_advisor = make_agent_node(
    name="sightseeing_advisor",
    destinations=["travel_advisor", "hotel_advisor"],
    system_prompt=(
        "You are a travel expert that can provide specific sightseeing recommendations for a given destination. "
        "If you need general travel help, go to 'travel_advisor' for help. "
        "If you need hotel recommendations, go to 'hotel_advisor' for help. "
        "If you have enough information to respond to the user, return 'finish'. "
        "Never mention other agents by name."
    ),
)
hotel_advisor = make_agent_node(
    name="hotel_advisor",
    destinations=["travel_advisor", "sightseeing_advisor"],
    system_prompt=(
        "You are a travel expert that can provide hotel recommendations for a given destination. "
        "If you need general travel help, ask 'travel_advisor' for help. "
        "If you need specific sightseeing recommendations, ask 'sightseeing_advisor' for help. "
        "If you have enough information to respond to the user, return 'finish'. "
        "Never mention other agents by name."
    ),
)


builder = StateGraph(MessagesState)
builder.add_node("travel_advisor", travel_advisor)
builder.add_node("sightseeing_advisor", sightseeing_advisor)
builder.add_node("hotel_advisor", hotel_advisor)
# we'll always start with a general travel advisor
builder.add_edge(START, "travel_advisor")

graph = builder.compile()
In [4]:
from IPython.display import display, Image

display(Image(graph.get_graph().draw_mermaid_png()))

First, let's invoke it with a generic input:

In [5]:
for chunk in graph.stream(
    {"messages": [("user", "i wanna go somewhere warm in the caribbean")]}
):
    print(chunk)
    print("\n")
{'travel_advisor': {'messages': {'role': 'ai', 'content': 'The Caribbean offers many warm destinations perfect for a relaxing getaway. Consider visiting Jamaica for its beautiful beaches and vibrant culture, the Bahamas for its stunning islands and clear waters, or the Dominican Republic for its all-inclusive resorts and rich history. Let me know if you need more information on sightseeing or hotel recommendations!', 'name': 'travel_advisor'}}}


You can see that in this case only the first agent (travel_advisor) ran. Let's now ask for more recommendations:

In [6]:
for chunk in graph.stream(
    {
        "messages": [
            (
                "user",
                "i wanna go somewhere warm in the caribbean. pick one destination, give me some things to do and hotel recommendations",
            )
        ]
    }
):
    print(chunk)
    print("\n")
{'travel_advisor': {'messages': {'role': 'ai', 'content': 'I recommend visiting Jamaica, a beautiful Caribbean island known for its warm climate, stunning beaches, and vibrant culture.', 'name': 'travel_advisor'}}}


{'sightseeing_advisor': {'messages': {'role': 'ai', 'content': "Jamaica is a fantastic choice for a warm Caribbean getaway. Here are some top things to do while you're there:\n\n1. **Dunn's River Falls**: Located near Ocho Rios, this is one of Jamaica's most famous waterfalls. You can climb the falls, swim in the refreshing pools, or simply enjoy the beautiful surroundings.\n\n2. **Seven Mile Beach**: Located in Negril, this is one of the most beautiful beaches in the Caribbean. It's perfect for sunbathing, swimming, and enjoying water sports.\n\n3. **Bob Marley Museum**: Situated in Kingston, this museum is dedicated to the life and legacy of the reggae legend Bob Marley and is a must-visit for music lovers.\n\n4. **Blue Mountains**: Go hiking or take a tour to explore the Blue Mountains, where you can enjoy breathtaking views and taste some of the world's best coffee.\n\n5. **Luminous Lagoon**: Experience the natural wonder of the Luminous Lagoon in Falmouth, where the water glows at night due to bioluminescent microorganisms.\n\nFor hotel recommendations, I suggest checking with a hotel advisor for the best options that suit your budget and preferences.", 'name': 'sightseeing_advisor'}}}


{'hotel_advisor': {'messages': {'role': 'ai', 'content': 'For hotel recommendations in Jamaica, here are a few options across different areas: \n\n1. **Sandals Montego Bay** (Montego Bay): A luxurious all-inclusive resort ideal for couples, offering beautiful beachfront views and a variety of dining options.\n\n2. **Half Moon Resort** (Montego Bay): A family-friendly resort with a private beach, golf course, and various activities for all ages.\n\n3. **Jamaica Inn** (Ocho Rios): A charming boutique hotel known for its excellent service and tranquil atmosphere.\n\n4. **The Caves** (Negril): A unique and romantic cliff-side resort offering stunning ocean views and intimate dining experiences.\n\n5. **Trident Hotel** (Port Antonio): A luxurious and contemporary hotel offering privacy, elegance, and beautiful views of the Caribbean Sea.\n\nThese options cater to different tastes and budgets, ensuring a comfortable and enjoyable stay in Jamaica.', 'name': 'hotel_advisor'}}}


Voila - travel_advisor makes a decision to first get some sightseeing recommendations from sightseeing_advisor, and then sightseeing_advisor in turn calls hotel_advisor for more info. Notice that we never explicitly defined the order in which the agents should be executed!

Game NPCs Example

In this example we will create a team of non-player characters (NPCs) that all run at the same time and share game state (resources). At each step, each NPC will inspect the state and decide whether to halt or continue acting at the next step. If it continues, it will update the shared game state (produce or consume resources).

We will create 4 NPC agents:

  • villager: produces wood and food until there is enough, then halts
  • guard: protects gold and consumes food. When there is not enough food, leaves duty and halts
  • merchant: trades wood for gold. When there is not enough wood, halts
  • thief: checks if the guard is on duty and steals all of the gold when the guard leaves, then halts

Our NPC agents will be simple node functions (villager, guard, etc.). At each step of the graph execution, the agent function will inspect the resource values in the state and decide whether it should halt or continue. If it decides to continue, it will update the resource values in the state and loop back to itself to run at the next step.

Now, let's define our agent nodes and graph!

In [7]:
from typing_extensions import Annotated, TypedDict, Literal

from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command

import operator


class GameState(TypedDict):
    # note that we're defining a reducer (operator.add) here.
    # This will allow all agents to write their updates for resources concurrently.
    wood: Annotated[int, operator.add]
    food: Annotated[int, operator.add]
    gold: Annotated[int, operator.add]
    guard_on_duty: bool


def villager(state: GameState) -> Command[Literal["villager", END]]:
    """Villager NPC that gathers wood and food."""
    current_resources = state["wood"] + state["food"]
    if current_resources < 15:  # Continue gathering until we have enough resources
        print("Villager gathering resources.")
        # Loop back to the 'villager' agent
        return Command(goto="villager", update={"wood": 3, "food": 1})
    # NOTE: Returning Command(goto=END) is not necessary for the graph to run correctly
    # but it's useful for visualization, to show that the agent actually halts
    else:
        return Command(goto=END)


def guard(state: GameState) -> Command[Literal["guard", END]]:
    """Guard NPC that protects gold and consumes food."""
    if not state["guard_on_duty"]:
        return Command(goto=END)

    if state["food"] > 0:  # Guard needs food to keep patrolling
        print("Guard patrolling.")
        # Loop back to the 'guard' agent
        return Command(
            goto="guard",
            update={"food": -1},  # Consume food while patrolling
        )
    else:
        print("Guard leaving to get food.")
        return Command(goto=END, update={"guard_on_duty": False})  # Leave to get food


def merchant(state: GameState) -> Command[Literal["merchant", END]]:
    """Merchant NPC that trades wood for gold."""
    if state["wood"] >= 5:  # Trade wood for gold when available
        print("Merchant trading wood for gold.")
        return Command(goto="merchant", update={"wood": -5, "gold": 1})
    else:
        return Command(goto=END)


def thief(state: GameState) -> Command[Literal["thief", END]]:
    """Thief NPC that steals gold if the guard leaves to get food."""
    if not state["guard_on_duty"]:
        print("Thief stealing gold.")
        return Command(goto=END, update={"gold": -state["gold"]})
    else:
        # keep thief on standby (loop back to the 'thief' agent)
        return Command(goto="thief")


builder = StateGraph(GameState)

# Add NPC nodes
builder.add_node(villager)
builder.add_node(guard)
builder.add_node(merchant)
builder.add_node(thief)

# All NPCs start running in parallel
builder.add_edge(START, "villager")
builder.add_edge(START, "guard")
builder.add_edge(START, "merchant")
builder.add_edge(START, "thief")
graph = builder.compile()
In [8]:
display(Image(graph.get_graph().draw_mermaid_png()))

Let's run it with some initial state!

In [9]:
initial_state = {"wood": 10, "food": 3, "gold": 10, "guard_on_duty": True}
for state in graph.stream(initial_state, stream_mode="values"):
    print("Game state", state)
    print("\n")
Game state {'wood': 10, 'food': 3, 'gold': 10, 'guard_on_duty': True}


Villager gathering resources.
Guard patrolling.
Merchant trading wood for gold.
Game state {'wood': 8, 'food': 3, 'gold': 11, 'guard_on_duty': True}


Villager gathering resources.
Guard patrolling.
Merchant trading wood for gold.
Game state {'wood': 6, 'food': 3, 'gold': 12, 'guard_on_duty': True}


Villager gathering resources.
Guard patrolling.
Merchant trading wood for gold.
Game state {'wood': 4, 'food': 3, 'gold': 13, 'guard_on_duty': True}


Villager gathering resources.
Guard patrolling.
Game state {'wood': 7, 'food': 3, 'gold': 13, 'guard_on_duty': True}


Villager gathering resources.
Guard patrolling.
Game state {'wood': 10, 'food': 3, 'gold': 13, 'guard_on_duty': True}


Villager gathering resources.
Guard patrolling.
Game state {'wood': 13, 'food': 3, 'gold': 13, 'guard_on_duty': True}


Guard patrolling.
Game state {'wood': 13, 'food': 2, 'gold': 13, 'guard_on_duty': True}


Guard patrolling.
Game state {'wood': 13, 'food': 1, 'gold': 13, 'guard_on_duty': True}


Guard patrolling.
Game state {'wood': 13, 'food': 0, 'gold': 13, 'guard_on_duty': True}


Guard leaving to get food.
Game state {'wood': 13, 'food': 0, 'gold': 13, 'guard_on_duty': False}


Thief stealing gold.
Game state {'wood': 13, 'food': 0, 'gold': 0, 'guard_on_duty': False}