LangChain: the agent framework that killed provider lock-in

LangChain: the agent framework that killed provider lock-in

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.

The ecosystem: Deep Agents, LangChain, LangGraph, and LangSmith

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.

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.

Setting up: uv, provider extras, and API keys

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.

Your first agent: a weather tool

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.

Standalone models, conversations, and streaming

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 advanced agent: context, structured output, and memory

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:

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.

Multimodal input: images by URL or base64

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.

RAG: embeddings, vector stores, and a retriever tool

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: the defining feature of 1.0

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.

Dynamic system prompts

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.

Dynamic model selection

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.

Custom middleware with hooks

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.

Prebuilt middleware

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.

What has shipped since 1.0: the release line so far

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.

v1.4.0: MCP support in the core package

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 and v1.2.0: streaming and tool extras

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: model profiles and new middleware

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.

LangGraph v1.2.0: the runtime underneath

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.

The bottom line

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.

Go deeper

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.

LangChain 1.0 and create_agent

Deep Agents

Models, messages, and streaming

Memory and checkpointing

RAG, embeddings, and vector stores

Middleware

The latest releases

Sources