Search Tech Journey

Find topics, journeys and posts

6-month learning plan121 / 130
back to blog
llmadvanced 50m read

S121 · Multi-Agent Orchestration — LangGraph, CrewAI Patterns

When one LLM agent isn't enough — the three patterns (hierarchical, peer-to-peer, graph) that structure multi-agent systems, how LangGraph and CrewAI implement them, and the coordination failures that eat 2 AM pagers.

LLMsM14 · LLMs & Applications· Session 121 of 130 90 min

🎯 Design a multi-agent system for a real task (researcher + writer + reviewer) using LangGraph or CrewAI, with explicit handoff, state, and stopping.

Why this session exists

Single-agent systems (S120) hit a wall around 8–10 steps or when a task requires clearly-different skills — research, writing, review, code, plan. Multi-agent systems break the problem into roles, give each role its own LLM + tools, and pass state between them. Done well, this beats one giant agent. Done badly, it multiplies every single-agent failure mode by N. This session teaches you the three orchestration patterns, when each pays off, and the coordination hazards nobody warns you about.

You will be able to
  • Explain hierarchical (manager + workers), peer-to-peer (round-robin), and graph (LangGraph-style) orchestration patterns.
  • Design roles + handoffs for a real task and know when to add more agents vs make one better.
  • Build a small multi-agent system in ~120 lines using LangGraph's state-graph model.
  • Diagnose 'infinite handoff', 'agents forget context', and 'work is duplicated' bugs.
  • Compare LangGraph, CrewAI, AutoGen at the mental-model level and pick one for a new project.

Prerequisites

  • S120 · LLM Agents — the single-agent primitives (function calling, ReAct, max_steps).
  • S116 · Prompting — each agent gets its own system prompt shaping its role.


(a) Intuition · 5 min

A small studio publishing a magazine
🌍 Real world

A magazine story needs a researcher (facts + sources), a writer (turns research into prose), and an editor (fact-checks, polishes, sends back for revision). Making one polymath do all three is possible but brittle — the writer gets bored, the editor is too soft on their own draft.

Splitting the roles gives each person a clear system prompt: 'you are the researcher; your job ends when you deliver a source list' vs 'you are the editor; your job is to reject anything under bar and demand revisions'. Coordination costs a few extra meetings; quality goes up.

💻 Code world

Multi-agent orchestration is exactly this. Each agent is an LLM with a specific system prompt, its own toolbox, and a well-defined 'done' condition. A manager (or a graph, or a round-robin) routes work between them. State (research notes, drafts, review comments) is passed via a shared context or explicit messages.

Get it right and you can decompose problems that a single agent can't hold in its head. Get it wrong and you have three agents talking past each other while your cost meter spins.

When multi-agent actually beats single-agent
  • The task decomposes cleanly into different SKILLS (research vs write vs code vs review).
  • You need separation of concerns — a critic that isn't the same LLM as the generator.
  • The context is too big for one agent's window (each agent sees only its slice).
  • You want parallel work streams (three researchers gather facts on different sub-topics simultaneously).
  • You need role-specific tools (only the coder gets code_exec; only the reviewer gets style_lint).

The three orchestration patterns

  1. 2023
    AutoGen · Microsoft
    Multi-agent as free-form conversation between LLM 'characters'. Popularised the space.
  2. 2023
    CrewAI · roles + tasks
    Sequential, role-based orchestration. Great mental model for teams new to multi-agent.
  3. 2024
    LangGraph · state graphs
    Multi-agent as a directed graph with typed state, conditional edges, checkpointing. Production-grade.
  4. 2024
    Anthropic 'agents' post
    Warns against reaching for multi-agent when a workflow will do. Frames the trade-off clearly.
  5. 2025
    OpenAI Swarm + Agents SDK
    OpenAI's lightweight hand-off-oriented multi-agent library. Yet another take on the same primitives.

(b) Visual walkthrough · 15 min

The three canonical patterns

Pattern side-by-side

Hierarchical / Supervisor

One manager delegates + collects

  • Simplest to reason about
  • Manager handles routing; workers stay dumb
  • Great for research + writing pipelines
  • Bottleneck: the supervisor is a single point of failure
Peer-to-peer / round-robin

Agents talk in a group chat

  • AutoGen's default pattern
  • Emergent behaviour — sometimes brilliant, often chaotic
  • Hardest to debug; conversations drift
  • Best for creative / brainstorming tasks
Graph (LangGraph)

