langchain · difficulty ◆◆
LangChain 1.0 Masterclass — agents, tools, memory, RAG & middleware
One framework, one import path, every provider — build a real agent from a weather tool to a RAG retriever to custom middleware.
Swap OpenAI for Anthropic tomorrow and your agent code barely changes — that is the whole point of LangChain 1.0.
AI-generated draft · reviewed and editedThis text was generated with AI as a first draft, then reviewed and edited by the author, who holds editorial responsibility for it.$ uv add "langchain[openai]"- Oct 22, 2025
- 1.0 release
- create_agent
- agent builder
- init_chat_model
- model init
- composable
- middleware
- InMemorySaver
- memory
LangChain & LangGraph v1.0 milestone
What it is
LangChain 1.0 is a Python framework for building and working with AI agents. Its core promise is abstraction: regardless of whether you call OpenAI, Anthropic, Mistral, or Google, you get the same classes and methods. A system built on LangChain that uses vector stores, embedding models, chat models, and agents can swap the underlying provider and keep the code almost identical. In 1.0 the framework pivoted hard toward being an agent library — create_agent is now the standard entry point, and middleware is the defining feature.
Why it matters
Before 1.0, building an agent meant juggling langchain-core, langchain-community, and per-provider packages with separate import paths (from langchain_core import ..., from langchain_openai import ...). The agent side lived in LangGraph. In 1.0 everything funnels through the main langchain package: from langchain.agents import create_agent, from langchain.chat_models import init_chat_model, from langchain.tools import tool. You still can use langchain-core and langchain-community, but for most work the main package is enough. The failure mode 1.0 removes is provider lock-in: your retry logic, your tool wiring, and your memory setup stop caring which vendor backs the call.
Example
$ A minimal agent with one tool — a weather lookup that pings a free APIimport 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)Expected shape of the output: a joke-laden weather report for Vienna, built from the wttr.in JSON the tool fetches. Treat the wording as illustrative rather than as a transcript - it comes from a live model call and varies by model and temperature. The model string "gpt-4.1-mini" is auto-recognized as OpenAI (verified: init_chat_model returns a ChatOpenAI instance), so langchain-openai must be installed and an OPENAI_API_KEY present, and an unrecognised model string fails with "Unable to infer model provider".
$ Structured output + context + memory — the agent locates the user, returns a typed response, and remembers the threadfrom 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 "HJKL111": return "Paris"
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)Expected output: a one-line summary such as "The current weather in Vienna is partly cloudy" followed by the temperature. The exact numbers depend on a live model call and on the weather, so read them as illustrative. What the example is really demonstrating is the wiring: the agent resolves the city from the user_id in context, is forced into the ResponseFormat shape, and - because of the InMemorySaver checkpointer - remembers the thread. Change thread_id to "2" and it forgets the earlier conversation.
$ RAG as a tool — turn a vector store into a retriever the agent can callfrom 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 agent is expected to make two separate similarity searches, one for the likes and one for the dislikes, and combine them - which is the point of exposing retrieval as a tool instead of a fixed step in the code. The wording of the answer above is illustrative: reproducing it needs embedding and chat keys. This is the old-school RAG path: langchain-community + FAISS + create_retriever_tool, all three still importable in 1.x.
$ Middleware — swap the system prompt by user role, and swap the model by message countfrom dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRequest, ModelResponse, dynamic_prompt, wrap_model_call
from langchain.chat_models import init_chat_model
@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)Output for user_role="child" is illustrative - a live model call produces it, and the wording changes run to run. The mechanism is the point: @dynamic_prompt reads the role from request.runtime.context and rewrites the system prompt before the model is called. The same middleware pattern - wrap_model_call - lets you pick a stronger model when the message count crosses a threshold.
Common flags
- create_agent
- The standard 1.0 way to build an agent; takes model, tools, system_prompt, context_schema, response_format, checkpointer, middleware
- init_chat_model
- One function to initialize any provider's chat model from a model string
- @tool
- Decorator that turns a plain function into a tool the agent can call
- ToolRuntime[Context]
- Gives a tool access to runtime context (e.g. user_id) without passing it as a parameter
- response_format
- Forces the agent to answer in a structured dataclass/pydantic shape
- InMemorySaver
- LangGraph checkpointer that remembers conversation history per thread_id
- create_retriever_tool
- Wraps a vector-store retriever as a tool the agent can call for RAG
- middleware
- Composable hooks (dynamic_prompt, wrap_model_call, before/after_agent) that sit between request and response
History
Origin
LangChain 1.0 shipped stable on October 22, 2025, after a run of alphas from late August through late September that year. (The packages themselves reached PyPI on October 17, half an hour apart for LangChain and LangGraph, and were announced as a coordinated milestone on the 22nd.) The 1.0 milestone reframed LangChain as an agent framework: create_agent became the standard builder, middleware became the defining abstraction, and the package surface collapsed into the main langchain package. This tutorial follows NeuralNine's "LangChain Full Crash Course - AI Agents in Python" (published on his channel), which walks the same 1.0 API end to end.
The 1.0 pivot
Before 1.0, LangChain was a general toolkit for models, vector stores, and tools, while the agent side lived in LangGraph. In 1.0 the framework leaned into agents: create_agent is built on top of LangGraph, and middleware gives you dynamic prompts, model selection, summarization, human-in-the-loop, rate limits, and PII redaction as composable layers. The old per-provider import paths (langchain_core, langchain_openai) still exist, but the modern path is one import from langchain.
Under the hood
How create_agent composes everything
create_agent is the single entry point that wires model, tools, system prompt, context, structured output, memory, and middleware into one runnable agent. It is implemented on top of LangGraph, so under the hood you get a graph with nodes for the model and tool calls. The context_schema lets you pass typed runtime data (like a user_id) that tools read through ToolRuntime — so a tool can locate a user without the user_id being a tool parameter. The response_format forces the model into a structured dataclass, and the checkpointer (InMemorySaver) persists message history keyed by thread_id.
The middleware lifecycle
Middleware hooks fire at defined points in the agent cycle: before_agent, before_model, wrap_tool_call, wrap_model_call, after_model, after_agent. The dynamic_prompt decorator rewrites the system prompt from context; wrap_model_call lets you override the model per request (e.g. use a stronger model once a conversation passes N messages). You can also subclass AgentMiddleware and override these hooks directly for custom logic — timing, logging, guardrails. Prebuilt middleware covers summarization, human-in-the-loop, model/tool call limits, model fallback, and PII redaction.
RAG the old-school way still works
For retrieval you can still use the classic path: OpenAIEmbeddings + FAISS.from_texts to build a vector store, then vector_store.as_retriever() and create_retriever_tool to expose it as a tool. The agent can call that tool multiple times in one run — retrieve likes, retrieve dislikes, then combine — which is exactly what a single prompt can't do. This is the pattern that makes RAG feel native to an agent rather than bolted on.
Fun facts
Pros & cons
pros
- + One import path and one API across every provider
- + create_agent composes tools, context, memory, and middleware in one call
- + Middleware makes dynamic prompts, model routing, and guardrails trivial
- + Structured output via response_format is first-class
- + RAG slots in as just another tool
cons
- − 1.0 is a breaking change from the pre-1.0 langchain-core/community split
- − The old-school RAG path still needs langchain-community + FAISS installed separately
- − Middleware lifecycle (before/wrap/after hooks) has a learning curve
- − Provider extras must be installed per vendor, and API keys are required per provider
Takeaways
- 1Install with uv add "langchain[openai]" and load keys from a .env via python-dotenv
- 2Build a first agent with create_agent + a @tool function, then invoke with a messages list
- 3Add context_schema + ToolRuntime to pass typed runtime data into tools
- 4Add response_format for structured output and InMemorySaver + thread_id for memory
- 5Turn a FAISS vector store into a retriever tool for RAG, then explore middleware for dynamic prompts and model routing
