kmail.at
contact

elsewhere

EnglishEN
← learning

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.

2026-09-08 · 14 min read

$ uv add "langchain[openai]"
Oct 22, 2025
1.0 release

LangChain & LangGraph v1.0 milestone

create_agent
agent builder
init_chat_model
model init
composable
middleware
InMemorySaver
memory

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 API
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)

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 thread
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 "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 call
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 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 count
from 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

  1. 1Install with uv add "langchain[openai]" and load keys from a .env via python-dotenv
  2. 2Build a first agent with create_agent + a @tool function, then invoke with a messages list
  3. 3Add context_schema + ToolRuntime to pass typed runtime data into tools
  4. 4Add response_format for structured output and InMemorySaver + thread_id for memory
  5. 5Turn a FAISS vector store into a retriever tool for RAG, then explore middleware for dynamic prompts and model routing

Related commands