Explicit nodes + edges + typed state

  • Deterministic, testable, checkpointable
  • Conditional edges support arbitrary logic
  • Production default at any serious shop
  • Steeper learning curve

The state that flows between agents

What lives in shared state (LangGraph-style)

messages: list[Message]
The running conversation history. Every agent appends its output.
chat
current_task / next_agent
Routing metadata — the supervisor uses this to decide which worker runs next.
route
artifacts: dict[str, Any]
The 'work products' — research notes, draft doc, review comments, final answer. Persist across steps.
artifacts
step_count / max_steps
The runaway-loop guard. Non-negotiable in production.
safety
user_context
The original request, user id, permissions. Read-only through the whole graph.
context

A concrete example — research + write + review

1route
Supervisor decides the next agent

Uses an LLM call with a small system prompt like 'given the state, pick next: researcher / writer / reviewer / finish'. Returns a single role name.

2work
Worker runs with its own system prompt + tools

Researcher has search + fetch. Writer only sees notes + gets no external tools. Reviewer sees the draft + can flag issues.

3state
Worker output appended to shared state

Note: not free-form 'here's what I did'; explicit artifacts (research_notes, draft, review_comments).

4stop
Loop until supervisor picks 'finish'

With max_steps (10–15 typical for a 3-agent system) as a hard cap.


Common misconception
✗ What most people think

"Multiple specialised agents beat one generalist. Splitting a hard task across a researcher, a writer and a critic gives each a focused prompt, so quality goes up."

✓ What is actually true

Every agent boundary is a lossy serialisation: shared state must be compressed into text, sent, and reinterpreted. That costs tokens, latency, and — critically — information that was implicit in the originating agent's context. Multi-agent systems win when subtasks are genuinely independent and parallelisable; they lose when the subtasks need shared context, which is most of the time.

Why the myth is so sticky

Because the org-chart analogy is intuitive and because specialisation genuinely helps prompts. The confusion is between specialised prompts and separate agents: you can get the former with a single agent using different instructions per phase, at zero communication cost. The failure is also hard to attribute — when a multi-agent system produces a mediocre answer, it looks like a prompt problem in one of the agents rather than a structural loss at the boundaries.

Prove it to yourself

Build the single-agent baseline first and make the comparison explicit:

# Baseline : one agent, phased instructions, shared context throughout
# Candidate: N agents, same phases, message-passing between them
#
# Measure on the SAME eval set:
#   quality        (task-specific rubric)
#   total tokens   (all agents summed -- expect a large multiple)
#   p95 latency    (sequential handoffs add up)
#   failure modes  (where did information get lost?)
#
# Multi-agent must beat the baseline on quality by enough to pay
# for the token and latency multiple. Frequently it does not.
From first principles
Start with the question

Why do multi-agent debate and critic patterns improve output at all? The critic has the same weights and the same knowledge as the generator.

  1. 1
    Generation is autoregressive and committed: once a token is emitted it stays in the context, and everything after is conditioned on it. There is no backtracking.
    forced by · the sampling process has no undo; a wrong early commitment shapes the entire continuation
  2. 2
    So the model has a strong consistency pressure toward its own prior output — continuing a flawed argument is more likely than contradicting it mid-stream.
    forced by · coherent continuation is exactly what next-token training optimises for
  3. 3
    Verification is a different task from generation, and often an easier one: checking whether a proof step is valid, or whether a claim is supported by a cited passage, requires less search than producing it.
    forced by · the asymmetry between finding a solution and checking one is the same asymmetry that makes NP interesting
  4. 4
    Placing the verification in a fresh context removes the consistency pressure: the critic sees the output as an object to evaluate, not as its own utterance to continue.
    forced by · the commitment lives in the context, so a new context has no commitment
  5. 5
    Therefore the gain comes from the context reset and the task switch — not from having a second model, and not from "collaboration".
    forced by · nothing in the argument required different weights, only different conditioning
⇒ Therefore

Therefore critic patterns work because generation and verification are different problems with different difficulty, and separate contexts break self-consistency bias.

And note what this predicts: the gain should be far larger when the critic has access to something the generator did not — a test suite, a compiler, a retrieval tool, ground-truth data. Self-critique with no external signal gives modest and sometimes negative returns, because the critic shares every blind spot in the weights. So the design rule is: a critic without an external verifier is weak; a critic wired to a real oracle is the single most valuable component in an agent system.

Mental modelDistributed systems, but the messages are prose

