LCEL is excellent for chains — linear or branched compositions where data flows one direction. But real agents have characteristics chains struggle with:
Limitation 1 — Loops
Agents loop. "Call a tool, see the result, decide next step" is inherently iterative.
In LCEL you can't easily write:
1. LLM decides what to do.
2. Execute tool.
3. Go back to step 1 with the result.
4. Repeat until done.
LCEL is acyclic by design. Loops require workarounds (recursive function calls, custom Runnables).
LangGraph treats loops as first-class. You define states; transitions can cycle back.
Limitation 2 — Persistent state
Across loop iterations, the agent accumulates state:
- The user's question.
- Tools already called.
- Intermediate results.
- Decisions made.
In LCEL, "state" is whatever's passed through the chain. Each invocation starts fresh.
For multi-turn agents, you want state that persists across LLM calls AND across user turns. LangGraph provides this natively.
Limitation 3 — Conditional flow that depends on runtime state
LCEL's RunnableBranch handles simple conditionals at the start of a chain. But "if tool A returned X, do Y; else do Z; if you've already done Y three times, escalate" — that's state-machine logic.
LangGraph models it as states and transitions:
[start] → [decide] → [tool_call] → [decide] → [tool_call] → ... → [respond]
↓ ↓
[escalate] [retry_or_give_up]
Limitation 4 — Human-in-the-loop
Some agents need to pause and wait for human approval:
- "Should I refund this customer? (yes/no)"
- "Should I send this email? (review draft)"
LCEL has no native interrupt/resume. LangGraph does — pause execution, persist state, resume when human responds.
Limitation 5 — Streaming intermediate state
A chain streams its final output. An agent should stream its thinking:
- "I'm searching the order DB..."
- "Found the order. Checking refund eligibility..."
- "Refund eligible. Initiating refund..."
LangGraph streams state changes per step. Users see progress, not just the final answer.
What LangGraph adds
LangGraph is a separate library (pip install langgraph) in the LangChain ecosystem. It provides:
State graph
A graph where:
- Nodes are functions that modify state.
- Edges define transitions between nodes.
- Conditional edges route based on state.
- State is a typed dict (or Pydantic model) passed between nodes.
from langgraph.graph import StateGraph
from typing import TypedDict, Annotated
import operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add] # appends to list
iteration: int
def decide_node(state: AgentState) -> AgentState:
# Call LLM, decide what to do next
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response], "iteration": state["iteration"] + 1}
def tool_node(state: AgentState) -> AgentState:
# Execute tool calls from the last LLM message
...
graph = StateGraph(AgentState)
graph.add_node("decide", decide_node)
graph.add_node("tool", tool_node)
graph.add_edge("decide", "tool")
graph.add_edge("tool", "decide")
# ... conditional edges, entry point, end conditions
Checkpoint persistence
State is saved at each node transition. You can:
- Resume from any checkpoint.
- Inspect state at any point.
- Replay from a saved state.
from langgraph.checkpoint.memory import MemorySaver
graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "user-123"}}
result = graph.invoke({"messages": [user_msg]}, config=config)
# State saved under thread_id 'user-123'
# Later, same user:
result = graph.invoke({"messages": [next_user_msg]}, config=config)
# Resumes from saved state for user-123
For production: use SQLite, Postgres, or Redis-backed checkpointers instead of MemorySaver.
Streaming
Stream state updates per node:
for chunk in graph.stream({"messages": [user_msg]}, config=config):
print(chunk)
# {"decide": {...}}, {"tool": {...}}, {"decide": {...}}, ...
Each chunk shows which node ran and what state changes resulted. Built-in observability.
Interrupts
Pause execution before a node:
graph = builder.compile(
checkpointer=MemorySaver(),
interrupt_before=["execute_refund"],
)
# Execution pauses before execute_refund. State is saved.
# Human reviews the planned refund, approves/denies.
# Resume:
graph.invoke(None, config=config)
This is the production pattern for high-stakes actions.
When to use LCEL vs LangGraph
| Need | Use |
|---|---|
| Single LLM call | LCEL |
| Sequence of fixed transformations | LCEL |
| RAG with retrieve → generate | LCEL |
| Conditional branching (simple) | LCEL with RunnableBranch |
| Multi-step agent with tools and looping | LangGraph |
| State across turns | LangGraph |
| Multi-agent system | LangGraph |
| Human-in-the-loop | LangGraph |
| Long-running with checkpoints | LangGraph |
Most production "agentic" applications eventually want LangGraph.
When LangGraph might be overkill
- One-shot prompts.
- Simple RAG (retrieve, answer, done).
- Workflows with no LLM-driven branching.
For these, LCEL is simpler and lighter.
The mental model
LCEL: "I know the data flow. I just need to compose pieces."
LangGraph: "I have a state machine where the LLM (or other functions) update state and decide which state comes next."
If your problem fits a state machine — multiple states, transitions, possibly cycles — LangGraph is the natural framing.
Common framing mistakes
- Forcing complex logic into LCEL. Once you have 3+ branches and loops, switch.
- Using LangGraph for trivial chains. Overhead without benefit.
- Treating LangGraph as just an agent loop. It's a general state machine; loops are one pattern.
What the rest of this module covers
- Nodes, edges, state (lessons 3-4).
- Conditional edges and routing (5-6).
- Persistence and checkpointing (7-8).
- Human-in-the-loop and interrupts (9-10).
By the end of Module 2 you'll be able to build any single-agent state machine. Module 3 extends to multi-agent.
Takeaway
Chains are linear; agents loop. Chains are stateless; agents accumulate state. LCEL handles chains; LangGraph handles state machines. Use LangGraph when you need loops, state, complex routing, persistence, human-in-the-loop, or streaming intermediate state. Most production agents need it.