Why did LangChain v1 replace AgentExecutor with create_agent?
answer
- the old loop had nowhere to stop
- state you can persist and resume
- hooks instead of subclassing
- the guard changed shape too
- legacy surface moved out of the main package
basics
~20 sAgentExecutor was an opaque while-loop: you could not pause it, resume it, inspect its state or extend it. LangChain v1's create_agent returns a LangGraph graph, so the same loop gains persistence, human-in-the-loop pausing, streaming of intermediate steps and middleware hooks.
solid answer
~50 s`AgentExecutor` belonged to the pre-1.0 era. It ran the tool loop inside a single callable: you passed an agent and tools, set `max_iterations`, and got a final answer. Everything in the middle was inaccessible — no way to stop before a risky tool ran, resume after a restart, or inject a policy step between turns without forking the class. In LangChain 1.x, `create_agent` from `langchain.agents` builds the same loop as a compiled LangGraph graph over a `messages` state. That change buys four things: **persistence** (attach a checkpointer and a run survives a process restart), **human-in-the-loop** (pause before a tool call, approve or edit, resume), **streaming** of intermediate steps rather than only the final answer, and **middleware** — hooks like `before_model`, `after_model` and `wrap_model_call` plus prebuilts such as `SummarizationMiddleware` and `HumanInTheLoopMiddleware` that extend the loop without subclassing it. The cost is that you now think in graph terms, and the executor-era surface has moved to the classic compatibility package.
code
python · 12 linesfrom langchain.agents import create_agent
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[lookup_order],
system_prompt="You are a support assistant. Cite the order id you used.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What is the status of ORD-1234?"}]}
)
print(result["messages"][-1].content)go deeper
Know that AgentExecutor is the older way and that create_agent is the v1 entry point, invoked with a messages dict.
Name the concrete capabilities the graph-based agent adds — persistence, pausing, streaming of intermediate steps, middleware hooks — and say where the legacy surface went.
Bring migration reality: the guard changed from a graceful iteration cap to a step-counting exception, prompts and memory moved, and the graph model is a second thing your team must operate.
Argue the strategic point — an agent framework that owns its control flow cannot be extended safely, so exposing state and step boundaries is what makes approval gates, cost caps and recovery possible without forking — and be equally clear about when no agent is warranted.
## What the executor was In the 0.1/0.2 era, the standard agent was an `AgentExecutor` wrapping an agent runnable and a tool list. You built the runnable with a helper such as `create_tool_calling_agent(model, tools, prompt)`, handed it to `AgentExecutor(agent=..., tools=..., max_iterations=15)`, and called `invoke`. Inside, a loop asked the model for an action, ran the tool, appended the observation and repeated until the model produced a final answer or the iteration cap tripped. It worked, and for a simple bounded task it was pleasant. Its limits were structural rather than cosmetic: - **No pause point.** The loop was a Python `while`. There was nowhere to suspend before a destructive tool ran and wait for a human. - **No durable state.** A crash lost the run entirely. Long research tasks had no resume story. - **Opaque middle.** You could observe via callbacks, but you could not *intervene*: no way to rewrite the messages before the next model call, trim history, or swap models on failure without subclassing. - **All-or-nothing output.** Streaming gave you tokens of the final answer; the intermediate reasoning and tool results were awkward to surface incrementally. - **Fixed control flow.** Anything that was not a straight loop — a branch, a second agent, a retry with a different model — meant leaving the abstraction. ## What v1 changed `create_agent(model, tools, ...)` returns a **compiled graph**. It is still invoked simply — `agent.invoke({"messages": [{"role": "user", "content": "..."}]})` — and returns the full message list, so the easy case stays easy. But because the loop is now a graph with a state and named steps, it inherits the graph runtime's capabilities: **Persistence.** Pass a checkpointer and each super-step is written to durable storage under a thread id. A crashed or deliberately paused run resumes from the last checkpoint instead of restarting. **Human-in-the-loop.** Because the run can suspend at a step boundary and its state is durable, an approval gate becomes a first-class feature rather than a fork of the executor. `HumanInTheLoopMiddleware` packages the common shape: pause before configured tools, surface the pending call, resume on approval or with edited arguments. **Streaming of everything.** The graph emits state updates and messages as they happen, so a UI can show "calling search…", the tool result, then the final answer — the difference between a spinner and a visible agent. **Middleware.** This is the headline v1 extensibility mechanism. Hooks — `before_model`, `after_model`, `wrap_model_call`, `wrap_tool_call` — let you insert behaviour into the standard loop without reimplementing it: enforce a turn budget, redact content, retry on a different model, compact the history. `SummarizationMiddleware` handles the last of those out of the box, keeping long runs inside the context window. **Structured output.** `response_format` lets the agent finish with a validated object rather than free text, which is what makes an agent embeddable in a larger system rather than only in a chat window. ## What it costs Honesty about the tradeoffs matters in an interview: - **A second mental model.** You are now operating a graph runtime; debugging means reasoning about state and steps, not reading a loop. - **Different guard semantics.** The executor's `max_iterations` had a graceful stop mode; the graph's `recursion_limit` raises an error and counts super-steps, so a naive port halves your effective budget and turns a soft stop into an exception. - **Migration is not mechanical.** Prompt handling, memory and callbacks all moved. Legacy pieces including the executor and its `create_*_agent` builders live in the classic compatibility package rather than the main one, so pinning old code is possible but is a dead end. - **Overkill for simple cases.** If your task is one model call plus one deterministic tool, none of the graph machinery earns its keep and a plain composed pipeline is cheaper and easier to test. ## How to answer this in an interview The weak answer is "v1 is newer and better". The strong answer names the capability gap and ties each item to a production need you have actually had: approval before a destructive action, resuming a long run after a deploy, showing progress in a UI, and capping cost without forking the framework. Add the honest caveat that a bounded, well-understood task rarely needs an agent at all — which is a better reason to reach past both APIs than any feature list.
- Which capability of the v1 agent is hardest to retrofit onto the old executor, and why?Durable pause-and-resume. Streaming and logging could be approximated with callbacks, and budgets could be bolted on by subclassing, but suspending mid-run and continuing after a process restart requires the run's state to be a serialisable value the runtime checkpoints at step boundaries. A Python while-loop holding everything in local variables has no such boundary, so approval gates and crash recovery were effectively out of reach.
- A team ports max_iterations=15 straight to recursion_limit=15. What goes wrong?Two things. The graph limit counts super-steps and a tool-using turn costs at least two, so 15 buys about seven iterations instead of fifteen. And exhaustion now raises GraphRecursionError instead of returning the executor's forced stop message, so callers that previously got a degraded answer suddenly get an exception. Both need to be handled explicitly during migration.
- When is create_agent still the wrong choice?When the control flow is known in advance. If the task is retrieve-then-answer, or one classification followed by one deterministic call, an agent adds a model call whose only job is to pick a step you already know, plus non-determinism that makes evaluation harder. Compose a fixed pipeline instead and keep the agent for genuinely data-dependent, unbounded work.
saying these in an interview costs you the question
- Describing AgentExecutor or initialize_agent as the current LangChain API
- Claiming the change was only a rename with no capability difference
- Assuming max_iterations maps one-to-one onto recursion_limit
- Thinking middleware requires subclassing the agent
- Reaching for an agent when the control flow is already known