Every classic distributed-systems problem reappears here, degraded: partial failure (an agent returns nonsense and no exception is raised), consistency (two agents holding divergent beliefs about shared state), deadlock (agents waiting on each other), and cost amplification (one user request fanning out to dozens of model calls).

The difference is that your RPC payload is natural language, so there is no schema, no type checking, and no way to detect a corrupted message. Design accordingly: fewer nodes, narrower interfaces, explicit state.

  • Prefer a supervisor/worker hierarchy over a free-for-all. Peer-to-peer agent chatter has no termination guarantee and no clear failure attribution.
  • Make the handoff a schema, not a paragraph. Structured messages between agents remove the interpretation step that loses information.
  • Keep shared state outside the conversation — a scratchpad, a database, a file — so agents read current state rather than reconstructing it from message history.
  • Every system needs a global step budget and a termination condition. "The supervisor decides when it's done" is not a termination condition.
🔔 Fires when you see

Fire this model the moment you see: agents passing long prose summaries to each other · a system with no global step cap · two agents disagreeing about a fact with no arbiter · token spend growing faster than task complexity · a proposed org chart of agents drawn before a single-agent baseline exists.

The tradeoff

A complex task is not being handled well by one agent. Add specialised agents, add tools to the single agent, or decompose into a deterministic pipeline of single-agent calls?

Multi-agent with a supervisor
+ you gain each worker gets a small, clean context focused on its subtask, which reduces distraction and lets you use different models per role (cheap for extraction, strong for synthesis); genuinely independent subtasks can run in parallel, cutting wall-clock latency
− you pay token cost multiplies with the number of agents and rounds; every handoff loses implicit context; debugging requires tracing across agents; and the supervisor itself becomes a reliability bottleneck
pick when subtasks are genuinely independent and parallelisable — e.g. researching five separate entities — and the results only need combining at the end
One agent, more tools
+ you gain full shared context throughout, so nothing is lost in translation; one trace to debug; lowest token overhead; and adding a tool is a much smaller change than adding an agent
− you pay tool descriptions consume context and, past roughly a few dozen tools, selection accuracy degrades; and one long trajectory accumulates its own context clutter
pick when the subtasks share state or need each other's intermediate results — the common case
Deterministic pipeline of single-purpose LLM calls
+ you gain each call is independently testable and cacheable, control flow is code so it is observable and reliable, and cost per run is predictable to the token
− you pay the sequence is fixed, so genuinely variable workflows do not fit; and you write and maintain the orchestration yourself
pick when the stages are known and stable, which is far more often than teams assume
What a senior engineer actually does

Build the single-agent baseline and measure it before drawing any org chart. Most multi-agent designs are answering a prompt-engineering problem with an architecture, and they pay for it in tokens, latency and debuggability every single run.

The one structure that reliably earns its cost is generator plus externally-grounded verifier — a critic wired to a compiler, a test suite, or retrieval. That is not really multi-agent; it is a feedback loop, and feedback loops are the thing that actually makes agent systems reliable.


(c) Hands-on · 25 min

Build a supervisor + researcher + writer + reviewer using LangGraph. This is a runnable skeleton you can drop into a real project.

"""multi_agent.py — supervisor + researcher + writer + reviewer via LangGraph.
 
Requires: pip install langgraph langchain-openai
"""
from __future__ import annotations
import os
from typing import Annotated, Literal, TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
    HumanMessage, AIMessage, SystemMessage, BaseMessage,
)
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
 
MODEL_MAIN   = "gpt-4o-mini"      # cheap for supervisor + workers
MODEL_REV    = "gpt-4o-mini"      # separate LLM ideally, same here for cost
MAX_STEPS    = 12
 
 
# ---------- Shared state ----------
class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]  # running convo
    next_agent: str                                        # supervisor's routing decision
    research_notes: str
    draft: str
    review: str
    step: int
 
 
# ---------- Prompts per role ----------
def sys(role: str) -> str:
    return {
        "supervisor": (
            "You are the SUPERVISOR of a small writing team.\n"
            "Team: researcher, writer, reviewer.\n"
            "Read the state and pick the SINGLE next role. Respond with ONE word: "
            "researcher | writer | reviewer | FINISH.\n"
            "- If research_notes empty: pick researcher.\n"
            "- If draft empty: pick writer.\n"
            "- If review empty: pick reviewer.\n"
            "- If review says APPROVED: pick FINISH.\n"
            "- If review says REVISE: pick writer."
        ),
        "researcher": (
            "You are the RESEARCHER. Given the user's request, produce 3–5 "
            "bullet points of factual claims + sources. Keep it under 200 words. "
            "Return ONLY the bullet points."
        ),
        "writer": (
            "You are the WRITER. Given the research notes and (optionally) a prior "
            "review, produce a polished 200-word summary. Return ONLY the summary text."
        ),
        "reviewer": (
            "You are the REVIEWER. Read the draft. If it accurately reflects the "
            "research and is under 250 words, respond with exactly 'APPROVED'. "
            "Otherwise respond with 'REVISE: <2-sentence critique>'."
        ),
    }[role]
 
 
