This is the fifth and final post in our LangGraph fundamentals series, building on everything from why graphs through stateful agents, conditional edges, and human-in-the-loop.
A single agent handling research, writing, and fact-checking within one set of instructions tends to be mediocre at all three simultaneously — the same reason a single generalist employee handling every function in a growing company eventually gets replaced by specialists. A multi-agent system applies that same specialization instinct: separate agents, each with a narrower, more focused responsibility, coordinated through shared graph state. Because coordinating multiple autonomous agents introduces real design questions beyond anything covered so far in this series, this closing post goes deeper than the others.
Defining specialized agents as distinct nodes
from typing import TypedDict
class ContentState(TypedDict):
topic: str
research_notes: str
draft: str
fact_check_notes: str
final_content: str
def researcher_agent(state: ContentState) -> ContentState:
result = model.invoke(
f"Research key facts and points about: {state['topic']}. List them concisely."
)
state["research_notes"] = result.content
return state
def writer_agent(state: ContentState) -> ContentState:
result = model.invoke(
f"Write a short article about {state['topic']} using these notes:\n{state['research_notes']}"
)
state["draft"] = result.content
return state
def fact_checker_agent(state: ContentState) -> ContentState:
result = model.invoke(
f"Check this draft against the original research notes for factual consistency. "
f"List any discrepancies.\n\nNotes: {state['research_notes']}\n\nDraft: {state['draft']}"
)
state["fact_check_notes"] = result.content
return stateEach function here is deliberately narrow — the researcher only researches, the writer only writes from provided notes, the fact-checker only compares a draft against those same notes. Each can use a different system prompt, different tools, even a different underlying model entirely (a smaller, cheaper model for fact-checking, the more capable one for writing — the same task-to-model matching principle covered in our OpenAI series) — genuinely independent design decisions per agent, rather than one prompt trying to balance every concern at once.
Coordinating them through shared state
graph = StateGraph(ContentState)
graph.add_node("research", researcher_agent)
graph.add_node("write", writer_agent)
graph.add_node("fact_check", fact_checker_agent)
graph.set_entry_point("research")
graph.add_edge("research", "write")
graph.add_edge("write", "fact_check")
graph.add_edge("fact_check", END)
app = graph.compile()
result = app.invoke({"topic": "Kubernetes autoscaling", "research_notes": "", "draft": "", "fact_check_notes": "", "final_content": ""})This is the exact same coordination mechanism covered throughout this series — nodes, edges, shared state — applied to a different unit of work per node: instead of one step being "call this specific tool," each step here is an entire specialized agent, potentially with its own internal loop or tool access. The graph structure doesn't distinguish between "a simple function" and "an entire agent" as a node — both are just a function taking state and returning updated state, which is precisely what makes this pattern compose cleanly with everything covered earlier in this series, including looping a specific agent (via a conditional edge back to it) if its output doesn't meet a quality bar.
A supervisor pattern: one agent deciding which specialist to use
def supervisor(state: ContentState) -> str:
if not state["research_notes"]:
return "research"
if not state["draft"]:
return "write"
if not state["fact_check_notes"]:
return "fact_check"
return "end"
graph.add_conditional_edges(
"supervisor_node",
supervisor,
{"research": "research", "write": "write", "fact_check": "fact_check", "end": END},
)Rather than a fixed sequence, a supervisor agent (or, as shown here, a simpler routing function) can decide dynamically which specialist to invoke next, based on what's already present in shared state — useful when the right sequence genuinely isn't fixed in advance, the same underlying justification for a LangChain agent over a fixed chain, applied here at the level of choosing between entire specialized sub-agents rather than individual tools.
Letting the fact-checker trigger a rewrite loop
A supervisor pattern composes naturally with the retry-loop concept from earlier in this series — a fact-checker finding real discrepancies can route back to the writer rather than simply reporting a problem no one acts on:
def supervisor(state: ContentState) -> str:
if not state["research_notes"]:
return "research"
if not state["draft"]:
return "write"
if not state["fact_check_notes"]:
return "fact_check"
if "discrepancy" in state["fact_check_notes"].lower() and state.get("revisions", 0) < 2:
return "write" # loop back to the writer with fact-check notes as feedback
return "end"
def writer_agent(state: ContentState) -> ContentState:
feedback = f"\n\nAddress this feedback: {state['fact_check_notes']}" if state.get("fact_check_notes") else ""
result = model.invoke(
f"Write a short article about {state['topic']} using these notes:\n{state['research_notes']}{feedback}"
)
state["draft"] = result.content
state["revisions"] = state.get("revisions", 0) + 1
return stateThis is the exact same "bound your loop before relying on it converging" principle from the stateful-agent post earlier in this series — revisions < 2 caps how many times the writer can be sent back for revision, the multi-agent equivalent of max_iterations on a LangChain agent or the attempts cap on the retry-loop pattern.
Isolating each agent's context
A subtlety worth naming directly: passing an agent's entire accumulated state as its prompt context, rather than only what it actually needs, quietly reintroduces a version of the unbounded-context-growth cost problem covered in our OpenAI series — a fact-checker genuinely only needs the research notes and the draft, not every field ever written by any other agent in the graph. Constructing each agent's prompt deliberately from just its relevant state fields, rather than dumping the whole state object into every prompt, keeps each agent's context focused and its token cost proportional to what it actually needs, rather than growing with the total size of the graph's accumulated state.
Evaluating a multi-agent system as a whole, and per agent
The evaluation practices from our LangChain series (a real dataset, known-good expected outputs, comparing runs) apply here at two distinct levels that are worth measuring separately:
- End-to-end quality: given a topic, is the final content genuinely good — accurate, well-written, appropriately scoped?
- Per-agent quality: is the researcher actually surfacing the most relevant facts? Is the fact-checker catching real discrepancies, or missing them, or flagging false positives that trigger unnecessary rewrite loops?
A multi-agent system with a poor final result can be failing at any one of its specialized steps, and — exactly as with the retrieval-versus-generation distinction covered in our OpenAI RAG post — the fix differs meaningfully depending on which agent is actually underperforming. Evaluating each specialist's output independently, not just the final combined result, is what tells you which specific agent's prompt or model choice actually needs attention.
Why this beats one large, generalist prompt
- Each agent's prompt stays focused and genuinely testable in isolation — you can evaluate the fact-checker's accuracy independently of the writer's prose quality, rather than trying to isolate one concern's contribution to one enormous, do-everything prompt's overall output.
- Different agents can reasonably use different models entirely — a genuinely significant cost lever, covered in our OpenAI series, that's much harder to apply granularly to a single monolithic prompt handling every concern at once.
- Debugging isolates to a specific agent's node in a trace (the observability practices covered in our LangChain series apply directly here) — a wrong fact in final output traces to the fact-checker's specific step, not to an undifferentiated single prompt where every concern is tangled together.
What to actually remember from this series
- Separate agents with narrower, focused responsibilities often outperform one generalist agent trying to balance every concern in a single prompt.
- A multi-agent graph uses the exact same nodes/edges/state mechanism as everything earlier in this series — an agent is just a node whose function happens to be more sophisticated internally.
- A supervisor pattern lets a graph dynamically choose which specialist to invoke next, based on shared state, rather than following one fixed sequence, and can trigger a bounded rewrite loop when a downstream agent finds real problems.
- Give each agent only the state it actually needs, not the entire accumulated object — the same cost discipline as bounding conversation history, applied to inter-agent context.
- Evaluate per-agent quality, not just the end-to-end result — a poor final output can trace back to any one specialist, and the fix differs depending on which.
- Specialization enables independent model choice, independent evaluation, and much clearer debugging per agent — advantages that are genuinely difficult to achieve with one monolithic prompt handling everything at once.
That closes out our LangGraph fundamentals series — from graphs vs. chains through stateful agents, conditional control flow, human-in-the-loop, and now multi-agent coordination. If your team is building genuinely complex, production-grade agentic systems, this is exactly the kind of hands-on work we build our AI & Generative AI training around.
