For years, building an AI agent meant choosing a vendor and living with the consequences. OpenAI's SDK, Anthropic's SDK, Google's SDK — each with its own classes, its own message format, its own retry semantics. Swap providers and you rewrote your application. LangChain 1.0, released stable on October 22, 2025, was built to kill that problem: one import path, one API, and a single function called create_agent that wires a model, tools, memory, and middleware into a runnable agent. This article is the full tour of what that actually means in practice — the ecosystem, the setup, and every major capability, from a weather tool to structured output, memory, multimodal input, RAG, and the middleware system that makes 1.0 genuinely different. It also covers what has shipped since launch — the release line has moved fast, and the latest version, langchain v1.4.0 (September 1, 2026), brings first-class Model Context Protocol (MCP) support into the core package.
Swap OpenAI for Anthropic tomorrow and your agent code barely changes. That is the whole point of LangChain 1.0.
Before touching code, it is worth being precise about the pieces that make up the modern LangChain ecosystem, because people use the names interchangeably and they are not the same thing. As of 2026 the open-source stack is three layers — Deep Agents, LangChain, and LangGraph — plus the LangSmith observability platform. They are fully composable, so you move between layers instead of picking one.
create_deep_agent. Fun fact: Deep Agents is just the core LangChain agent plus a bunch of middleware.create_agent, and its middleware system is how you modify that loop. This is the layer this article is about.InMemorySaver checkpointer, is what gives agents memory.The rule of thumb from the LangChain team: start with Deep Agents when you want a capable agent out of the box; reach for LangChain when you want the core building blocks and fine-grained control over which tools and context reach the model; reach for LangGraph when your agent does not fit a standard loop or you need to mix deterministic and agentic steps. The key shift in 1.0 is that LangChain stopped being a general toolkit for models, vector stores, and tools, and became a library for building agents. The agent side, which used to live in LangGraph, now has a first-class home in create_agent. The old per-provider packages still exist, but for most work the main langchain package is enough.
The modern way to install LangChain is with uv, the Rust-based Python package manager — though pip works fine if you prefer it. The one thing you must get right is the provider extras. LangChain itself is provider-agnostic, but to actually call a model you need the integration package for that vendor, and you pull it in with square brackets:
uv add "langchain[openai]"
That single line installs langchain plus langchain-openai and the OpenAI client. You do the same for every provider you plan to use — langchain[anthropic], langchain[mistralai], langchain[google-genai]. Then you create a .env file holding your API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, and so on) and load them with python-dotenv. A model string like "gpt-4.1-mini" is auto-recognized as OpenAI — but only if langchain-openai is installed AND an OPENAI_API_KEY is in the environment. Missing either fails at invoke time, not import time.
The fastest way to understand 1.0 is to build a real agent, not a mock. The canonical example is a weather assistant that pings a free, no-account API. The @tool decorator turns a plain function into a tool the agent can call, and create_agent is the central function that wires everything together:
import requests
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.tools import tool
load_dotenv()
@tool
def get_weather(city: str) -> str:
"""Return weather information for a given city."""
response = requests.get(f"https://wttr.in/{city}?format=j1")
return response.json()
agent = create_agent(
model="gpt-4.1-mini",
tools=[get_weather],
system_prompt="You are a helpful weather assistant who always cracks jokes and is humorous while remaining helpful.",
)
response = agent.invoke({
"messages": [{"role": "user", "content": "What is the weather like in Vienna?"}]
})
print(response["messages"][-1].content)
A few things worth noticing. The @tool decorator can take a description so the agent knows what the function does, and a return_direct flag if you want the tool's raw output returned straight to the user instead of being fed back to the model. The agent is invoked with a dictionary containing a messages list — the same role/content structure you know from the raw APIs. The response object holds the entire message history, and response["messages"][-1].content pulls the final answer. Run it and you get a joke-laden Vienna weather report: partly cloudy, about 15°C, with wind and humidity.
You do not always need an agent. Sometimes you just want to talk to a model in an abstract way, and for that 1.0 gives you init_chat_model — one function that initializes any provider's chat model from a model string:
from langchain.chat_models import init_chat_model
model = init_chat_model("gpt-4.1-mini", temperature=0.1)
response = model.invoke("Hello, what is Python?")
print(response.content)
Swap the model string to "mistral-medium" and everything else in the application stays the same — that is the abstraction in action. For multi-turn conversations you can either use the list-and-dictionary notation or import the message classes HumanMessage, AIMessage, and SystemMessage from langchain.messages and build a conversation list. And when responses are long, you can stream them token by token instead of waiting for the whole thing:
for chunk in model.stream("Explain quantum computing."):
print(chunk.content, end="", flush=True)
Streaming turns a long wait into a real-time read-along, which matters for any interactive application.
The real power of 1.0 shows up when you combine three capabilities in one agent: typed context, structured output, and memory. The idea is an agent that knows where you are without you telling it, answers in a fixed shape, and remembers the conversation. Three pieces make that work:
Context holding a user_id), pass it to create_agent, and tools read it through ToolRuntime[Context] — so a locate_user tool can resolve a city from the user's ID without the ID being a tool parameter.ResponseFormat with summary, temperature_celsius, temperature_fahrenheit, humidity) forces the agent into a structured shape. The result comes back in response["structured_response"] — no parsing, no guessing.thread_id in the config. Same thread, the agent remembers; a different thread, it forgets.from dataclasses import dataclass
from langchain.chat_models import init_chat_model
from langchain.tools import tool, ToolRuntime
from langgraph.checkpoint.memory import InMemorySaver
@dataclass
class Context:
user_id: str
@dataclass
class ResponseFormat:
summary: str
temperature_celsius: float
temperature_fahrenheit: float
humidity: float
@tool
def locate_user(runtime: ToolRuntime[Context]) -> str:
"""Look up a user's city based on the context."""
match runtime.context.user_id:
case "ABC123": return "Vienna"
case "XYZ456": return "London"
case _: return "unknown"
model = init_chat_model("gpt-4.1-mini", temperature=0.3)
checkpointer = InMemorySaver()
agent = create_agent(
model=model,
tools=[get_weather, locate_user],
system_prompt="You are a helpful weather assistant.",
context_schema=Context,
response_format=ResponseFormat,
checkpointer=checkpointer,
)
config = {"configurable": {"thread_id": "1"}}
response = agent.invoke(
{"messages": [{"role": "user", "content": "What is the weather like?"}]},
config=config,
context=Context(user_id="ABC123"),
)
print(response["structured_response"].summary)
print(response["structured_response"].temperature_celsius)
The agent resolves the city from the user_id in context, is forced into the ResponseFormat shape, and — because of the checkpointer — remembers the thread. Ask a follow-up with the same thread_id and it knows what you were talking about; change it to "2" and it has no idea. That is short-term, thread-scoped memory, and it is the foundation of every conversational agent.
Agents are not text-only. In 1.0 you can pass images to a model in two ways: by URL, or as base64-encoded bytes from a local file. The message content becomes a list of typed blocks — a text block plus an image block:
message = {
"role": "user",
"content": [
{"type": "text", "text": "Describe the contents of this image."},
{"type": "image", "url": "https://example.com/logo.png"},
],
}
# Or from a local file, base64-encoded:
# {"type": "image", "base64": b64_string, "mime_type": "image/png"}
response = model.invoke([message])
print(response.content)
The same structure works with the HumanMessage class — you just drop the role key and keep the content list. Base64 encoding inflates the payload by about a third, so prefer URLs when the image is publicly reachable; use base64 for local or private files.
Retrieval-augmented generation (RAG) is how you give an agent access to knowledge it was not trained on. The classic path still works in 1.0: embed your documents, store them in a vector store, and turn the store into a retriever the agent can call as a tool. The example that makes the concept click uses a small set of statements about fruits and computers — including the fact that "Apple" is both a fruit and a company:
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.tools import create_retriever_tool
from langchain.agents import create_agent
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
texts = [
"I love apples", "I enjoy oranges", "I think pears taste very good",
"I hate bananas", "I dislike raspberries", "I despise mangoes",
"I love Linux", "I hate Windows",
]
vector_store = FAISS.from_texts(texts, embedding=embeddings)
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
retriever_tool = create_retriever_tool(
retriever,
name="knowledge_base_search",
description="Search the small product/fruit knowledge base for information.",
)
agent = create_agent(
model="gpt-4.1-mini",
tools=[retriever_tool],
system_prompt="You are a helpful assistant. If there are any questions about fruits, use the retriever tool first, retrieve the context, and answer concisely. You may need to use the tool multiple times.",
)
response = agent.invoke({
"messages": [{"role": "user", "content": "What three fruits does the person like and what three fruits does the person dislike?"}]
})
print(response["messages"][-1].content)
The embedding model is what makes this work. When you run a similarity search, the embeddings correctly rank "I love apples" closer to "I enjoy oranges" than to "Apple makes very good computers" — because the concepts differ even though the string matches. The agent then makes two separate similarity-search calls to the tool — one for likes, one for dislikes — and combines them into the answer: "The person likes oranges, apples, and pears. The person dislikes mangoes, raspberries, and bananas." That multi-call behavior is exactly what a single prompt cannot do; it is what makes RAG feel native to an agent rather than bolted on.
Middleware is what makes create_agent genuinely different from every other agent builder. It sits between request and response, and it lets you customize behavior at every step of the agent loop — without touching the agent's core logic. The lifecycle has six named hooks: before_agent, before_model, wrap_tool_call, wrap_model_call, after_model, and after_agent. You can choose a different system prompt, a different model, summarize, rate-limit, or redact PII — all as composable layers.
The @dynamic_prompt decorator rewrites the system prompt from context. The canonical example switches the explanation style based on the user's role — expert, beginner, or child:
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRequest, dynamic_prompt
@dataclass
class Context:
user_role: str
@dynamic_prompt
def user_role_prompt(request: ModelRequest) -> str:
base = "You are a helpful and very concise assistant."
match request.runtime.context.user_role:
case "expert": return base + " Provide detailed technical responses."
case "beginner": return base + " Keep your explanations simple and basic."
case "child": return base + " Explain everything as if you were literally talking to a 5-year-old."
case _: return base
agent = create_agent(
model="gpt-4.1-mini",
middleware=[user_role_prompt],
context_schema=Context,
)
response = agent.invoke(
{"messages": [{"role": "user", "content": "Explain PCA."}]},
context=Context(user_role="child"),
)
print(response["messages"][-1].content)
Ask for PCA as a child and you get: "Imagine you have a big box of crayons with lots of colors. Sometimes you want to choose just a few crayons that can still help you color many pictures nicely. PCA is like a magic helper." The same prompt, three different audiences, zero changes to the agent's core logic.
The wrap_model_call hook lets you pick a different model per request. A common pattern: use a cheap basic model for short conversations, and escalate to a stronger model once the conversation passes a message-count threshold. You read the message count from the request state, override the model, and hand the request to the handler:
from langchain.agents.middleware import ModelRequest, ModelResponse, wrap_model_call
from langchain.chat_models import init_chat_model
basic_model = init_chat_model("gpt-4o-mini")
advanced_model = init_chat_model("gpt-4.1-mini")
@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
message_count = len(request.state.messages)
model = advanced_model if message_count > 3 else basic_model
return handler(request.override(model=model))
agent = create_agent(
model=basic_model,
middleware=[dynamic_model_selection],
)
You can verify which model actually ran by reading response.metadata["model_name"]. This is cost control and quality control in one — the agent spends cheap when the question is easy and spends big when the conversation gets hard.
For full control, subclass AgentMiddleware and override the hook methods directly. A timing middleware that measures the whole agent run is the classic example — override before_agent to record a start time and after_agent to print the elapsed time. This is how you build logging, guardrails, and custom behavior that fires at exactly the right point in the cycle.
LangChain ships several ready-made middlewares for common production patterns:
There are more — tool-call limits, model retry, an LLM tool emulator, and a planning/to-do middleware. The pattern is the point: middleware lets you extend an agent's capabilities without rewriting it, and you can compose several layers at once.
The 1.0 launch was October 22, 2025, and the framework has not stood still. The release line has moved through four minor versions in under a year, each one adding production-grade capability. The headline is langchain v1.4.0 (September 1, 2026), which folds Model Context Protocol support directly into the core package — but the smaller releases matter too, because they quietly changed how you write agents.
The biggest change since 1.0 is that MCP — the open protocol for connecting models to tools and context — now ships inside LangChain itself. The langchain.mcp namespace, built on FastMCP, replaces the standalone langchain-mcp-adapters package. Install it with the mcp extra:
uv add "langchain[mcp]"
The centerpiece is MCPAdapter, which infers the transport from whatever you hand it — a remote URL over streamable HTTP, a local script over stdio, an in-process FastMCP server, or an MCPConfig dict for several servers behind one adapter. You open the adapter, discover its tools, and pass them straight to create_agent
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter
async def main():
async with MCPAdapter("https://example.com/mcp") as adapter:
tools = await adapter.list_tools()
agent = create_agent("claude-sonnet-5", tools)
return await agent.ainvoke({
"messages": [{"role": "user", "content": "..."}]
})
That single adapter replaces a whole class of hand-written tool integrations. It also handles the hard parts of MCP you would otherwise build yourself: authentication (bearer tokens, full OAuth 2.1 with dynamic client registration, or any httpx.Auth), interrupt-driven elicitation (when a server asks for input mid-call, the question surfaces as a LangGraph interrupt() so a human answers and the run resumes), and richer tool metadata — including a destructive_hint annotation you can gate behind human approval. The namespace is still beta (importing it raises a LangChainBetaWarning), but the direction is clear: MCP is the standard way to connect agents to the outside world.
v1.3.0 (May 12, 2026) added version="v3" to stream_events and astream_events for agents — a content-block-centric streaming protocol with typed, per-channel projections, so you can stream text, reasoning, tool calls, and usage separately instead of parsing one undifferentiated stream. v1.2.0 (December 15, 2025) added a new extras attribute on tools for provider-specific parameters — Anthropic's programmatic tool calling and tool search, and built-in tools executed client-side by OpenAI and others — plus strict schema adherence in agent response_format via ProviderStrategy
v1.1.0 (November 25, 2025) was the first feature release after 1.0, and it added two things worth knowing. First, model profiles: every chat model now exposes a .profile attribute describing its capabilities (derived from the open-source models.dev project), which lets the framework infer things like native structured-output support and context-aware summarization triggers. Second, two new prebuilt middlewares: ModelRetryMiddleware for automatically retrying failed model calls with configurable exponential backoff, and an OpenAI content-moderation middleware that checks user input, model output, and tool results for unsafe content. It also added support for passing a SystemMessage instance directly to create_agent's system_prompt parameter, enabling cache control and structured content blocks.
Because create_agent runs on LangGraph, the graph runtime's releases matter too. LangGraph v1.2.0 (May 12, 2026) added DeltaChannel (beta), a channel type that stores only the incremental delta at each step instead of re-serializing the full accumulated value — a real win for long-running threads whose message lists grow large, since it keeps checkpoint sizes small. It also added per-node timeouts (a hard run_timeout, an idle idle_timeout that resets on progress, or both via TimeoutPolicy), node-level error handlers for Saga/compensation patterns, and graceful shutdown via RunControl.request_drain() — stop an in-flight run cooperatively and save a resumable checkpoint. For most create_agent users these are invisible, but they are the difference between a demo and a system you can run in production.
LangChain 1.0 is a genuine reset. It collapsed a sprawling ecosystem into one import path, made create_agent the standard way to build an agent, and introduced middleware as the abstraction that lets you control prompts, models, and guardrails at runtime. The practical recipe for getting started is short: install with uv add "langchain[openai]", load keys from a .env, build a first agent with create_agent and a @tool function, then layer on context, structured output, memory, RAG, and middleware as the need appears. The failure mode 1.0 removes is provider lock-in — and that is the single most valuable thing it does.
Middleware sits between request and response. You can choose a different system prompt, a different model, summarize, rate-limit, or redact PII — all without touching the agent's core logic.
This article is the 10,000-foot view. If you want to actually understand the machinery — how create_agent composes the loop, how middleware hooks fire, how memory and RAG really work — the resources below go deep on each concept. Every link is a primary or authoritative source, verified 2026-09-08.
create_agent abstraction, the middleware hooks, and the package simplification. https://www.langchain.com/blog/langchain-langgraph-1dot0create_agent as the standard builder, the middleware lifecycle, and the simplified namespace. https://docs.langchain.com/oss/python/releases/langchain-v1create_react_agent and per-provider packages to create_agent and middleware. https://docs.langchain.com/oss/python/migrate/langchain-v1create_agent The API reference for the standard agent builder. https://reference.langchain.com/python/langchain/agents/factory/create_agentcreate_deep_agent quickstart and core capabilities. https://docs.langchain.com/oss/python/deepagents/overviewinit_chat_model The unified model-initialization function, including the provider-prefix inference rules and the full provider table. https://reference.langchain.com/python/langchain/chat_models/base/init_chat_modelthread_id works. https://docs.langchain.com/oss/python/langgraph/add-memorythread_id primary key, and how state is persisted. https://docs.langchain.com/oss/python/langgraph/checkpointersInMemorySaver The in-memory checkpointer used for debugging and testing. https://reference.langchain.com/python/langgraph.checkpoint/memory/InMemorySavercreate_retriever_tool The function that wraps a retriever as a tool the agent can call. https://reference.langchain.com/python/langchain-core/tools/create_retriever_toollangchain_community FAISS integration, including from_texts and as_retriever. https://reference.langchain.com/python/langchain-community/vector-storesHumanInTheLoopMiddleware adds approval, edit, and reject decisions to tool calls. https://docs.langchain.com/oss/python/langchain/human-in-the-looplangchain v1.4.0 (MCP), v1.3.0 (v3 streaming), v1.2.0 (tool extras), v1.1.0 (model profiles, model retry, content moderation), and langgraph v1.2.0 (DeltaChannel, per-node timeouts, graceful shutdown). https://docs.langchain.com/oss/python/releases/changeloglangchain.mcp namespace, MCPAdapter, transports, authentication, and tool loading. https://docs.langchain.com/oss/python/langchain/mcp.profile attribute and how model capabilities are inferred. https://docs.langchain.com/oss/python/langchain/models