llm_main = ChatOpenAI(model=MODEL_MAIN, temperature=0)
llm_rev  = ChatOpenAI(model=MODEL_REV,  temperature=0)
 
 
# ---------- Node implementations ----------
def supervisor(state: State) -> State:
    prompt = (
        f"USER REQUEST: {state['messages'][0].content}\n"
        f"research_notes: {state['research_notes'][:200] or '(empty)'}\n"
        f"draft: {state['draft'][:200] or '(empty)'}\n"
        f"review: {state['review'][:200] or '(empty)'}\n"
    )
    r = llm_main.invoke([SystemMessage(sys("supervisor")),
                         HumanMessage(prompt)])
    choice = r.content.strip().split()[0].lower()
    print(f"[step {state['step']}] SUPERVISOR → {choice}")
    return {"next_agent": choice, "step": state["step"] + 1}
 
 
def researcher(state: State) -> State:
    r = llm_main.invoke([SystemMessage(sys("researcher")),
                         HumanMessage(state['messages'][0].content)])
    print(f"[step {state['step']}] RESEARCHER produced {len(r.content)} chars")
    return {"research_notes": r.content}
 
 
def writer(state: State) -> State:
    prompt = (f"RESEARCH NOTES:\n{state['research_notes']}\n\n"
              f"PRIOR REVIEW (if any): {state['review']}\n\n"
              f"Write the summary now.")
    r = llm_main.invoke([SystemMessage(sys("writer")), HumanMessage(prompt)])
    print(f"[step {state['step']}] WRITER produced {len(r.content)} chars")
    return {"draft": r.content, "review": ""}       # clear review so we re-review
 
 
def reviewer(state: State) -> State:
    prompt = (f"RESEARCH NOTES:\n{state['research_notes']}\n\n"
              f"DRAFT:\n{state['draft']}")
    r = llm_rev.invoke([SystemMessage(sys("reviewer")), HumanMessage(prompt)])
    print(f"[step {state['step']}] REVIEWER → {r.content[:80]}...")
    return {"review": r.content}
 
 
# ---------- Routing ----------
def route(state: State) -> Literal["researcher","writer","reviewer","finish"]:
    if state["step"] >= MAX_STEPS:
        print("!! MAX_STEPS hit — forcing FINISH")
        return "finish"
    return {
        "researcher": "researcher",
        "writer":     "writer",
        "reviewer":   "reviewer",
        "finish":     "finish",
    }.get(state["next_agent"], "finish")
 
 
# ---------- Build the graph ----------
g = StateGraph(State)
g.add_node("supervisor", supervisor)
g.add_node("researcher", researcher)
g.add_node("writer",     writer)
g.add_node("reviewer",   reviewer)
 
g.add_edge(START, "supervisor")
g.add_conditional_edges("supervisor", route, {
    "researcher": "researcher",
    "writer":     "writer",
    "reviewer":   "reviewer",
    "finish":     END,
})
# After each worker, hand back to the supervisor.
for worker in ("researcher", "writer", "reviewer"):
    g.add_edge(worker, "supervisor")
 
app = g.compile()
 
 
# ---------- Run ----------
if __name__ == "__main__":
    initial = {
        "messages": [HumanMessage("Write a 200-word summary of how HNSW graph indexes work.")],
        "next_agent": "",
        "research_notes": "", "draft": "", "review": "", "step": 0,
    }
    final = app.invoke(initial)
    print("\n===== FINAL DRAFT =====\n")
    print(final["draft"])

Anatomy of the graph

What each part does

TypedDict State
The shared object every node reads/writes. LangGraph merges dicts returned by nodes into the running state. Explicit fields (research_notes, draft, review) make the pipeline testable.
state
supervisor(state) node
An LLM call that inspects state and returns ONE word — the next agent's name. This is the routing brain. Keep its prompt tiny and deterministic.
router
researcher / writer / reviewer nodes
Each is a small LLM call with its own system prompt. They only read what they need and write to specific state fields.
workers
add_conditional_edges + route()
The declarative way to say 'after supervisor, go to whichever agent it named'. Route() also enforces max_steps.
edges
return {"review": ""} in writer node
When the writer writes a new draft (after 'REVISE'), we clear the old review so the reviewer must re-review. Explicit state resets prevent stale-artefact bugs.
reset
Try itForce a runaway loop — then fix it

