Skip to content

Multi-Agent Systems

Multi-agent systems decompose complex tasks across specialized agents — a researcher, a coder, a writer, a reviewer. Each agent does what it's best at, and a coordinator orchestrates the handoffs. This covers architectures, coordination patterns, and communication strategies.

Table of Contents

Installation / Setup

pip install langgraph langchain-openai

Core Syntax

Multi-agent architectures

Architecture How it works Best for
Supervisor Central coordinator assigns tasks Clear task delegation
Workers Specialized agents handle sub-tasks Parallel sub-tasks
Handoffs Agents pass work to each other Sequential workflows
Debate Agents argue different perspectives Balanced analysis
Hierarchical Managers manage workers Large-scale systems

Supervisor pattern

graph TD
    A[User] --> B[Supervisor]
    B --> C[Researcher]
    B --> D[Coder]
    B --> E[Writer]
    C --> B
    D --> B
    E --> B
    B --> F[Final Output]
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

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

# specialized agents
researcher = create_react_agent(llm, [search_tool], state_modifier="You research topics.")
coder = create_react_agent(llm, [run_python], state_modifier="You write Python code.")
writer = create_react_agent(llm, [], state_modifier="You write clear reports.")

# supervisor decides who to call
supervisor_prompt = """You are a supervisor. Route to the right worker:
- researcher: for information lookup
- coder: for code writing
- writer: for report writing

Current task: {task}"""

def supervisor(task):
    response = llm.invoke(supervisor_prompt.format(task=task))
    route = response.content.strip().lower()
    if "research" in route:
        return researcher.invoke({"messages": [("human", task)]})
    elif "code" in route:
        return coder.invoke({"messages": [("human", task)]})
    else:
        return writer.invoke({"messages": [("human", task)]})

LangGraph multi-agent

from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from operator import add

class State(TypedDict):
    messages: Annotated[list, add]
    next: str

def supervisor_node(state: State):
    response = llm.invoke([
        {"role": "system", "content": "Route to researcher, coder, or FINISH."},
        *state["messages"],
    ])
    return {"next": response.content.strip()}

def researcher_node(state: State):
    result = researcher.invoke(state)
    return {"messages": [result["messages"][-1]]}

def coder_node(state: State):
    result = coder.invoke(state)
    return {"messages": [result["messages"][-1]]}

graph = StateGraph(State)
graph.add_node("supervisor", supervisor_node)
graph.add_node("researcher", researcher_node)
graph.add_node("coder", coder_node)

graph.add_edge(START, "supervisor")
graph.add_conditional_edges("supervisor", lambda s: s["next"], {
    "researcher": "researcher",
    "coder": "coder",
    "FINISH": END,
})
graph.add_edge("researcher", "supervisor")
graph.add_edge("coder", "supervisor")

Handoff pattern

def handoff(from_agent, to_agent, task):
    """Transfer work from one agent to another."""
    result = from_agent.invoke(task)
    # extract relevant context
    context = result["messages"][-1].content
    # hand off
    return to_agent.invoke({"messages": [("human", f"Continue from previous agent:\n{context}")]})

Common Operations

Agent communication via shared state

# agents communicate through shared state
class SharedState:
    def __init__(self):
        self.data = {}

    def set(self, key, value):
        self.data[key] = value

    def get(self, key, default=None):
        return self.data.get(key, default)

# researcher sets data
state.set("findings", "Key insight: ...")

# coder uses data
findings = state.get("findings")
code = coder.invoke(f"Write code based on: {findings}")

Parallel agent execution

from concurrent.futures import ThreadPoolExecutor

def parallel_agents(task, agents):
    """Run multiple agents in parallel on different sub-tasks."""
    sub_tasks = decompose(task)
    with ThreadPoolExecutor(max_workers=len(agents)) as pool:
        results = list(pool.map(
            lambda a: a.invoke({"messages": [("human", a.task)]}),
            agents
        ))
    return results

Debate pattern

