Supervisor Triage and Loop-Back Workers: A Minimal LangGraph Multi-Agent Pattern
Lesson 3: Multi-Agent — Supervisor Triage + Dedicated Workers
This series explains LangGraph in practice using runnable minimal demos. This is Lesson 3: splitting a single Agent into a "supervisor scheduling hub" + multiple "dedicated workers", so each specialist only does what it is good at. The accompanying demo is in the same directory:
ticket_triage.py.
TL;DR: The core of multi-agent is "triage + specialization" — a supervisor reads the request, decides which specialists are needed (possibly several), and dispatches round by round (loop-back); each worker is an independently compiled subgraph, and after processing, control returns to the supervisor to continue scheduling; aggregation happens only when the queue is empty. The benefit is that each specialist's prompt, tools, and context are isolated from one another.
Outline (§1–§8)
§1 Why Multi-Agent Is Needed
The problem with a single large agent is "attention dilution" — the longer the context, the more the model's attention scatters across key information; the multi-agent answer is "divide and conquer" — each specialist only looks at its own patch. The following breaks it down from three angles.
Pain points of a single agent. An agent with one "all-purpose big prompt" hits three bottlenecks at once: first, context bloat — as the execution flow gets longer, the model starts to forget, hallucinate, drift from instructions, or even overflow the context window; forcibly compressing context then dilutes or even drops key instructions. Second, responsibility conflict — it has to carry multiple responsibilities and call multiple tools, and token consumption spikes while repeatedly exploring the tool space. Third, difficult debugging — context explosion, responsibility conflict, and tool-call confusion blend together, making it extremely hard to locate a problem when it arises.
Benefits of splitting. After splitting into supervisor + dedicated workers, the supervisor handles task decomposition and assignment, each worker specializes in one function and only calls the tools relevant to it, and contexts are mutually isolated — the context bloat and responsibility confusion of a single agent disappear; debugging also becomes simpler: you can construct test cases for each specialist independently and tune them separately.
Cost of splitting (honest boundaries). Multi-agent is not a free lunch: agents pass data through state, so state consistency must be guaranteed, which creates coordination overhead; splitting responsibilities makes the execution chain longer, and total token usage can grow several times over; the supervisor's decomposition and assignment also introduces the risk of routing errors; context passed between agents is subject to length and storage limits, so information is also lost in transit.
§2 Supervisor: The Triage Hub
The supervisor is the "scheduling hub" — it does not touch the concrete ticket logic itself; it only handles four things: decomposition and routing (deciding which dedicated worker to call), providing input (handing the ticket context to the dispatched worker), merging results (aggregating each specialist's output), and maintaining global memory (remembering the progress of the whole session). Its boundary with workers is a single line: whether it handles concrete ticket logic — if it touches that, it is worker business, not supervisor business.
Here we need to distinguish a pair of easily confused concepts: supervisor vs router. A supervisor is a full agent that maintains global context and dynamically decides which sub-agent to call over multiple rounds; a router only does one-shot classification and forwards the request to the corresponding agent, without maintaining persistent state. In one sentence: supervisor is architecture-level task allocation, router is node-level routing. In our demo, classify + pending + loop-back is a supervisor — because loop-back, a "non-one-shot routing", is exactly the signature of a supervisor.
The supervisor's decision basis is clear in route():
def route(state: State) -> str:
"""Conditional routing: if pending is non-empty, dispatch to the front specialist; if empty, go to aggregate."""
return state["pending"][0] if state["pending"] else "FINISH"
It reads the front of the pending queue to decide "who to dispatch next"; when pending is empty, it returns the termination signal FINISH, ending scheduling and moving to aggregation.
§3 Dedicated Workers: Subgraphs Are First-Class Citizens
Why compile each worker as a "subgraph" rather than writing an ordinary node function? Because a subgraph is a mini-graph with its own internal steps — it can be compiled independently, reused independently, and its internal structure is encapsulated from the outside; an ordinary node function has only one step, which cannot hold a specialist's complete workflow (in the demo, each worker's internals are two steps: analyze → done). This is the foundation of LangGraph multi-agent: subgraphs are first-class citizens.
There are two kinds of state relationships between subgraph and main graph: when the schema is the same, the subgraph reads and writes the same fields as the main graph, so you can directly attach it with add_node("worker", worker_subgraph) (which is the case in the demo); when the schema differs, you need to write a wrapper function that maps the main graph state into the subgraph's input and maps the subgraph's output back to the main graph.
So what "first-class citizen" concretely means is: a subgraph can be compiled independently, can be mounted as a node into another graph, and can be nested and reused — this lets each specialist be developed, tested, and versioned independently, and then composed by the supervisor.
graph LR
subgraph main[Main graph supervisor]
CL[classify] --> RW[refund_worker]
RW --> AG[aggregate]
end
subgraph sub[refund_worker subgraph internals]
AN[analyze] --> DN[done]
end
RW -.->|expand| AN
§4 Assembly: Mounting Subgraphs into the Main Graph
The pieces from the previous sections are all here — the supervisor's classify, three dedicated worker subgraphs, and aggregate. This section assembles them into a complete main graph. build_graph() does three things: mount nodes (add_node puts classify, the three worker subgraphs, and aggregate into the main graph), connect conditional routing (add_conditional_edges lets classify dispatch by triage key), and connect loop-back (all three workers return to classify when done).
def build_graph():
g = StateGraph(State)
g.add_node("classify", classify)
g.add_node("refund_worker", refund_worker) # the three specialists are "subgraphs"
g.add_node("tech_worker", tech_worker)
g.add_node("billing_worker", billing_worker)
g.add_node("aggregate", aggregate)
g.add_edge(START, "classify")
g.add_conditional_edges(
"classify",
route,
{"refund": "refund_worker", "tech": "tech_worker", "billing": "billing_worker", "FINISH": "aggregate"},
)
# loop-back: after each specialist finishes, return to classify for the next round of scheduling
g.add_edge("refund_worker", "classify")
g.add_edge("tech_worker", "classify")
g.add_edge("billing_worker", "classify")
g.add_edge("aggregate", END)
return g.compile(checkpointer=MemorySaver())
Note the last three add_edge("xxx_worker", "classify") — that is the whole secret of loop-back. Without them, the graph is a one-shot router: classify → worker → end; with them, after a worker finishes, control returns to classify, and the supervisor reads pending again to decide "continue dispatching the next specialist" or "go to aggregate".
The whole graph looks like this:
graph TD
ST([START]) --> CL[classify supervisor]
CL -->|refund| RW[refund_worker]
CL -->|tech| TW[tech_worker]
CL -->|billing| BW[billing_worker]
CL -->|FINISH| AG[aggregate]
RW --> CL
TW --> CL
BW --> CL
AG --> EN([END])
From START enter → classify triages → dispatches round by round to the three workers according to pending (the three back-edges below are loop-back, returning to classify for another dispatch) → when pending is empty, take the FINISH branch → aggregate → END.
§5 Conditional Routing: Dispatching to the Right Specialist
After the supervisor triages, how does it "dispatch" a ticket to the right worker? By using conditional routing: the route function reads the front of pending and returns a "triage key", and the path map then translates that key into the actual node name.
def route(state: State) -> str:
"""Conditional routing: if pending is non-empty, dispatch to the front specialist; if empty, go to aggregate."""
return state["pending"][0] if state["pending"] else "FINISH"
route returns a triage key (refund / tech / billing), not a node name. The real "key → node" translation happens in the third argument (path map) of add_conditional_edges:
g.add_conditional_edges(
"classify",
route,
{
"refund": "refund_worker", # triage key → node name
"tech": "tech_worker",
"billing": "billing_worker",
"FINISH": "aggregate", # when queue is empty, go straight to aggregate
},
)
There is a very easy pitfall here: the return value of route must match the keys of the path map; otherwise the runtime throws a KeyError (cannot find the corresponding node). While writing this demo I stepped on it once — initially I made route return the node name "refund_worker" directly, while the path map keys were "refund"; the two did not match, and invoke immediately raised KeyError: 'refund_worker'. After changing it to "route returns keys, path map translates", it worked. This is also why the path map exists: to decouple the "decision result" from the "actual node" — later, if you want to change routing, you only touch the path map, not route.
§6 Aggregation + Loop-Back: Pull Back and Keep Scheduling
After a ticket is "dispatched" to a worker, the worker subgraph processes it while maintaining the pending queue:
def _make_worker(name: str, action: str):
"""Build a two-step dedicated worker subgraph."""
def analyze(state: State) -> dict:
# Step 1: process and append the result to handled
return {"handled": state.get("handled", []) + [f"{name}:{action}"]}
def done(state: State) -> dict:
# Step 2: dequeue — remove itself from pending, triggering the next round of scheduling
return {"pending": state["pending"][1:]}
analyze uses handled + [...] to accumulate results, and done uses pending[1:] to dequeue (slicing off the front element, which is itself). Combined, the worker has "done its job and removed itself from the pending list".
Once pending is cleared by each worker dequeuing itself, route returns FINISH, the flow reaches aggregate, and handled is joined into the final report:
def aggregate(state: State) -> dict:
return {"report": " | ".join(state["handled"])}
Finally, the loop-back closure — after each worker runs, it relies on an edge back to the supervisor to continue scheduling the next round:
g.add_edge("refund_worker", "classify")
g.add_edge("tech_worker", "classify")
g.add_edge("billing_worker", "classify")
These three back-edges are the concrete implementation of "loops back to the supervisor" from futureagi Pattern 2.
§7 A Few Confusing Points (Quick Reference)
The pitfalls along the way have been pointed out earlier; here they are collected for convenience:
- Subgraph state incompatible with main graph — when the schema differs, you must write a wrapper function for input/output mapping; you cannot directly
add_node(see §3). - Path map keys must match
routereturn values — whenroutereturns"refund_worker"but the path map key is"refund", the runtime raisesKeyError(see §5; the demo stepped on this at the beginning).
One more point not covered earlier:
- Loops must terminate. The supervisor loops by pushing and popping from the
pendingqueue; if any worker forgets to "dequeue" (does not executepending[1:]), the queue never empties, and the graph loops until it hits therecursion_limit(default about 10,000 steps) and raises aGraphRecursionError. So when writing loop-back, first confirm that "each round of the loop advances the state and eventually reaches the termination condition".
§8 Production Extensions + Summary
Production extensions. This demo uses the core StateGraph + subgraphs in order to explain the mechanism clearly and without depending on an LLM (clone it and it runs). In production, there are two directions to upgrade:
- Switch to the higher-level API: the official
langgraph-supervisorpackage (create_supervisor) has been deprecated; the current recommendation iscreate_agent+ wrapping each worker as a@tool"sub-agent" pattern —create_agentrequires an LLM model, and the main agent calls sub-agents like calling tools. The core subgraph mechanism is unchanged; the higher-level wrapper just saves effort. - Make workers truly specialized: the demo's workers share a single
Stateand simulate with rules; in production, each worker has its own prompt / tools / state, each calling its own LLM and permissions (for example, only the refund specialist holds the refund tool's permission).
Summary: In one sentence — multi-agent uses "supervisor + dedicated workers" to split one big agent into a scheduling hub and several specialists: the supervisor triages and dispatches round by round (loop-back), workers each handle their own area with isolated contexts, and finally aggregation happens.
Two points often probed in interviews:
- "Why use multi-agent instead of one big prompt?" — a big prompt leads to context bloat, responsibility conflicts, and hard debugging; splitting lets each specialist have focused context and be independently testable (§1).
- "How does the supervisor choose a worker?" — it reads the front of the
pendingqueue;routereturns a triage key, and the path map translates it into a node name; when the queue is empty it returnsFINISHto close out (§2/§5).
References
- LangGraph official docs: Subagents — https://docs.langchain.com/oss/python/langchain/multi-agent/subagents
- LangGraph official docs: Migrate from langgraph-supervisor — https://docs.langchain.com/oss/python/migrate/langgraph-supervisor
- futureagi: What is LangGraph? Stateful Agent Graphs Explained in 2026 — https://futureagi.com/blog/what-is-langgraph-2026/
- CallSphere: Building a Multi-Agent System with LangGraph: Supervisor and Worker Patterns — https://callsphere.ai/blog/langgraph-multi-agent-system-supervisor-worker-patterns
- Tencent Cloud: Lesson 10 — Agent Architecture: LangGraph / Multi-Agent — https://cloud.tencent.cn/developer/article/2764676
- Academic: AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation — https://arxiv.org/abs/2308.08155
- LangGraph official docs: use-subgraphs — https://docs.langchain.com/oss/python/langgraph/use-subgraphs
Full code: ticket_triage.py
Top 2 from juejin.cn, machine-translated. The original thread is authoritative.
Personally, I don't think there are any shortcuts, because mastering any technology or any field of knowledge should be gradual and step by step. When mastering a technology (like the agent you mentioned), I think there are two points: first, grasp the framework first and then fill in the details; second, try to learn while advancing in the project. Hope this helps or inspires you.
Big shot, may I ask how to quickly get started with agents while still grasping the key points?