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.
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.
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.
- 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.
