Home

Chapter 2 · Lesson 0017

LangChain — the agent framework

30–40 min read · Hands-on coding

Motivation

Every AI engineering job posting lists LangChain. It is the most widely adopted framework for building LLM applications, with an ecosystem of hundreds of integrations — and a reputation, earned a few years ago, for being needlessly complex. That reputation is increasingly out of date. LangChain rebuilt its core agent API in late 2025 (version 1.0), replacing the tangle of chain classes that earned the complaints with a single function: create_agent. This lesson teaches the framework as it exists today, not as it existed in 2023 tutorials you’ll still find online.

Prerequisites

  • Lessons 11–14: you built pipelines, tool calling, agents, and routers from scratch — this lesson shows what a framework does differently
  • Lesson 16: PydanticAI — you will compare LangChain against it directly
  • Install: uv add langchain langchain-openai

What LangChain is in 2026 — and how it relates to LangGraph and CrewAI

LangChain is a Python (and JavaScript) library that provides ready-made abstractions for building LLM applications: a standard interface across model providers, a tool-calling system, memory, and — as of version 1.0 — a built-in agent loop you don’t have to write yourself.

Before going further, it’s worth being precise about a question that confuses a lot of learners: is LangChain an alternative to LangGraph, which you cover in the next lesson? No. According to LangChain’s own documentation, the ecosystem splits into three tiers, and LangChain and LangGraph occupy different ones:

Tier What it gives you Examples
Framework Abstractions and integrations — fast to start, opinionated defaults LangChain, CrewAI, PydanticAI
Runtime Durable execution, persistence, low-level control LangGraph, Temporal
Harness Opinionated, batteries-included autonomous agents Deep Agents SDK, Claude Agent SDK

The key fact: LangChain 1.0 is built on top of LangGraph. When you call create_agent, LangGraph’s execution engine is running underneath, managing the loop of “call the model, check for tool calls, run them, repeat.” You don’t need to know anything about LangGraph to use LangChain — that’s the point of a framework, it’s a layer of abstraction over the runtime. But when your application outgrows what create_agent gives you by default — when you need fine-grained control over branching, long-running durable workflows, or custom state — you drop down to LangGraph directly, which is exactly what the next lesson covers.

CrewAI, covered two lessons from now, sits at the same tier as LangChain: it’s a framework, just one specialised for a different shape of problem — a team of role-playing agents rather than a single agent with tools.

What LangChain is good at

Where LangChain still adds friction

Verdict: reach for LangChain when you want a working agent fast and your workflow is a single agent calling tools in a loop. Reach for LangGraph (next lesson) the moment you need explicit control over branching, state, or long-running execution.

Building your first LangChain agent with create_agent

create_agent takes a model and a list of tools, and returns a working agent — no manual loop required:

from langchain.agents import create_agent

def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"It's sunny and 22°C in {city}."

agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[get_weather],
    system_prompt="You are a helpful assistant. Be concise.",
)

result = agent.invoke({
    "messages": [{"role": "user", "content": "What's the weather in Lisbon?"}]
})
print(result["messages"][-1].content)

The model argument takes a "provider:model-name" string — create_agent resolves the provider and credentials for you, reading the relevant API key from your environment (OPENAI_API_KEY, ANTHROPIC_API_KEY, and so on). tools is a list of plain Python functions; a clear docstring on each one becomes part of what the model reads to decide when to call it.

Internally, create_agent builds exactly the loop you implemented by hand in Lesson 15 — call the model, check whether it asked to call a tool, run the tool, feed the result back, repeat until the model returns a final answer. The difference is that loop, plus error handling, retries, and streaming support, now ships as one function call.

Tools: the @tool decorator and ToolRuntime

The @tool decorator turns a typed Python function into something the agent can call, using the function’s type hints and docstring to build the schema the model sees:

from langchain.tools import tool

@tool
def search_orders(customer_id: str, status: str = "all") -> str:
    """Look up a customer's orders, optionally filtered by status.

    Args:
        customer_id: The customer's unique ID
        status: Filter by order status — "pending", "shipped", or "all"
    """
    return f"3 orders found for {customer_id} with status={status}"

Type hints are required — they define the schema the model uses to decide what arguments to pass. Most tools you write will look exactly like this.

Some tools need more than their declared arguments — they need to read the conversation so far, know who the current user is, or write to persistent storage. For that, add a ToolRuntime parameter. It’s automatically injected and invisible to the model — the LLM never sees it in the tool’s schema, so it can’t (and doesn’t need to) try to fill it in:

from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime

@dataclass
class UserContext:
    user_id: str

@tool
def get_account_balance(runtime: ToolRuntime[UserContext]) -> str:
    """Get the current user's account balance."""
    user_id = runtime.context.user_id
    return f"Balance for {user_id}: $1,204.50"