def debate(topic, num_rounds=3):
    agent_a = create_react_agent(llm, [], state_modifier="You argue FOR the topic.")
    agent_b = create_react_agent(llm, [], state_modifier="You argue AGAINST the topic.")

    history = []
    for round in range(num_rounds):
        a_response = agent_a.invoke({"messages": [
            ("human", f"Argue for: {topic}\n\nPrevious: {history}")]})
        history.append(("for", a_response["messages"][-1].content))

        b_response = agent_b.invoke({"messages": [
            ("human", f"Argue against: {topic}\n\nPrevious: {history}")]})
        history.append(("against", b_response["messages"][-1].content))

    # summarize
    return llm.invoke(f"Summarize this debate:\n{history}").content

Practical Examples

Example 1 — Software development team

def dev_team(feature_request):
    # architect designs
    architecture = architect.invoke(f"Design architecture for: {feature_request}")

    # coder implements
    code = coder.invoke(f"Implement this design:\n{architecture}")

    # reviewer checks
    review = reviewer.invoke(f"Review this code:\n{code}")

    # if issues found, coder fixes
    if "issue" in review.lower():
        code = coder.invoke(f"Fix issues:\n{review}\n\nCode:\n{code}")

    return code

Example 2 — Research pipeline

def research_team(topic):
    # parallel research
    with ThreadPoolExecutor(max_workers=3) as pool:
        sources = list(pool.map(
            lambda agent: agent.invoke({"messages": [("human", f"Research {topic} from {agent.source}")]})["messages"][-1].content,
            [academic_agent, news_agent, code_agent]
        ))

    # synthesize
    synthesis = synthesizer.invoke(f"Synthesize these sources:\n{sources}")
    return synthesis["messages"][-1].content

Common Patterns

Pattern: supervisor with handoff

def supervised_workflow(task):
    # supervisor assigns
    assignment = supervisor.invoke(f"Who should handle: {task}")
    agent_name = assignment.content.strip()

    # execute
    agent = agent_registry[agent_name]
    result = agent.invoke({"messages": [("human", task)]})

    # check if handoff needed
    if needs_handoff(result):
        next_agent = supervisor.invoke(f"Hand off from {agent_name}: {result}")
        result = agent_registry[next_agent.content.strip()].invoke(
            {"messages": [("human", f"Continue: {result}")]})

    return result

Pattern: agent registry

class AgentRegistry:
    def __init__(self):
        self.agents = {}

    def register(self, name, agent, description):
        self.agents[name] = {"agent": agent, "description": description}

    def get_descriptions(self):
        return "\n".join(f"- {k}: {v['description']}" for k, v in self.agents.items())

    def get(self, name):
        return self.agents[name]["agent"]

Pattern: message passing

def message_pass(from_agent, to_agent, message):
    """Structured message passing between agents."""
    result = from_agent.invoke({"messages": [("human", message)]})
    response = result["messages"][-1].content
    return to_agent.invoke({"messages": [("human", f"Message from {from_agent.name}: {response}")]})

Best Practices

  • Start with one agent — add multi-agent only when needed.
  • Supervisor pattern is the simplest and most common.
  • Specialize agents — each agent should have a clear, narrow role.
  • Use shared state for communication — not message passing.
  • Limit agent count — 2–4 agents is usually enough.
  • Add error handling — agents can fail; have fallbacks.
  • Test each agent independently before combining.
  • Log agent interactions — multi-agent systems are hard to debug.

Common Mistakes

Mistake Fix
Too many agents Start with 2-3; add only when needed
Agents with overlapping roles Make each agent's role distinct
No supervisor Add a coordinator to prevent chaos
No error handling Add try/except and fallback agents
Ignoring communication overhead Minimize agent-to-agent messages

Troubleshooting

Symptom Cause / Fix
Agents loop forever Add max iterations; check supervisor logic
Wrong agent handles task Improve supervisor classification
Agents don't collaborate Use shared state, not isolated contexts
System is too slow Reduce agents; run in parallel where possible

Official Documentation

Last Updated

2026-09-06 — Initial version.