Agents in LangChain 1, create_agent and middleware
In LangChain's first major version the way agents get built changed entirely. The previous approach based on an agent executor was retired, and one agent creation function took its place, running on the LangGraph runtime. The current release is 1.3.14, under the MIT licence.
That carries a direct consequence for learning: practically every tutorial older than this version shows code that is now retired. If you copy an example from an article and receive a warning or an import error, this is exactly that situation.
What exactly was retired
Three things to recognise in old code.
The agent executor, the class binding a model to tools and running the loop. The function initialising an agent from a type name. And the function creating an agent in the reasoning and acting pattern, in the form it held in the main package.
All three are replaced by one agent creation function, available in the main package's agents module. It takes a model, a tool list, and a prompt, and returns something callable.
Worth knowing at once that retirement here means removal rather than a mere deprecation marker. The main package was trimmed to a handful of modules, and the old executor along with the initialisation function moved into a separate legacy package named langchain-classic, installed on its own. An unchanged old import therefore ends in an import error rather than a warning. You only see warnings after installing that package and rewriting the import paths onto its namespace.
The reason for retirement is technical and worth understanding, since it explains the rest of the changes. The old executor was a loop: call the model, check whether it wants a tool, call the tool, repeat. That loop had no room for branching, for stopping and waiting for a human decision, or for resuming after a failure. Anything beyond a simple loop required working around the library.
The new approach places an agent on a graph that has those things built in. An agent is a graph with a defined node layout, and you get a convenient constructor instead of assembling it by hand.
The basic arrangement
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def check_status(number: str) -> str:
"""Returns an order's status by its number."""
return db.status(number)
agent = create_agent(
model="anthropic:claude-sonnet-5",
tools=[check_status],
prompt="You answer questions about orders. Use the tool, do not guess.",
)
result = agent.invoke({"messages": [{"role": "user", "content": "where is 4471"}]})Three things deserve noticing.
The model is supplied as a string prefixed with the provider, which reduces switching providers to changing one literal. A prepared model object can be passed instead when you need particular settings.
A tool is an ordinary function with a documentation string. That string reaches the model and is the only information it uses when deciding to call, so a sentence stating plainly when to call the tool beats one stating merely what it does.
The result is a structure holding message history rather than a string. That detail trips most people arriving from the old approach.
Middleware, the heart of the new approach
This element separates the current solution from the previous one most, and it deserves learning, since it replaces most of the workarounds people used before.
Middleware is a layer hooked into a particular moment of an agent's work. You can change what goes to the model, inspect a decision to call a tool before it executes, modify a tool's result before it returns to the model, or halt the flow.
Four uses appear most often.
Human approval: the agent intends to call a tool performing an irreversible operation, the layer halts the flow and waits for confirmation. Previously that required breaking the loop apart and writing your own.
Trimming history: on a long conversation the context stops fitting, so the layer removes older messages or summarises them before sending to the model.
Restricting permissions: the layer checks whether the current context permits calling a given tool and rejects the attempt before anything executes. That is the right place for such a check, since it does not depend on what the model read in the conversation.
Recording and measurement: the layer sees every call, so it is the natural home for attaching identifiers, counting cost, or emitting events to a tracing tool, Arize Phoenix for instance.
The arrangement's advantage is that layers are independent and composable. Adding approval requires no changes to the history trimming code, and disabling one of them in a test environment means removing an entry from a list.
The relationship to LangGraph
Worth settling, since the question of LangChain versus LangGraph comes up often and stopped making sense in its old form.
An agent created by that function is a graph. The constructor assembles a graph of a fixed shape: a node calling the model, a node executing tools, and a conditional edge deciding whether to return to the model or finish.
That means everything the lower layer provides is available: checkpoints recording state after every step, resuming an interrupted run, time travel, and browsing state history. I covered that at length in the text on memory in LangGraph.
Practical advice on choosing a level. Start with the constructor, since it covers the typical case: a model plus tools plus a prompt. Descend to assembling a graph by hand when you need a shape the constructor does not offer, two models in different roles for instance, or branching by question category.
Moving between levels is not a rewrite, since you work inside the same runtime. That is in fact this change's main benefit: previously, outgrowing the executor meant abandoning the library.
Migrating old code
An order that works when you hold a running agent in the old approach.
Start with an inventory. List what your current code does beyond a simple loop: trimming history, handling tool errors, approval, logging. That is the list of things that become layers.
Then replace the agent creation itself. Model, tools, and prompt transfer almost one to one, so a first version runs quickly.
The third step is moving workarounds onto layers. That is usually where the most code disappears, since things written by hand around the old loop now have a proper home.
The fourth is handling the result. The old executor returned a dictionary with a key holding the answer as text; the new one returns a structure with message history, so the places reading the result need fixing.
Do not migrate everything at once if you hold several agents. One to begin with, a week in production, then the rest. The older approach can be kept alive through the legacy package, so the time pressure is lower than the messages suggest, though it costs you installing that package and fixing imports everywhere the old executor appears.
Tools and what decides accuracy
Since an agent reduces to a model calling tools, tool quality decides the outcome more than anything else.
The description matters most and gets written badly most often. The model sees only the function name, the documentation text, and the argument schema, so that is all the information behind its decision. "Checks an order's status" states what the tool does. "Call this when the user asks about an order's state and supplied its number" states when to call it, and that difference shows in accuracy statistics.
Describe arguments with the narrowest types available. A field accepting any string will be filled with anything, while a field accepting one of three values limits mistakes at the source. Schema validation, in Zod on the interface side or its Python equivalent, plays a double role here: it describes the shape to the model and rejects an invalid call.
Error handling deserves separate thought. A tool that throws returns a message to the agent, and the agent decides what next from it. A message stating what went wrong and what could be done instead turns a failure into an intermediate step. A technical message usually leads to repeating the same attempt.
The last item is tool count. An agent with five tools chooses accurately; an agent with thirty starts losing its way, since the descriptions compete inside the context. Beyond a certain count it pays to consider splitting into several specialised agents, or a layer selecting a subset of tools from the question.
Costs and run limits
An agent differs from a single model call in deciding for itself how many times to call, and that translates directly into the bill.
Every step carries the whole conversation so far along with tool results. A run of six steps is not six calls of similar cost but six calls of rising cost, since the context accumulates.
Two things therefore deserve setting from the start. A step limit halting the run protects against a loop where the agent calls a tool returning an error and retries indefinitely. And history trimming, since without it a long run stops fitting the context window.
Matching the model to the role pays too. An agent deciding which tools to call need not be the strongest model when the tool set is clearly described. Save the stronger model for steps requiring reasoning, and with two models in different roles you have already descended to a hand built graph.
The last item is observation. Without recorded runs you do not know how many steps a typical request takes or which tool gets called most often without need. Those numbers usually surprise on first inspection and lead to the simplest savings.
That record comes by one of two routes, worth telling apart because they differ in setup cost. The first is a layer inside the agent's code, described above. The second is a proxy between the application and the model vendor, counting tokens and timings without touching code, though the best known example of that approach, Helicone, has belonged to Mintlify since March 2026 and runs in maintenance mode, so on a new deployment the in code layer is the safer pick.
LangChain against the alternatives
| Option | Level | Dependency | Pick it when |
|---|---|---|---|
| create_agent | A constructor over a graph | LangChain plus LangGraph | A typical agent with tools |
| A hand built graph | Full flow control | LangGraph | An unusual flow shape |
| OpenAI Agents SDK | A constructor from a provider | Tied to that provider | You work in one ecosystem |
| Your own loop | No abstraction | The model client alone | A simple case, you want to understand it all |
The last row deserves honest consideration and gets skipped. A loop calling the model, checking whether it wants tools, and calling them is about forty lines of code. With one agent and three tools, writing it yourself gives full understanding and zero dependencies.
The library's value appears with things your own loop would have to add: durable state, resumption, human approval, streaming intermediate events, handling parallel tool calls. If you need none of them, the library is a cost with no return.
The third row is a choice on a different axis: it ties you to one model provider in exchange for fewer pieces to assemble. With certainty about the provider that is sometimes sensible; with a need to compare models less so.
Common mistakes
The first is learning from tutorials older than the first major version. The code they show is retired, and the differences are large enough that no small fix repairs it.
The second is a tool description stating what it does rather than when to call it. The model sees only the name, the description, and the argument schema, so that sentence decides accuracy more than the system prompt.
The third is no limit on step count. An agent calling a tool that returns an error can retry indefinitely while the bill grows in the background.
The fourth is approval for irreversible operations resting on a prompt rather than a layer. A prompt is a suggestion the model can ignore; a layer is code it cannot bypass.
The fifth is reading the result as in the old approach. The new one returns a structure with message history rather than text under a fixed key.
The sixth is reaching for the library for a case forty lines of your own loop would handle. The abstraction pays off with durability and resumption, not with one agent and three tools.
FAQ
Is AgentExecutor retired?
Yes, along with the agent initialisation function and the previous function creating an agent in the reasoning and acting pattern. One agent creation function in the agents module replaces them, built on the LangGraph runtime. The retired classes are gone from the main package and live on in a separate legacy package.
Why was the approach changed?
Because the old executor was a simple loop with no room for branching, stopping for a human decision, or resuming after a failure. Anything beyond that loop required working around the library, and the new approach has those things built in.
What is middleware in this context?
A layer hooked into a particular moment of an agent's work: before calling the model, before executing a tool, or after it. It exists for approving operations, trimming history, restricting permissions, and measurement, meaning things previously written by hand around the loop.
Do I have to learn LangGraph?
For a typical agent no, since the constructor assembles the graph for you. Worth knowing that an agent is a graph, though, since that makes checkpoints, resumption, and state history browsing available without changing libraries.
How urgently must I migrate?
Without haste, though doing nothing at all is not an option. The agent executor vanished from the main package, so keeping old code around for a while means installing the legacy package and fixing its import paths; only then does it keep running, with deprecation warnings. A sensible route is migrating one agent, a week of observation in production, and only then the rest. The largest part of the work is moving your own workarounds onto layers.
The agent documentation sits on the project site, and the code and releases in the GitHub repository.