Search interest tells a consistent story right now: LangChain is sliding while LangGraph, CrewAI, and Pydantic AI climb. That’s not a branding problem or a hype cycle running out of steam β it’s what it looks like when a framework’s core abstraction stops matching the problem builders actually have. LangChain won the first wave of the LLM app boom by making “call a model, chain some steps, maybe search a vector store” trivial. Agents in production are a different problem: state that needs to survive a crash, multiple agents that need to hand off work to each other, and a debugging session that needs to show you exactly which step went wrong and why. That’s the gap this shakeout is about.
The common mistake in how this gets covered is framing it as a popularity contest β LangGraph “winning,” LangChain “losing.” That’s not the useful lens. The useful lens is architectural: what does each framework assume about how your agent runs, and does that assumption still hold once you’re past a demo and into a system other people depend on. This post walks through that, grounded in actually building on both stacks, not in trend-chasing.
You’ll learn:
- What LangChain got right, and why it became the default agent framework in the first place
- The specific LangChain production problems β state management, debugging opacity, multi-agent coordination β that push teams to migrate
- What LangGraph actually changes structurally: state graphs, checkpointers, and resumable runs
- How
AgentExecutorand a LangGraphStateGraphdiffer at the code level, side by side - Where CrewAI and Pydantic AI fit into this same shift, and why they’re not interchangeable with LangGraph
- When LangChain is still the right call, and when a LangGraph migration is worth the rewrite
- How to migrate from LangChain to LangGraph incrementally instead of all at once
Table of Contents
- What LangChain got right, and why it became the default
- Where LangChain hurts in production: state, debugging, and churn
- What LangGraph actually changes: state graphs and checkpoints
- AgentExecutor vs LangGraph StateGraph: the code side by side
- CrewAI vs LangGraph vs Pydantic AI: what each one solves
- The three pain points driving the LangChain migration
- When LangChain is still the right choice
- When migrating to LangGraph is worth the rewrite
- How to migrate from LangChain to LangGraph incrementally
What LangChain got right, and why it became the default
It’s worth being honest about why LangChain became the default agent framework, because the migration story only makes sense in contrast to it. Before LangChain, every team building on top of an LLM API was writing the same boilerplate from scratch: prompt templating, retry logic around rate limits, a thin wrapper around a vector store, some ad hoc way of stuffing retrieved context into a prompt. LangChain packaged all of that into composable pieces β LLMChain, retrievers, document loaders, output parsers β and shipped integrations for basically every model provider and vector database that existed. For the first wave of RAG chatbots and simple tool-using agents, that was exactly the right trade: you didn’t need to think about architecture, you needed to ship a working demo by Friday, and LangChain got you there.
The AgentExecutor abstraction extended that same idea to agents: give the model a list of tools, let it reason in a loop, parse its output, call the tool, feed the result back. For a single agent doing a handful of tool calls in a mostly-linear sequence, this works fine and still does. A huge amount of legitimate production traffic is exactly that shape, and there’s no reason to migrate a system that fits it.
Where LangChain hurts in production: state, debugging, and churn
The trouble shows up once the system stops being a straight line, and it’s the same trouble that turns a RAG pipeline that works in the demo into one that fails in production. The AgentExecutor loop is a black box: you hand it a prompt and a tool list, and it internally manages the reasoning loop, the intermediate steps, and when to stop. That’s convenient right up until something goes wrong at 2am and you need to know exactly which tool call the agent made, with what arguments, based on what intermediate reasoning, and why it decided to make a fourth call instead of stopping at three. LangChain’s callback system can surface this, but it’s bolted on after the fact β you’re reconstructing a story from log lines instead of reading it off an explicit state machine.
State is the second, sharper pain point. LangChain’s memory abstractions were built around conversational chat history β a list of messages that grows. That’s not the same problem as agent state: which subtasks are done, what a sub-agent handed back, what the plan looks like after step three failed and needed a retry with different parameters. It’s also not the same problem as durable memory that persists across agent sessions, which is a third thing again. Teams end up bolting a database, a Redis hash, or a hand-rolled state dict onto the side of the chain to track this, which means the actual state of the agent run lives partly in LangChain’s objects and partly in code you wrote to compensate for what LangChain doesn’t track. That split is where bugs live.
Multi-agent coordination is the third pressure point, and it’s the one search data reflects most clearly. The moment you need two or more agents to hand off work β a planner that delegates to a researcher, a researcher that hands verified findings to a writer β AgentExecutor has no native concept of that. You’re back to writing your own orchestration layer on top of a framework whose entire value proposition was supposed to be “you don’t have to write that.”
There’s a quieter fourth pain point that compounds the other three: LangChain’s own churn. The library has gone through enough breaking reorganizations β module paths moving, Chain subclasses getting deprecated in favor of LCEL (LangChain Expression Language), then LCEL itself sitting somewhat awkwardly next to the newer agent APIs β that teams with a production system on an older version face a real choice between upgrading through several breaking changes or staying pinned indefinitely. A framework that’s still actively redesigning its own core abstractions is a harder thing to build a stable production system on top of than one that’s settled, and that instability is itself a reason some teams decide a migration to a more stable-feeling newer stack is less risk than continuing to ride LangChain’s version treadmill.
What LangGraph actually changes: state graphs and checkpoints
LangGraph’s pitch isn’t “LangChain but newer.” It’s a structurally different idea: your agent is a state graph, not an opaque loop. You define nodes β units of work, which can be an LLM call, a tool call, or arbitrary Python β and edges between them, including conditional edges that route based on the current state. The state itself is an explicit, typed object that flows through the graph and gets updated at each node. That single change resolves the two biggest LangChain pain points at once.
Debugging gets better because the graph is inspectable by construction. You can trace a run node by node and see the exact state at each point, not reconstruct it from callback logs. LangGraph’s checkpointer takes this further: it persists state after every node, which means a crashed run can resume from its last checkpoint instead of starting over, and you can literally replay a past run’s state transitions to see what happened. That’s the difference between “add logging and hope it’s enough” and “the framework was built assuming you’d need to inspect this.”
Multi-agent coordination stops being something you bolt on, because a graph is naturally suited to it: each agent is a subgraph or a node, and the shared state object is the handoff mechanism. A planner node writes a plan into state; a researcher node reads it, does its work, and writes findings back; a router edge decides who runs next based on what’s in state. This is the actual mechanism, not a marketing description β it’s why LangGraph shows up as the default answer when people ask how to build a multi-agent system on top of the LangChain ecosystem.
The trade-off is real, though: LangGraph is more explicit, which means more code up front for a simple case. A single-agent chatbot that answers questions over a knowledge base doesn’t need a state graph. Writing one for it is over-engineering, not architecture.
AgentExecutor vs LangGraph StateGraph: the code side by side
It’s easier to see the actual difference than to describe it. A LangChain agent loop is a single call that hides the reasoning steps inside the executor:
from langchain.agents import AgentExecutor, create_tool_calling_agent
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({"input": "Find the latest invoice and flag anomalies"})
Everything between “input” and “result” β how many tool calls happened, in what order, on what intermediate reasoning β lives inside the executor’s internals. verbose=True gets you a log stream, not a queryable state.
A LangGraph version makes that reasoning loop a graph you define yourself:
from langgraph.graph import StateGraph, END
def call_model(state: AgentState) -> AgentState:
response = llm.invoke(state["messages"])
return {"messages": [response]}
def route(state: AgentState) -> str:
last = state["messages"][-1]
return "tools" if last.tool_calls else END
graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", tool_node)
graph.add_conditional_edges("agent", route, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
app = graph.compile(checkpointer=checkpointer)
Nothing here is hidden. state is a typed object you defined, every node is a plain function you can unit test in isolation, and route is the exact decision logic that used to live inside the executor’s black box. The checkpointer argument is what buys resumability: app.compile(checkpointer=checkpointer) persists state after every node runs, so app.invoke can pick a crashed run back up from its last completed node instead of re-running the whole thing. That’s the structural difference the rest of this post is describing β not a new feature bolted onto the old model, a different model.
The explicit graph is also where operational controls get somewhere to live. Once routing is a function and state is an object, an agent kill switch and scoped tool permissions become a node and an edge rather than a wrapper you hope wraps everything.
CrewAI vs LangGraph vs Pydantic AI: what each one solves
CrewAI and Pydantic AI are climbing in the same search data, but they’re solving adjacent problems, not the same one, and conflating them with LangGraph is a common mistake.
CrewAI sits a level higher than LangGraph. Instead of you defining nodes and edges, you define roles β a “researcher” agent, a “writer” agent, an “editor” agent β each with a goal and a backstory, and CrewAI handles the delegation and sequencing between them. It’s the right tool when your multi-agent system maps naturally onto a team metaphor and you want that orchestration handled for you. The cost is the same one LangChain’s AgentExecutor had: less visibility into exactly how the delegation happens, because CrewAI owns that logic internally.
Pydantic AI is a different kind of reaction to the same underlying complaint. It’s not about orchestration at all β it’s about type safety and minimizing magic. Where LangChain and even LangGraph can leave you passing loosely typed dicts through your pipeline, Pydantic AI leans on Pydantic models for structured inputs and outputs at every boundary: the agent’s output is a validated model, not a string you’re hoping is JSON. For backend engineers who already think in terms of schemas and want an agent framework that behaves like the rest of their typed codebase instead of introducing a parallel untyped world, that’s the actual draw β not a competing orchestration story, a competing philosophy about how much the framework should infer versus how much you should declare.
Worth noting that none of these three own the tool layer the way they own orchestration. Tools increasingly arrive over MCP, the Model Context Protocol, which all of them can consume β so “which framework” and “which tools” are separate decisions, and picking one doesn’t lock in the other.
The three pain points driving the LangChain migration
Stripped of framework names, three concrete production problems are driving this whole shift. First, debugging opacity: when something in an agent’s reasoning goes wrong, how long does it take you to find out why, and does the framework give you a state trace or a pile of log lines to reconstruct by hand. Second, resumability: if a long-running agent process dies mid-task, does it restart from scratch or from where it left off, and can you actually answer that question without reading framework internals. Third, coordination: when you need more than one agent, does the framework give you a real primitive for state handoff, or are you writing that layer yourself on top of an abstraction that assumed a single agent the whole time.
Every team migrating from LangChain to LangGraph, CrewAI, or Pydantic AI is answering one of those three questions with “the old stack made this harder than it needed to be.” That’s the actual signal in the search data β not hype, a real accumulation of production friction that a new architecture addresses directly.
When LangChain is still the right choice
Don’t rewrite a system that isn’t in pain. If you’re running a single agent through a mostly-linear sequence of steps β retrieve, reason, respond, maybe call one or two tools β AgentExecutor still does that job fine, and LangChain’s integration surface (every vector store, every model provider, every document loader you can think of) is still the fastest way to wire that up. If your team already has a stable, pinned LangChain version in production with test coverage around it, the migration cost of a rewrite has to clear a real bar, not just a trend. A working system with no debugging pain, no multi-agent requirement, and no resumability requirement is not a system that needs LangGraph. Ship it, maintain it, move on.
When migrating to LangGraph is worth the rewrite
The signal to migrate isn’t “LangGraph is newer.” It’s a specific list of symptoms, and it’s worth checking your own codebase against it honestly before you start a rewrite:
- You’re debugging opaque agent failures more than once a week, and you can’t get a clear state trace when you do β you’re reading logs to guess what happened instead of inspecting what happened.
- You need two or more agents to hand off work, and you’re already writing custom orchestration code β a shared dict, a message queue, a hand-rolled router β to make that happen on top of a framework that assumed one agent the whole time.
- Your agent runs are long enough or important enough that a crash mid-task without resumability is a real incident, not an annoyance someone re-runs manually.
- Your control flow has grown enough conditional branches and retry loops that you’re fighting
AgentExecutor’s single-loop model instead of using it, and the workarounds are starting to look like a worse version of a state graph anyway.
If two or more of those are true, the rewrite usually pays for itself within a couple of incidents you’d otherwise have spent debugging blind. If none of them are true, you don’t have a framework problem β you have a working system, and the search trend is not a reason to touch it.
How to migrate from LangChain to LangGraph incrementally
The mapping from LangChain concepts to LangGraph is more mechanical than a full rewrite implies. A chain becomes a linear sequence of nodes. Memory becomes the state object plus a checkpointer. AgentExecutor’s tool loop becomes an explicit node that calls tools and a conditional edge that decides whether to loop again or move on. You don’t have to migrate everything at once β isolating the actual agent loop into a LangGraph subgraph while leaving retrievers, loaders, and other LangChain integrations in place (LangGraph is built to interoperate with them, not replace them wholesale) is a realistic incremental path, and it’s usually where teams start.
Pick the checkpointer to match how long your runs actually live. An in-memory one is fine for tests, but a run that spans minutes and survives a deploy needs a durable backend, which in practice means Postgres or Redis β the same durability question any background job queue forces you to answer, arriving in a new costume.
None of this is really about which framework wins a popularity contest. It’s the same architecture question backend engineers have always asked about any system: does the abstraction you’re using match the shape of the problem you actually have, or are you writing code around it to compensate for the mismatch. LangChain matched the first shape of LLM apps well. Production agents outgrew that shape. Make the call about your own codebase on that basis, before the debugging sessions and the bolted-on state management make it for you.
Related: once your graph is running, the routing nodes are good candidates for a small typed decision model. See Jev by TypeSafe.

