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.
🎯 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.
- 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 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.
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.
- 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
- 2023AutoGen · MicrosoftMulti-agent as free-form conversation between LLM 'characters'. Popularised the space.
- 2023CrewAI · roles + tasksSequential, role-based orchestration. Great mental model for teams new to multi-agent.
- 2024LangGraph · state graphsMulti-agent as a directed graph with typed state, conditional edges, checkpointing. Production-grade.
- 2024Anthropic 'agents' postWarns against reaching for multi-agent when a workflow will do. Frames the trade-off clearly.
- 2025OpenAI Swarm + Agents SDKOpenAI'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
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
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
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)
A concrete example — research + write + review
Uses an LLM call with a small system prompt like 'given the state, pick next: researcher / writer / reviewer / finish'. Returns a single role name.
Researcher has search + fetch. Writer only sees notes + gets no external tools. Reviewer sees the draft + can flag issues.
Note: not free-form 'here's what I did'; explicit artifacts (research_notes, draft, review_comments).
With max_steps (10–15 typical for a 3-agent system) as a hard cap.
"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."
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.
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.
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.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.
- 1Generation 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
- 2So 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
- 3Verification 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
- 4Placing 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
- 5Therefore 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 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.
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.
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.
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?
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
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.
(d) Production reality · 15 min
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.
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.
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.
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.
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.
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.
Where this shows up next
(e) Recall + stretch · 10 min
Explain-out-loud test
- When does multi-agent beat single-agent?
- What are the three orchestration patterns and when to pick each?
- 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.