The LangChain ReAct agent you're importing is deprecated
AI Agents · By Caleb Sakala · June 10, 2026
If you copied from langgraph.prebuilt import create_react_agent out of a tutorial written in 2024 or early 2025, the import still runs, but it now prints a deprecation warning. For anyone searching for the right LangChain ReAct agent in 2026, the short answer is to use create_agent from the langchain package. The older constructors still work, and they will keep working for a while, but they are on the way out, and the migration has a few sharp edges the warning message never mentions.
This guide covers which constructor to call now and when to skip the ReAct loop entirely.
Three functions answer to the name LangChain ReAct agent
"ReAct agent" in LangChain has meant three different pieces of code over about two years, and search results mix all of them together.
The original langchain.agents.create_react_agent built a classic AgentExecutor around a text ReAct prompt, the kind that asks the model to write Thought, Action, and Observation lines. Then langgraph.prebuilt.create_react_agent arrived and became the version most production code uses: it builds a graph that calls the model, runs any tool calls, and feeds the results back until the model stops asking for tools. The reference page for that function now carries a Deprecated badge at the top.
Its replacement, create_agent, lives in the langchain package. It builds the same graph-based runtime on top of LangGraph, adds a middleware system, and is the constructor LangChain now points everyone toward.
# 2024 tutorial code, still runs, now deprecated
from langgraph.prebuilt import create_react_agent
agent = create_react_agent("openai:gpt-4", tools=[search])
# 2026 replacement
from langchain.agents import create_agent
agent = create_agent("openai:gpt-5.4", tools=[search])
In the common case you pass the same arguments and get the same loop underneath. The reason to switch is not a nicer API surface for a three-line example. New features, middleware hooks, and fixes land on create_agent, while the deprecated function is scheduled for removal in version 2.0.
What the deprecation warning tells you to do is sometimes wrong
One detail here wastes an afternoon if you miss it. The deprecation warning on langgraph.prebuilt.create_react_agent tells you to import create_agent from langchain.agents. On some version combinations, that import fails, because the symbol is not where the warning says it is. A maintainer thread documents the warning pointing at the wrong path, and a separate forum report covers create_agent going missing from langchain.agents on a 1.1.0 install.
Fixing it is dull but worth knowing before you start: pin your versions. Confirm that langchain and langgraph are on releases that actually ship the symbol you are importing, rather than upgrading one package and trusting the warning. The LangGraph v1 migration guide is the source to trust when the inline warning and the installed package disagree.
Why the loop quits at 25 steps
A different failure shows up once the agent is running. It works on short tasks, then on a harder one it either errors out or returns a strange apology instead of an answer.
LangGraph caps how many times the graph can cycle. It sets recursion_limit to 25 by default, and when a run hits the ceiling, LangGraph raises a GraphRecursionError. With the prebuilt ReAct agent the behavior is subtler. Its state tracks a remaining_steps counter, and per the function reference, when remaining_steps drops below 2 while the model is still asking for tools, the agent returns a final message reading "Sorry, need more steps to process this request." instead of raising. No exception, no stack trace, just a polite non-answer that is easy to misread as a model quality problem. An open issue covers cases where the error does not fire when you would expect it.
Raising the ceiling takes one line:
agent.invoke(
{"messages": [{"role": "user", "content": task}]},
config={"recursion_limit": 50},
)
A run that needs 50 steps is usually a signal rather than a setting. Most of the time the agent is stuck in a two-step loop: it calls a tool, the tool returns something it cannot use, and it tries again with the same arguments. Raising the limit just buys more iterations of the same mistake. Read the message history before you touch the number.
When a LangChain ReAct agent is the wrong tool
The ReAct pattern exists so a model can decide, at runtime, which tool to call next based on what the last tool returned. Simon Willison's working definition of an agent captures it well: a model running tools in a loop to reach a goal. That runtime choice is the whole point, and it is also the whole cost. Every loop is another model call and another chance to pick the wrong tool.
A lot of work labeled "agent" never needs that choice. A vendor-invoice pipeline that pulls totals from 400 PDFs a day follows one path: extract, validate, record. The model does not need to deliberate over whether to run the parser again. Wrapping that in a ReAct loop adds latency and a fresh failure mode in exchange for flexibility the task never uses.
Workflow shape matters more than agent cleverness here. A Chase Agents automation built for that invoice job runs as a fixed sequence: an extraction step returns structured purchase_recommendation objects that route to an approval step, and only the orchestrator step holds the tool that can issue a payment. The authorization boundary sits in the workflow, not in a system prompt the model could talk its way past. That gives you a run you can replay and reason about, because the path is not being chosen fresh on every call. Reach for a reasoning loop when the next action genuinely depends on the last result. For a known sequence, a graph with fixed edges costs less and is far easier to debug.
Moving to create_agent without losing structured output
Most people migrating care about one thing: keeping a clean typed result. create_agent handles this through response_format, and as of langchain 1.0, passing a Pydantic schema directly defaults to the provider's native structured output when the model supports it, falling back to tool-based extraction otherwise.
from pydantic import BaseModel
from langchain.agents import create_agent
class Invoice(BaseModel):
vendor: str
total: float
due_date: str
agent = create_agent("openai:gpt-5.4", tools=[fetch_pdf], response_format=Invoice)
result = agent.invoke({"messages": [{"role": "user", "content": "Parse invoice 5512"}]})
result["structured_response"] # Invoice(vendor=..., total=..., due_date=...)
Tools migrate the same way. When a Slack-triggered triage automation needs the same three tools on every run, declaring them once beats rebuilding the list inside each call. create_agent accepts tools loaded from an MCP server at construction, which mirrors how a Chase Agents workspace maps a database or a chat tool into every run scoped to that workspace: one declaration, reused across invocations, instead of per-call setup.
What to check before your next deploy
Grep your codebase for create_react_agent. If it appears, you are importing a function with a removal date on it. Swap it for create_agent; the migration is mostly mechanical, and the two traps to watch are a deprecation warning that can point at a symbol your installed version does not have, and a 25-step ceiling that fails quietly instead of loudly. The harder question is one the rename never asks: for each of those agents, does the next action actually depend on the last result? Where the answer is no, the better migration is out of a reasoning loop entirely.