Agent Foundry
LangGraph

Multi-Agent Patterns

AdvancedTopic 19 of 22Open in Colab

Multi-Agent Patterns

Multi-agent systems use multiple LLM-powered agents that collaborate to solve problems. LangGraph provides the primitives — subgraphs, conditional edges, Send, and Command — to implement various multi-agent architectures. Each pattern has different trade-offs around autonomy, control, and complexity.

Five Multi-Agent Patterns

PatternDescriptionAgent AutonomyWhen to Use
SubagentsParent agent invokes child agents as toolsLow — parent controls flowWell-defined subtasks, parent needs to orchestrate
HandoffsAgent transfers control to another agent via tool callMedium — agents decide when to hand offSpecialized domains, conversation routing
SkillsAgents share tools and state, selected by routerLow — router picks the right skillTool-centric workflows, similar agent capabilities
RouterCentral router dispatches to specialist agents in parallelLow — router controls dispatchIndependent subtasks, parallel execution
Custom WorkflowHardcoded graph topology with agents at nodesNone — flow is predeterminedPredictable pipelines, compliance requirements

Subagents Pattern

Each agent is a compiled subgraph invoked as a tool by the parent agent:

from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END, add_messages
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
 
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
# Child agent as a tool
@tool
def research_agent(query: str) -> str:
    """Research a topic and return findings."""
    response = llm.invoke([
        ("system", "You are a research assistant. Provide concise findings."),
        ("human", query),
    ])
    return response.content
 
@tool
def math_agent(expression: str) -> str:
    """Evaluate a math expression."""
    response = llm.invoke([
        ("system", "You are a math expert. Solve the expression step by step."),
        ("human", expression),
    ])
    return response.content
 
tools = [research_agent, math_agent]
llm_with_tools = llm.bind_tools(tools)
 
def parent_agent(state: AgentState) -> dict:
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

Handoff Pattern

Agents transfer control to each other using tool calls. The receiving agent picks up the conversation:

from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END, add_messages
from langgraph.prebuilt import create_react_agent
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
class State(TypedDict):
    messages: Annotated[list, add_messages]
 
# Each agent can hand off to another via a tool
def make_handoff_tool(target_agent: str):
    @tool
    def handoff() -> Command:
        f"""Transfer to {target_agent} agent."""
        return Command(goto=target_agent)
    handoff.__name__ = f"transfer_to_{target_agent}"
    handoff.__doc__ = f"Transfer to {target_agent} agent."
    return handoff
 
# Build specialized agents
sales_agent = create_react_agent(
    llm,
    tools=[make_handoff_tool("support")],
    prompt="You are a sales agent. Hand off to support for technical issues.",
)
 
support_agent = create_react_agent(
    llm,
    tools=[make_handoff_tool("sales")],
    prompt="You are a support agent. Hand off to sales for pricing questions.",
)
 
# Router decides the initial agent
def router(state: State) -> Command:
    response = llm.invoke([
        ("system", "Route to 'sales' for pricing/purchase or 'support' for technical help."),
        *state["messages"],
    ])
    destination = "sales" if "sales" in response.content.lower() else "support"
    return Command(goto=destination)
 
graph = StateGraph(State)
graph.add_node("router", router)
graph.add_node("sales", sales_agent)
graph.add_node("support", support_agent)
graph.add_edge(START, "router")

The Command(goto=...) primitive lets agents dynamically route control flow without conditional edges.

Router with Parallel Execution

A central router dispatches independent subtasks to specialist agents simultaneously:

import operator
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
class RouterState(TypedDict):
    query: str
    results: Annotated[list[str], operator.add]
 
class AgentInput(TypedDict):
    query: str
    agent_type: str
 
def router(state: RouterState) -> dict:
    return {}
 
def dispatch(state: RouterState) -> list[Send]:
    return [
        Send("specialist", {"query": state["query"], "agent_type": "researcher"}),
        Send("specialist", {"query": state["query"], "agent_type": "analyst"}),
        Send("specialist", {"query": state["query"], "agent_type": "writer"}),
    ]
 
def specialist(state: AgentInput) -> dict:
    response = llm.invoke([
        ("system", f"You are a {state['agent_type']}. Respond to the query."),
        ("human", state["query"]),
    ])
    return {"results": [f"[{state['agent_type']}]: {response.content}"]}
 
graph = StateGraph(RouterState)
graph.add_node("router", router)
graph.add_node("specialist", specialist)
graph.add_conditional_edges("router", dispatch, path_map=["specialist"])
graph.add_edge(START, "router")
graph.add_edge("specialist", END)
 
app = graph.compile()

Custom Workflow Pattern

For predictable pipelines, hardcode the graph topology with agents at each node:

from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END, add_messages
from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
class PipelineState(TypedDict):
    messages: Annotated[list, add_messages]
    draft: str
    review: str
    final: str
 
def drafter(state: PipelineState) -> dict:
    response = llm.invoke([
        ("system", "Write a first draft based on the request."),
        *state["messages"],
    ])
    return {"draft": response.content}
 
def reviewer(state: PipelineState) -> dict:
    response = llm.invoke([
        ("system", "Review this draft and suggest improvements."),
        ("human", state["draft"]),
    ])
    return {"review": response.content}
 
def editor(state: PipelineState) -> dict:
    response = llm.invoke([
        ("system", "Produce the final version incorporating the review feedback."),
        ("human", f"Draft:\n{state['draft']}\n\nReview:\n{state['review']}"),
    ])
    return {"final": response.content}
 
graph = StateGraph(PipelineState)
graph.add_node("drafter", drafter)
graph.add_node("reviewer", reviewer)
graph.add_node("editor", editor)
graph.add_edge(START, "drafter")
graph.add_edge("drafter", "reviewer")
graph.add_edge("reviewer", "editor")
graph.add_edge("editor", END)
 
app = graph.compile()

Choosing the Right Pattern

ConsiderationBest Pattern
Need central control over subtasksSubagents or Router
Agents need to decide who handles whatHandoffs
Tasks are independent and parallelizableRouter with Send
Flow must be predictable and auditableCustom Workflow
Agents have distinct specializationsHandoffs or Subagents
Combining tools from different domainsSkills

Key Takeaways

  • LangGraph supports five multi-agent patterns: Subagents, Handoffs, Skills, Router, and Custom Workflow
  • Subagents give the parent agent full control, invoking child agents as tools
  • Handoffs let agents transfer control dynamically using Command(goto=...)
  • Router patterns use Send to dispatch to specialists in parallel
  • Custom Workflows hardcode the agent topology for predictable, auditable pipelines
  • Choose based on how much autonomy agents need and whether tasks are independent