Try:

# In State: add revision_count: int
# In writer: revision_count += 1
# In route(): if state.get("revision_count", 0) >= 3: return "finish"

Now the graph exits with a partial draft after 3 revisions. Production graphs always cap sub-loops, not just total steps.

💡 Hint · Change the reviewer's prompt to 'Always respond with REVISE'. Watch the writer + reviewer loop until MAX_STEPS. That's the exact 'infinite handoff' failure production teams hit. Fix by adding a revision counter to State that caps writer↔reviewer at 3 loops.

(d) Production reality · 15 min

War story Common multi-agent failure — the polite loopevery AutoGen POC that hits production
🔥 What broke

A three-agent AutoGen crew (planner + coder + tester) goes into a loop: coder produces code, tester finds a bug, coder fixes it, tester finds a different bug, coder fixes... 45 minutes and 300 messages later, no resolution.

🧯 The fix

Add explicit revision limits AND require the tester to eventually accept ('good enough') even with minor issues. Track a 'progress delta' — if two consecutive iterations reduce the issue list by 0, force finish.

Anthropic's advice: peer-to-peer patterns are the highest-variance multi-agent design. Prefer supervised / graph patterns in production.

🎓 Lesson to steal
Multi-agent loops don't self-terminate. Every sub-loop (writer↔reviewer, coder↔tester) needs its own cap. 'max total steps' alone is insufficient because you burn all your budget on one stuck sub-loop.
War story LangChain / LangGraph teamsmost 2024 agent frameworks
🔥 What broke

Early multi-agent frameworks (AutoGen, CrewAI 0.1) treated coordination as free-form conversation. Debugging meant reading 40-message transcripts. Reproducing bugs was near-impossible because agent order was non-deterministic.

🧯 The fix

LangGraph reframes multi-agent as a state graph: nodes are functions, edges are typed transitions, state is a typed dict. Now you can serialise state, checkpoint mid-run, replay from any point, and unit-test individual nodes.

Result: every serious 2025 multi-agent codebase converged on graph semantics. AutoGen v0.4, OpenAI Agents SDK, and CrewAI Flows all adopted graph-like APIs.

🎓 Lesson to steal
If your multi-agent framework doesn't have typed state + explicit routing + checkpointing, you'll burn weeks debugging things you can't reproduce. This is why LangGraph won.
War story Anthropic engineering — the workflow-first thesis· 2024internal guidance published
🔥 What broke

Teams reach for multi-agent as 'obviously better than single-agent'. Most tasks that seem multi-agent are actually multi-step workflows where each step could be a simple LLM call in a pipeline.

🧯 The fix

Anthropic's recommendation: 'workflow' (fixed step sequence with LLM calls) > 'agent' (dynamic planning) > 'multi-agent' (multiple dynamic planners). Escalate one level at a time and only when the previous can't handle it.

Start every project with a workflow. Add an agent for one step if planning is truly dynamic. Add a second agent only when a genuinely different SKILL (research vs code vs review) demands it.

🎓 Lesson to steal
Multi-agent is the last resort, not the first choice. It multiplies latency, cost, and failure modes. Use it when roles are genuinely different — not just for the 'cool' factor.
Post-mortem

Where this shows up next

Multi-agent systems touch every downstream LLM concern
S122 · LLM Evaluation
Multi-agent evals need trajectory metrics — role adherence, handoff correctness, artefact quality per agent.
S124 · LLM Serving
N agents = N× tokens; serving math looks nothing like a single-agent chatbot.
S127 · Streaming Analytics
Long-running crews look like distributed jobs; checkpointing + resumability become critical.
S128 · Cost & Sustainability
Cost per query in multi-agent can be 5–20× single-agent. Understanding the loop is understanding the bill.
S129 · Safety & Alignment
Multiple agents = multiple opportunities to act in the world. Blast radius grows with N.
S130 · Model Selection
Multi-agent is a good place to mix models — cheap workers, strong supervisor, specialised critic.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

  1. When does multi-agent beat single-agent?
  2. What are the three orchestration patterns and when to pick each?
  3. What are the two failure modes specific to multi-agent systems?

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.