LangGraph and LangChain agents solve different problems. LangChain agents are a thin abstraction over ReAct-style prompting loops — good for simple tool use, brittle when you need cycles, branching, or human-in-the-loop. LangGraph is a stateful graph orchestrator built for multi-step, multi-agent workflows with explicit control flow, checkpointing, and streaming. If you’re building a chatbot that calls a few tools, LangChain agents are fine. If you’re building a coding agent, a research pipeline, or anything with loops and conditional logic, you want LangGraph.
Architecture and mental model
LangChain agents follow the classic ReAct pattern: the LLM reasons, picks a tool, observes the result, repeats. The agent executor handles the loop. You configure tools, a prompt, and maybe a memory buffer. The control flow is implicit — the LLM decides what happens next at each step.
# LangChain agent (simplified)
from langchain.agents import create_react_agent, AgentExecutor
from langchain.tools import tool
@tool
def search_web(query: str) -> str:
...
agent = create_react_agent(llm, tools=[search_web], prompt=prompt)
executor = AgentExecutor(agent=agent, tools=[search_web], verbose=True)
result = executor.invoke({"input": "What's the weather in Tokyo?"})
LangGraph models workflows as graphs: nodes are functions (LLM calls, tools, custom logic), edges define transitions. State moves through the graph explicitly. You can have cycles, parallel branches, conditional edges, and subgraphs. The LLM doesn’t drive control flow — your graph does.
# LangGraph (simplified)
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
iterations: int
def call_model(state: AgentState):
response = llm.invoke(state["messages"])
return {"messages": [response], "iterations": state["iterations"] + 1}
def should_continue(state: AgentState):
return "tools" if state["iterations"] < 3 else END
graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", tool_node)
graph.add_edge("agent", "tools")
graph.add_conditional_edges("tools", should_continue)
graph.set_entry_point("agent")
app = graph.compile()
result = app.invoke({"messages": [HumanMessage("Research quantum computing")], "iterations": 0})
The difference matters. In LangChain, a loop means the LLM keeps calling tools until it decides to stop. In LangGraph, you define the loop explicitly — max iterations, exit conditions, branching logic. You can inspect and modify state at any node. You can pause, resume, or rewind.
Control flow and cycles
LangChain agents support basic loops via max_iterations and early_stopping_method. But you can’t express “run these three tools in parallel, then fan-in, then conditionally branch based on results” without fighting the framework. The agent executor is a single loop. Nested agents are possible but awkward — you’re composing executors, not graphs.
LangGraph makes parallel execution, fan-out/fan-in, and conditional branching first-class:
# Parallel tool execution, then fan-in
graph.add_node("search", search_tool)
graph.add_node("analyze", analyze_tool)
graph.add_node("summarize", summarize_node)
graph.add_edge("agent", "search")
graph.add_edge("agent", "analyze") # both run in parallel
graph.add_edge(["search", "analyze"], "summarize") # fan-in
Conditional edges let you route based on state:
def route(state: AgentState) -> str:
if state["error_count"] > 2:
return "escalate"
if state["confidence"] < 0.7:
return "human_review"
return "continue"
graph.add_conditional_edges("agent", route, {
"escalate": "escalation_node",
"human_review": "review_node",
"continue": "next_step"
})
This is not syntactic sugar — it changes what you can build. A coding agent that writes tests, runs them, analyzes failures, and retries with a different approach needs exactly this kind of control flow.
State management and checkpointing
LangChain agents use ConversationBufferMemory or ConversationSummaryMemory. State is a message list. You can’t easily attach arbitrary metadata (token counts, tool latencies, user preferences) without subclassing. Checkpointing is manual — you serialize the memory yourself.
LangGraph state is a typed dict. You define the schema. Checkpointing is built in via MemorySaver, SqliteSaver, or PostgresSaver. Every graph invocation produces a checkpoint. You can resume from any checkpoint, fork from a previous state, or replay with modified inputs.
from langgraph.checkpoint.sqlite import SqliteSaver
with SqliteSaver.from_conn_string("checkpoints.db") as saver:
app = graph.compile(checkpointer=saver)
# First run
config = {"configurable": {"thread_id": "user-123"}}
result = app.invoke({"messages": [HumanMessage("Plan a trip to Japan")]}, config)
# Resume later — state is restored automatically
result = app.invoke({"messages": [HumanMessage("Actually, make it cheaper")]}, config)
# Fork from an earlier checkpoint
checkpoints = list(saver.list(config))
earlier_config = {"configurable": {"thread_id": "user-123", "checkpoint_id": checkpoints[0].config["configurable"]["checkpoint_id"]}}
forked = app.invoke({"messages": [HumanMessage("Try a different approach")]}, earlier_config)
This enables human-in-the-loop workflows that are painful in LangChain: pause for approval, edit state, resume. Time-travel debugging — rewind to a checkpoint, change a parameter, replay — is trivial.
Streaming and observability
LangChain agents stream tokens via astream or astream_events. You get token-level streaming from the LLM, but tool calls and intermediate steps are opaque until complete. verbose=True prints to stdout — not production-friendly.
LangGraph streams at the graph level. astream yields state updates after each node. astream_events yields granular events: node start/end, tool calls, LLM tokens, conditional edge decisions. You can build a real-time UI showing exactly where the graph is.
async for event in app.astream_events({"messages": [HumanMessage("...")]}, config, version="v2"):
if event["event"] == "on_chain_start":
print(f"Node started: {event['name']}")
elif event["event"] == "on_tool_start":
print(f"Tool called: {event['name']} with {event['data']['input']}")
elif event["event"] == "on_chat_model_stream":
print(event["data"]["chunk"].content, end="")
This matters for production. You can log every transition, measure per-node latency, alert on stuck nodes, and show users a live trace. LangChain’s LangChainTracer helps but operates at a coarser granularity.
Debugging and testing
LangChain agents are hard to test deterministically. The LLM drives control flow, so the same input can produce different tool sequences. You can mock tools, but you can’t easily assert “the agent should have called search, then analyze, then summarize” because the agent might skip steps or loop unexpectedly.
LangGraph graphs are testable as pure functions. Feed a state, assert the output state. Test individual nodes in isolation. Test the full graph with controlled inputs. Because control flow is explicit, you can write integration tests that verify the exact path taken.
def test_research_graph_happy_path():
# Mock the LLM to return predictable tool calls
mock_llm = MockLLM(responses=[
AIMessage(content="", tool_calls=[{"name": "search", "args": {"query": "quantum computing"}}]),
AIMessage(content="", tool_calls=[{"name": "analyze", "args": {"text": "search results"}}]),
AIMessage(content="Final summary"),
])
graph = build_graph(mock_llm)
result = graph.invoke({"messages": [HumanMessage("Research quantum computing")], "iterations": 0})
assert result["iterations"] == 3
assert len(result["messages"]) == 4 # human + 3 AI messages
# Verify exact node execution order via checkpoints
You can also use graph.get_graph().draw_mermaid() to visualize the workflow — useful for documentation and architecture reviews.
Multi-agent patterns
LangChain supports multi-agent via initialize_agent with AgentType.OPENAI_MULTI_FUNCTIONS or by chaining executors. But there’s no built-in pattern for agent handoff, shared state, or supervisor/worker topologies. You build it yourself.
LangGraph has first-class multi-agent patterns. The create_react_agent function (yes, LangGraph has its own) produces a subgraph you can compose. Supervisor pattern:
from langgraph.prebuilt import create_react_agent
researcher = create_react_agent(research_llm, [search_tool, analyze_tool])
coder = create_react_agent(coder_llm, [write_code_tool, test_tool])
reviewer = create_react_agent(reviewer_llm, [review_tool])
def supervisor(state: AgentState) -> str:
last_message = state["messages"][-1]
if "research" in last_message.content.lower():
return "researcher"
if "code" in last_message.content.lower():
return "coder"
return "reviewer"
graph = StateGraph(AgentState)
graph.add_node("supervisor", supervisor_node)
graph.add_node("researcher", researcher)
graph.add_node("coder", coder)
graph.add_node("reviewer", reviewer)
graph.add_conditional_edges("supervisor", supervisor, {
"researcher": "researcher",
"coder": "coder",
"reviewer": "reviewer"
})
graph.add_edge("researcher", "supervisor")
graph.add_edge("coder", "supervisor")
graph.add_edge("reviewer", END)
Each agent is a self-contained graph with its own state, tools, and checkpointer. They share the parent graph’s state via the message list. This composability is why LangGraph wins for multi-agent systems.
Ecosystem and integrations
LangChain has a massive ecosystem: 100+ tool integrations, 50+ vector stores, 30+ chat models, document loaders for everything. If you need a niche integration (Salesforce, Notion, obscure vector DB), LangChain probably has it. LangGraph uses LangChain’s integrations under the hood — create_react_agent accepts LangChain tools. You don’t lose the ecosystem.
But LangGraph adds its own prebuilt components: ToolNode for parallel tool execution, InjectedState for passing graph state to tools, ValidationNode for output validation. The langgraph-prebuilt package includes common patterns (ReAct agent, supervisor, planner-executor) as reusable graphs.
Performance and latency
Both frameworks add minimal overhead — they’re orchestration layers, not model servers. The latency difference comes from architecture:
- LangChain agent: sequential by default. Each tool call waits for the previous. Parallel tool calling requires
OpenAIFunctionsAgentwithparallel_tool_calls=True(OpenAI only). - LangGraph: parallel execution is explicit. Fan-out/fan-in reduces wall-clock time for independent operations. Checkpointing adds ~5-15ms per node (SQLite) — negligible for most workloads but measurable at high throughput.
For high-throughput serving, both can run stateless (no checkpointer) to minimize latency. LangGraph’s compiled graph has slightly lower per-step overhead because there’s no agent executor loop parsing LLM output — the graph just routes.
Deployment and production concerns
LangChain agents deploy as standard Python services. You manage memory, sessions, and scaling yourself. LangChain’s Runnable interface helps with batching and async, but you’re building the server.
LangGraph adds LangGraph Platform (commercial) for managed deployment: horizontal scaling, built-in checkpointing, horizontal scaling, cron jobs, and a studio for debugging. The open-source version runs anywhere — FastAPI, AWS Lambda, Kubernetes. The checkpointing backends (PostgreSQL, Redis) are production-grade.
If you’re deploying to n4n.ai or similar inference gateways, both frameworks work — they just need an OpenAI-compatible endpoint. LangGraph’s explicit control flow makes it easier to implement routing directives (e.g., “use cheaper model for classification, expensive model for generation”) because you control which node calls which model.
When LangChain agents are the right choice
- Simple ReAct loops: one to three tools, linear flow, no branching
- Prototyping: you want working code in 10 minutes, not a graph definition
- Existing LangChain codebase: migrating to LangGraph isn’t free
- Single-turn tool use: “call this API, return result” — no conversation, no loops
- Teams unfamiliar with graph concepts: lower learning curve
# This is fine in LangChain. Don't overthink it.
executor = AgentExecutor(agent=agent, tools=[calculator, unit_converter], max_iterations=3)
When LangGraph is the right choice
- Multi-step workflows with conditional logic
- Cycles, retries, or iterative refinement (coding agents, research agents)
- Human-in-the-loop: approval gates, editing state mid-run
- Multi-agent systems: supervisor/worker, planner/executor, swarm
- Long-running conversations needing checkpoint/resume
- Production observability: per-node tracing, latency budgets, alerting
- Testing requirements: deterministic graph execution, node-level unit tests
Verdict: which to choose
| Use case | Recommendation | Reason |
|---|---|---|
| Chatbot with 2-3 tools | LangChain agents | Simpler, faster to build, sufficient |
| Coding agent (write → test → fix) | LangGraph | Needs cycles, conditional retry, state inspection |
| Research pipeline (search → analyze → synthesize) | LangGraph | Parallel fan-out, fan-in, explicit stages |
| Multi-agent supervisor/worker | LangGraph | Native composition, shared state, handoff patterns |
| Human approval workflows | LangGraph | Checkpointing, pause/resume, state editing |
| High-throughput stateless tool calling | Either | Both work; LangGraph slightly lower overhead |
| Team new to LLM orchestration | LangChain agents | Gentler learning curve, more tutorials |
| Production system needing observability | LangGraph | Event streaming, per-node metrics, traceability |
| Niche integration (legacy CRM, custom DB) | LangChain agents | Larger integration ecosystem |
| Time-travel debugging / replay | LangGraph | Built-in checkpointing, fork from any state |
Start with LangChain agents if your workflow is linear and shallow. Switch to LangGraph the moment you feel the agent executor fighting you — when you need a loop with a custom exit condition, when you want to run two tools in parallel, when you need to pause for human input, or when you’re adding a second agent. The migration path is incremental: wrap your LangChain agent as a LangGraph node, then expand the graph from there.