runtime.context holds immutable per-run data you pass in at invocation time (like a user ID). runtime.state holds the conversation’s short-term memory. runtime.store holds long-term memory that survives across conversations — covered next.

Middleware — customizing the agent loop

Middleware is LangChain 1.0’s answer to “I need to change how the agent loop behaves, without rewriting the loop.” It works like middleware in a web framework — Express or Django — intercepting a request on the way in and the response on the way out. In an agent, the “request” is a call to the model, and middleware can hook in at three points:

LangChain ships prebuilt middleware for common needs — summarizing long conversation history, requiring human approval before a tool runs, retrying a failed tool call automatically. You attach middleware as a list:

from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware

agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[get_weather],
    middleware=[
        SummarizationMiddleware(
            model="openai:gpt-4o-mini",
            max_tokens_before_summary=4000,
        ),
    ],
)

This agent automatically compresses old conversation history into a summary once the thread passes 4,000 tokens, instead of letting the context window fill up and the model start “forgetting” early messages. Without middleware, you’d have to write that truncation logic yourself, inside your own loop — which is exactly the kind of hand-rolled plumbing a framework exists to remove.

Memory: short-term and long-term

An agent with no memory forgets everything the moment invoke returns. LangChain distinguishes two kinds of memory, matching the State vs. Store distinction from ToolRuntime above:

Short-term memory (a checkpointer) persists the conversation within one thread — the back-and-forth of a single chat session:

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="openai:gpt-4o-mini",
    tools=[get_weather],
    checkpointer=InMemorySaver(),
)

config = {"configurable": {"thread_id": "session-1"}}

agent.invoke({"messages": [{"role": "user", "content": "My name is Priya."}]}, config)
result = agent.invoke({"messages": [{"role": "user", "content": "What's my name?"}]}, config)
print(result["messages"][-1].content)  # "Your name is Priya."

The thread_id is what ties two separate invoke calls into one remembered conversation. InMemorySaver is for development; production systems use a database-backed checkpointer instead, so a thread survives a server restart.

Long-term memory (a store) persists data across threads — a user’s preferences remembered weeks later, in a different conversation entirely. Tools read and write it through runtime.store, as shown in the previous section. Use short-term memory for “remember this conversation”; use long-term memory for “remember this user.”

LCEL: the older composition style

Before create_agent existed, LangChain’s main composition tool was LCEL (LangChain Expression Language) — chaining components with the | (pipe) operator:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_messages([("user", "Summarise in one sentence: {text}")])

chain = prompt | llm | StrOutputParser()
result = chain.invoke({"text": "A long article about vector databases..."})

You’ll still see LCEL in older tutorials, existing codebases, and anywhere you need a simple, fixed, linear pipeline with no tool calls or branching — it’s still a fine choice for that narrow case. But for anything agentic — a model deciding what to do next, calling tools, looping — create_agent is the current, idiomatic way to build it. Don’t reach for LCEL chains to build an agent; that’s what the rest of this lesson is for.

When to use LangChain vs PydanticAI vs raw API

Situation Recommendation
Need a working tool-using agent fast LangChain — create_agent in under 10 lines
Strict typed input/output validation matters most PydanticAI (Lesson 16) — typed by design
Need total control over every token, or maximum debuggability Raw API (Lesson 15)
Need branching, durable execution, or fine-grained state control LangGraph (next lesson) — what’s running underneath LangChain anyway
Simple linear pipeline, no tools, no branching LCEL, or even just a direct API call
Career note: LangChain remains the framework most commonly named in 2026 AI Engineering job postings. Knowing the current create_agent + middleware model — not the 2023-era chain classes — is what separates a candidate who's kept current from one repeating outdated tutorials.

Assignment

  1. Read: Agent Middleware by LangChain blog — about 12 minutes. The team that designed LangChain 1.0's middleware system explains why they built it this way — the same problem you'll hit the moment your agent needs anything beyond 'call a tool, get an answer'. Written for engineers who already understand the basic agent loop, which you do after Lesson 15.
  2. Do: Rebuild the self-critique agent from Lesson 15 using LangChain’s create_agent.
    1. Define a self_critique tool with @tool that sends a draft to a second model call and returns a score (1–10) and feedback, same behaviour as your Lesson 15 version
    2. Create the agent with create_agent(model=..., tools=[self_critique], system_prompt=...), instructing it to call self_critique before giving a final answer and only accept a score ≥ 7
    3. Add an InMemorySaver checkpointer and run the same question twice in the same thread — second time, ask it to recall what it answered first
    4. Compare line count and readability against your Lesson 15 from-scratch version

    Expected output: A working LangChain agent that self-critiques its draft and remembers the prior turn within a thread — in roughly a third of the code from Lesson 15. Rebuilding something you already hand-wrote is the fastest way to see exactly what a framework buys you — and what it hides from you.

Knowledge Check

Answer each question from memory, then click to reveal.

Additional Resources