Multi-agent and subgraphs
How to compose graphs, and — more usefully in an interview — when not to.
Start with the warning
Five agents at 90% reliability each is about 59% end to end. Multi-agent multiplies unreliability, cost and latency, and most tasks that look like they need several agents need one agent with several tools.
Reach for multi-agent for three reasons only:
| Reason | Example |
|---|---|
| Context isolation | a reader that would fill the window |
| Genuine parallelism | ten independent documents |
| Privilege separation | one agent may write, others read |
If none applies, you want tools, not agents. See Multi-agent patterns.
Subgraphs: a graph as a node
sub = sub_builder.compile()
parent = StateGraph(ParentState)
parent.add_node("research", sub)A compiled graph is callable like any node, so composition is free. Two ways the state can line up:
Shared keys. If parent and subgraph state share key names, they flow through automatically. Simple, and it couples the two schemas.
Transformed. Wrap the subgraph in a function that maps parent state in and subgraph output back out:
def research(state: ParentState) -> dict:
out = sub.invoke({"query": state["question"]})
return {"findings": out["summary"]}Prefer the wrapper. It keeps the subgraph’s schema independent, which means you can test and reuse it without knowing about the parent — the same argument as not sharing a database between services.
The supervisor pattern
One coordinator routes to specialists and decides when the work is done.
┌──────────────┐
│ supervisor │
└──┬───┬────┬──┘
│ │ │
research writer reviewer
│ │ │
└──────┴────┘
back to supervisorEach specialist returns to the supervisor, which routes again or finishes.
The supervisor’s routing is a conditional edge or a Command — and it is
worth asking whether it needs a model at all. A supervisor that routes on a
field in state is deterministic, testable and free.
Route = Literal["research", "writer", "done"]
def supervise(state: State) -> Command[Route]:
if not state["findings"]:
return Command(goto="research")
if not state["draft"]:
return Command(goto="writer")
return Command(goto="done")That is a supervisor with no model in it. Many production “multi-agent systems” are exactly this, and saying so is a stronger answer than describing a free-running crew.
Handoff versus call
Two different shapes, and the distinction matters:
| Call | Handoff | |
|---|---|---|
| Control | returns to caller | transfers away |
| Shape | a function | a goto |
| Fits | a sub-task | a change of role |
Calling a subgraph is a function call — do the research, come back.
Handing off transfers the conversation, as when a triage agent passes to a
billing agent that now owns the thread. Command(goto=...) expresses the
second; a subgraph node expresses the first.
Most systems want calls. Handoff is right when the receiving agent should own the rest of the interaction, which is mainly a customer-support shape.
Context isolation, concretely
The strongest technical reason to split. A sub-agent reading forty documents would fill the parent’s window with material nobody needs afterwards. Running it as a subgraph means the parent sees only the summary that comes back.
def deep_research(state: ParentState) -> dict:
# The subgraph reads 40 docs in its own state.
out = researcher.invoke({"topic": state["topic"]})
# The parent only ever sees the summary.
return {"brief": out["summary"]}This is a context engineering decision expressed as an architecture — see Context engineering.
Gotcha: a sub-agent does not see the parent’s conversation. Whatever it needs must be in the input you pass it. A brief that assumes shared context produces a confidently wrong result, and the symptom looks like a model failure rather than a plumbing one.
Debugging composition
Two things make a composed system inspectable, and both are worth naming:
- Stream with
subgraphs=Trueso child steps appear with their namespace (Streaming). - Give every agent a
name, so traces attribute work correctly. An unnamed sub-agent shows up as an anonymous span and you lose the ability to ask which agent burned the tokens.
Cost attribution per agent is the thing people wish they had after the first month — see Cost attribution.
Related
Interview angle 5
- “When do you use multiple agents?” - for context isolation, genuine parallelism, or privilege separation. Otherwise one agent with several tools, because five agents at 90% each is about 59% end to end.
- “How do you compose graphs?” - a compiled graph is callable as a node. Either share state keys, or wrap it in a function mapping parent state in and results out — prefer the wrapper, since it keeps the subgraph’s schema independent and testable.
- “What is the supervisor pattern?” - a coordinator that routes to specialists and decides when work is done. Worth asking whether it needs a model: a supervisor routing on a state field is deterministic, testable and free, and that is what many production systems actually are.
- “Call or handoff?” - a call returns to the caller and is a function; a handoff transfers ownership of the thread with
Command(goto=...). Most systems want calls; handoff fits a change of role, like triage passing to billing. - “What breaks in a multi-agent system?” - the sub-agent cannot see the parent’s conversation, so an incomplete brief produces a confidently wrong answer that looks like a model failure. And unnamed agents make traces and cost attribution useless.