Comprehensive LangGraph skill covering: - Core architecture: Graph API, Functional API, state management, agent loops - Three multi-agent patterns: supervisor (~94% accuracy), swarm (~40% fewer LLM calls), hierarchical teams (subgraphs with nested state) - Persistence: checkpointers vs stores, per-invocation/per-thread/stateless modes - Production: Agent Server deployment, LangSmith observability, 8 failure modes - Evals: routing accuracy, resolution coverage, LLM-as-judge methodology - Troubleshooting: symptom→cause→fix tables per pattern - 3 Python scripts: supervisor scaffold, swarm scaffold, eval generator - 3 runnable templates: supervisor, swarm, subgraph composition Ships 8 reference files, 3 scripts, and 3 templates.
6.7 KiB
Architecture — LangGraph Core Concepts
This reference covers LangGraph's foundational architecture. Read this first if you're new to LangGraph or building a system from scratch.
Overview
LangGraph is a low-level orchestration framework that models agent workflows as directed graphs. It is inspired by Google's Pregel and Apache Beam, with a public interface drawing from NetworkX. Unlike linear chain frameworks (LangChain Expression Language, traditional pipelines), LangGraph supports cycles — essential for agent tool-calling loops, iterative refinement, and multi-agent coordination.
User → [Agent Node] → {tool call?} → [Tool Node] → [Agent Node] → ... → Response
↘ (no tool) → Response
Core Abstractions
State
The graph's shared, persistent data. All nodes read from and write to the same state object. Defined as a TypedDict or Pydantic schema.
Key pattern — reducer annotations: Fields that multiple nodes write to in parallel need a reducer. The operator.add reducer is the most common — it concatenates lists:
from typing import Annotated, TypedDict
import operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add] # parallel-safe append
next_agent: str # single writer only
resolution_notes: Annotated[list[str], operator.add]
Rule of thumb: If multiple nodes can write to the same key, it needs a reducer. If only one node ever writes (e.g., a shared topic field), no reducer needed.
Nodes
Python functions (or runnable objects) that receive state and return state updates:
def my_node(state: AgentState) -> dict:
# Read from state, do work, return updates
return {"messages": [AIMessage(content="hello")]}
Nodes can be:
- LLM calls — invoke a model, return output
- Tool nodes — execute tool calls from an LLM
- Python functions — deterministic logic, transformations, validation
- Subgraphs — nested LangGraph graphs (see multi-agent-hierarchical reference)
- Agents — LangChain agents wrapped as a single node
Edges
Edges define how execution flows between nodes:
add_edge(start, end)— unconditionally traverse from start to endadd_conditional_edges(source, router, path_map)— dynamic routing based on state
# Standard edge
builder.add_edge("node_a", "node_b")
# Conditional edge
builder.add_conditional_edges(
"analyzer",
route_based_on_state, # function that returns a node name
{"billing": "billing_node", "tech": "tech_node", END: END}
)
Two APIs
Graph API (StateGraph)
Full control. You explicitly add nodes and wire edges with method calls.
from langgraph.graph import StateGraph, START, END, MessagesState
builder = StateGraph(MessagesState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode([search, calculator]))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")
graph = builder.compile()
Functional API (@task + @entrypoint)
Simpler decorator-based approach for linear or tree-shaped workflows:
from langgraph.func import entrypoint, task
@task
def research(topic: str) -> str:
return llm.invoke(f"Research {topic}").content
@task
def write_report(research: str) -> str:
return llm.invoke(f"Write report: {research}").content
@entrypoint()
def workflow(topic: str):
r = research(topic).result()
return write_report(r).result()
When to use each:
- Graph API — complex routing, cycles, multi-agent, subgraphs, conditional flows
- Functional API — sequential chains, parallel fan-out patterns, simpler orchestration
The Agent Loop (Tool-Calling)
The most common LangGraph pattern is the agent tool-calling loop:
1. LLM node receives messages
2. LLM returns response (may contain tool_calls)
3. Conditional edge: if tool_calls → ToolNode; if no tool_calls → END
4. ToolNode executes tools, returns ToolMessages
5. Edge back to LLM node
6. Repeat from 1
from langgraph.prebuilt import ToolNode, tools_condition
builder = StateGraph(MessagesState)
builder.add_node("llm", llm_call)
builder.add_node("tools", ToolNode([search, calculator]))
builder.add_conditional_edges("llm", tools_condition, {"tools": "tools", END: END})
builder.add_edge("tools", "llm")
tools_condition is a prebuilt router: returns "tools" if the last message has tool_calls, otherwise returns END.
Streaming
LangGraph supports multiple streaming modes:
# Stream all events (most detailed)
for event in graph.stream_events(inputs, version="v3"):
if event["method"] == "updates":
print(event["params"]["data"])
# Stream values only (final state per superstep)
for chunk in graph.stream(inputs):
print(chunk)
# Stream individual tokens from LLM nodes
for chunk in graph.stream(inputs, stream_mode="messages"):
print(chunk)
Key Architecture Patterns from the Official Guide
| Pattern | Structure | Best For |
|---|---|---|
| Prompt Chaining | Sequential LLM calls | Translation, verification, step-by-step generation |
| Parallelization | Fan-out → aggregate | Multi-criteria scoring, parallel research tasks |
| Routing | Classify → dispatch | Customer support triage, content-type routing |
| Orchestrator-Worker | Plan → fan-out → synthesize | Report writing, code generation across files |
| Evaluator-Optimizer | Generate → evaluate → loop | Iterative refinement, quality gates |
| Agent | LLM ↔ tool calls | General autonomous agents |
Design Principles
- State is the source of truth — all inter-node communication happens through state, not through side channels or global variables.
- Nodes are pure-ish — a node receives state, does work, returns updates. It should not depend on state that isn't passed to it.
- Reducers prevent conflicts — any state key written by multiple nodes in parallel MUST have a reducer.
- Start simple, add complexity only when needed — a single agent with good prompts beats a multi-agent system with bad routing. Add agents only when a single prompt or toolset becomes unwieldy.
- Use
Send()for dynamic fan-out — when you don't know how many workers you'll need at compile time, useSend()to spawn workers dynamically from the orchestrator node. - Subgraph state isolation — subgraphs with different state schemas need a wrapper function to transform state at the boundary. Shared-schema subgraphs can be added directly as nodes.