Building AI Agents with LangChain

A living guide to LangChain, LangGraph, Deep Agents, and LangSmith. Written in plain English.

Version v1.0 Updated 28 Jun 2026 Stack LangChain 1.x ยท LangGraph 1.x Language Python
How to use this doc. The main text explains ideas with light code. Full, copy-paste code lives in the Appendix. This is a living file โ€” we add to the Master topic list as we find better methods.

1. What is an AI agent?

A plain chatbot answers once and stops. An agent works in a loop. It thinks, picks a tool, runs it, looks at the result, then decides the next step. It repeats until the job is done.

The loop has four moving parts:

  • A model โ€” the brain that decides what to do (Claude, GPT, etc).
  • Tools โ€” actions it can take (search, run code, call an API).
  • Memory โ€” what it remembers within a chat and across chats.
  • A harness โ€” the scaffolding around the model that keeps it on track.

LangChain gives you all four. The rest of this guide covers each part, then shows how to ship and watch your agent in production.

2. The four tools in the LangChain family

People say "LangChain" but really mean four things. Here is who does what.

LangChain

The high-level kit. One line builds an agent. Same code works with any model.

LangGraph

The low-level engine under LangChain. Runs the loop, saves state, handles long jobs.

Deep Agents

A ready-made harness for big, multi-step tasks. Planning and sub-agents built in.

LangSmith

The eyes. Traces every step, runs tests, tracks cost. Works with any framework.

Rule of thumb: start with LangChain. Drop into LangGraph when you need custom control. Reach for Deep Agents for long autonomous jobs. Add LangSmith from day one to see what is happening.

Big news (late 2025). LangChain and LangGraph both hit 1.0. The new way to build an agent is create_agent(). It replaces the old AgentExecutor and create_react_agent. Old chains and helpers moved to a separate package called langchain-classic. 1.0 promises no breaking changes until 2.0.

3. Build your first agent

One function does it: create_agent. You give it a model, some tools, and a system prompt. You get back an agent you can call.

from langchain.agents import create_agent

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[search_web, send_email],
    system_prompt="You are a helpful research assistant.",
)

result = agent.invoke({"messages": [{"role":"user","content":"Research AI safety trends"}]})

That is a full agent. It loops: call the model, run any tool the model asks for, feed the result back, repeat until the model gives a final answer. The full runnable version is in the Appendix.

The standard message shape is a list under "messages". Each message has a role (user, assistant, system, tool) and content.

4. Models โ€” Claude, OpenAI, Groq, Ollama

An agent needs a brain. LangChain lets you swap brains without rewriting your app. One helper, init_chat_model, loads any provider.

from langchain.chat_models import init_chat_model

model = init_chat_model("claude-sonnet-4-6", model_provider="anthropic")
# swap one string to change brains โ€” the rest of your code stays the same

Each provider needs its own small package installed.

ProviderGood forInstallprovider name
Anthropic (Claude)Strong reasoning, long context, agentslangchain-anthropicanthropic
OpenAIGeneral use, wide tool supportlangchain-openaiopenai
GroqVery fast, cheap, open modelslangchain-groqgroq
OllamaRuns local, private, free, offlinelangchain-ollamaollama
Why this matters. You can build with cheap fast Groq while testing, run Ollama on your laptop for private data, then switch to Claude for hard tasks. Same agent code throughout.

Content blocks

In 1.0, every model returns the same shape, no matter the provider. You read the answer through response.content_blocks. Each block has a type: text, reasoning, or tool_call. This means provider quirks no longer leak into your code.

5. Tools โ€” giving the agent hands

A tool is just a function the model can call. The fastest way to make one is the @tool decorator. Write a normal Python function with a clear docstring. The docstring tells the model when to use it.

from langchain.tools import tool

@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"It's sunny and 70F in {city}"

Pass tools to the agent in a list: tools=[get_weather, ...]. The model reads the names and docstrings and decides which to call.

MCP โ€” borrow tools from anywhere

MCP (Model Context Protocol) is a shared standard for tools. Many apps now publish MCP servers. With langchain-mcp-adapters you can pull those tools straight into your agent.

  • MultiServerMCPClient connects to one or many MCP servers at once.
  • It turns each MCP tool into a normal LangChain tool with get_tools().
  • You can mix MCP tools and your own @tool functions in the same list.
@tool vs MCP โ€” which? Use @tool for your own logic. Use MCP to plug into outside services without writing wrappers. They work together.

6. Memory โ€” short-term and long-term

Out of the box, an agent forgets everything after each call. LangGraph adds two kinds of memory.

Short-term (a checkpointer)

Remembers this conversation. It saves the message history per thread_id. Reload the same thread and the chat continues. This is also what lets a long job pause and resume.

Options: InMemorySaver (testing), PostgresSaver, RedisSaver, MongoDBSaver (production).

Long-term (a store)

Remembers across conversations. Facts about a user saved under a namespace like ("memories", user_id). Add an embeddings index and you can search memories by meaning.

Options: InMemoryStore (testing), plus Postgres, Redis, and others for production.

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore

checkpointer = InMemorySaver()   # short-term: this chat
store = InMemoryStore()          # long-term: across chats

graph = builder.compile(checkpointer=checkpointer, store=store)

# thread_id keeps each conversation separate
graph.invoke(state, {"configurable": {"thread_id": "user-123"}})
Mental model. Checkpointer = the agent's working memory for one task. Store = its notebook it keeps forever.

7. Middleware โ€” the new control layer

Middleware is the headline feature of LangChain 1.0. It lets you hook into the agent loop and change behavior at each step โ€” without rewriting the agent. Think of it as plugins that wrap the model call.

LangChain ships ready-made middleware:

MiddlewareWhat it does
PIIMiddlewareFinds and redacts or blocks private data (emails, phone numbers).
SummarizationMiddlewareShrinks long chat history so you stay under the context limit.
HumanInTheLoopMiddlewarePauses for human approval before risky tool calls.
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware, SummarizationMiddleware

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[read_email, send_email],
    middleware=[
        PIIMiddleware("email", strategy="redact"),
        SummarizationMiddleware(model="claude-sonnet-4-6", trigger={"tokens": 500}),
    ],
)

You can also write your own by subclassing AgentMiddleware and hooking wrap_model_call โ€” for example, pick a bigger model for expert users. Full example in the Appendix.

7b. Structured output & validation

Often you want clean data back, not a paragraph. Set response_format on the agent and the result lands in result["structured_response"]. You define the shape with a schema (a Pydantic model gives real validation).

There are two strategies. LangChain auto-picks based on the model:

StrategyHowWhen
ProviderStrategyProvider's native structured output.Most reliable. OpenAI, Claude, xAI, Gemini.
ToolStrategyUses tool calling to force the shape.Any model that supports tool calling.

Tool-calling models (ToolStrategy)

This is the path for any model that can call tools. The field rules are what get checked.

from pydantic import BaseModel, Field
from typing import Literal
from langchain.agents.structured_output import ToolStrategy

class ProductReview(BaseModel):
    rating: int | None = Field(description="1-5", ge=1, le=5)
    sentiment: Literal["positive", "negative"]
    key_points: list[str]

agent = create_agent("claude-sonnet-4-6", tools=[], response_format=ToolStrategy(ProductReview))
result["structured_response"]  # ProductReview(rating=5, sentiment='positive', ...)

Validating model outputs (auto-retry)

Models slip up. With ToolStrategy, a bad value fails Pydantic validation, the agent feeds the exact error back, and the model retries. If the rule is 1-5 and the model returns 10, it gets corrected. You steer this with handle_errors: True (default retry), a fixed string, an exception type or tuple, a custom function, or False (raise).

ToolStrategy(schema=ProductReview, handle_errors="Give a rating from 1 to 5 and a short comment.")
Rule of thumb. Put your real rules in the schema (ge, le, Literal, required fields). That is your validation. handle_errors just decides what happens when a rule breaks.

Validating tool calls & tool outputs

Two layers. First, the tool's function signature validates the args the model passes, so bad inputs are rejected before your code runs. Second, wrap tool calls so a failure becomes a message the model can recover from instead of crashing the agent (use wrap_tool_call middleware). Full code in the Appendix (J and K).

8. Observability โ€” see inside your agent

Agents are loops with many hidden steps. When one goes wrong, you need to see every step. That is observability. The main tool is LangSmith.

Tracing

A trace records every model call, tool call, and decision, with timing, tokens, and cost. If you use LangChain, tracing turns on with two environment variables. No code change.

# set these and traces appear in LangSmith automatically
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=your-key

LangSmith is framework-agnostic. It has SDKs for Python, TypeScript, Go, and Java, so it can watch agents built outside LangChain too. (You can also self-host or use open tools like Langfuse if you prefer.)

Evaluation (evals)

Evals are tests for agents. Because output is not fixed, you score it instead of checking for an exact match.

  • Offline evals โ€” run your agent over a saved dataset of examples and score the results.
  • Online evals โ€” grade real production traffic as it happens, often with an "LLM-as-judge".
  • Trajectory evals โ€” score the path the agent took (its tool calls and steps), not just the final answer.

LangSmith plugs into pytest, Vitest, and GitHub Actions. You can fail a build when scores drop โ€” the same discipline as normal unit tests.

What changed in 2026

LangSmith grew from a tracing tool into a full agent platform: unified cost tracking across the whole workflow (model + retrieval + tools + external APIs), annotation queues for human review, and "Fleet" for running many agents.

9. Harnesses & Deep Agents

A harness is everything wrapped around the model: the tools, the guardrails, the feedback loops, the context rules, and the observability. The big lesson of 2026: a great model still fails without a good harness. The focus moved from the model to the scaffolding around it.

Deep Agents is LangChain's ready-made harness for long, complex, autonomous jobs. Install with pip install deepagents and build with create_deep_agent. It bundles four things a one-shot agent lacks:

  • A planning tool (write_todos) โ€” the agent keeps a visible to-do list. This simple habit greatly improves focus on long tasks.
  • A virtual filesystem โ€” ls, read_file, write_file, edit_file. The agent offloads notes and work products to "files" instead of cramming everything into context.
  • Sub-agents โ€” a task tool spawns fresh helper agents for isolated jobs. Each gets clean context and returns one summary. This keeps the main context tidy.
  • A tuned main loop โ€” sensible prompts and context handling out of the box.
from deepagents import create_deep_agent

agent = create_deep_agent(
    model="claude-sonnet-4-6",
    tools=[internet_search],
    system_prompt="You are an expert researcher.",
)
When to use it. Use a plain create_agent for quick tasks. Use Deep Agents when the job is long, multi-stage, and the agent must plan and work in steps (deep research, coding, multi-doc analysis).

10. Multi-agent patterns

Sometimes one agent is not enough. You split the work across specialists. Two common shapes, both built on LangGraph:

PatternHow it worksLibrary
SupervisorOne boss agent routes work to specialist agents and collects results.langgraph-supervisor
SwarmAgents hand off to each other directly as the task shifts. No central boss.langgraph-swarm

Deep Agents' sub-agents are a third, simpler option: one main agent spins up helpers on demand.

11. Human-in-the-loop

For risky actions (sending money, deleting data, emailing customers), you want a human to approve first. LangGraph supports this with interrupts: the agent pauses, saves its state via the checkpointer, waits for a human, then resumes from the exact same spot.

The easy path is HumanInTheLoopMiddleware. You list which tools need approval and the allowed choices (approve, edit, reject).

from langchain.agents.middleware import HumanInTheLoopMiddleware

HumanInTheLoopMiddleware(
    interrupt_on={"send_email": {"allowed_decisions": ["approve", "edit", "reject"]}}
)
Needs a checkpointer. Pausing and resuming only works if short-term memory is on. See Memory.

12. RAG / retrieval

RAG (retrieval-augmented generation) feeds the model your own documents so it answers from real facts, not guesses. The flow:

  1. Load your docs (PDFs, web pages, databases).
  2. Split them into chunks.
  3. Embed each chunk into a vector with init_embeddings.
  4. Store the vectors in a vector store (Postgres/pgvector, Chroma, etc).
  5. Retrieve the closest chunks at query time and pass them to the model.

In agent style, you wrap retrieval as a tool. The agent decides when to search your docs, rather than searching on every turn. This is more flexible than a fixed RAG chain.

13. Deployment

A notebook agent is not a product. To ship, you need an always-on server, saved state, and streaming. Options:

  • LangGraph Platform โ€” LangChain's managed service. Gives you an API endpoint, persistence, streaming, and scaling for your graph.
  • Self-host โ€” wrap your agent in FastAPI with a Postgres checkpointer. More control, more work.
  • Deep Agents Deploy โ€” a path for shipping Deep Agents specifically (announced 2026).

Whatever you pick: turn on LangSmith tracing, use a real database checkpointer (not in-memory), and gate risky tools behind human approval.

14. Master topic list (the living roadmap)

Everything worth covering, in one place. Covered is written above. Planned is next. Idea is a candidate to add later. We grow this list as the field moves.

TopicWhat it coversStatus
The agent loopThink โ†’ act โ†’ observe โ†’ repeatCovered
LangChain / LangGraph / Deep Agents / LangSmithWho does whatCovered
create_agentThe 1.0 way to build agentsCovered
ModelsClaude, OpenAI, Groq, Ollama via init_chat_modelCovered
Content blocksOne output shape for all providersCovered
Tools@tool functionsCovered
MCP toolslangchain-mcp-adapters, multi-server clientCovered
Short-term memoryCheckpointers, threads, resumeCovered
Long-term memoryStores, namespaces, semantic searchCovered
MiddlewarePII, summarization, custom hooksCovered
Structured outputToolStrategy, ProviderStrategy, PydanticCovered
Validation & error handlinghandle_errors retries, wrap_tool_call, schema rulesCovered
ObservabilityLangSmith tracesCovered
EvalsOffline, online, trajectory, CI gatesCovered
HarnessesWhy scaffolding beats raw model powerCovered
Deep AgentsPlanning, virtual FS, sub-agentsCovered
Multi-agentSupervisor, swarmCovered
Human-in-the-loopInterrupts and approvalsCovered
RAG / retrievalEmbeddings, vector stores, retriever-as-toolCovered
DeploymentLangGraph Platform, self-hostCovered
StreamingToken and step streaming to a UIPlanned
Context engineeringWhat goes in / stays / leaves the context windowPlanned
Skills (SKILL.md)Packaged know-how for Deep AgentsPlanned
Sandboxes & code executionSafe REPL / shell for agentsPlanned
SubgraphsComposing graphs inside graphsPlanned
Cost & rate-limit controlBudgets, caching, fallbacksPlanned
Guardrails & safetyInput/output filters, prompt-injection defenseIdea
Testing fixturesMock models and tools for fast testsIdea
CachingPrompt and embedding cachesIdea
Auth & multi-tenantPer-user keys, isolation, quotasIdea
Voice / multimodal agentsAudio and image in the loopIdea
LangGraph StudioVisual debugging of graphsIdea

15. Code appendix โ€” runnable snippets

Copy-paste examples. Set your API keys as environment variables first (e.g. ANTHROPIC_API_KEY).

A. Full first agent
# pip install -U langchain langchain-anthropic
from langchain.agents import create_agent
from langchain.tools import tool

@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"It's sunny and 70F in {city}."

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[get_weather],
    system_prompt="You are a concise assistant.",
)

result = agent.invoke({"messages": [
    {"role": "user", "content": "What's the weather in Paris?"}
]})
print(result["messages"][-1].content)
B. Swap models โ€” Claude, OpenAI, Groq, Ollama
from langchain.chat_models import init_chat_model

# pip install langchain-anthropic
claude = init_chat_model("claude-sonnet-4-6", model_provider="anthropic")

# pip install langchain-openai
gpt = init_chat_model("gpt-5.4-mini", model_provider="openai")

# pip install langchain-groq  (very fast)
groq = init_chat_model("llama-3.3-70b-versatile", model_provider="groq")

# pip install langchain-ollama  (local; run `ollama pull llama3.1` first)
local = init_chat_model("llama3.1", model_provider="ollama")

for m in (claude, gpt, groq, local):
    print(m.invoke("Say hi in 3 words.").content)
C. Memory โ€” short-term and long-term
# pip install -U langchain langgraph
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[],
    checkpointer=InMemorySaver(),     # short-term memory
)

cfg = {"configurable": {"thread_id": "chat-1"}}
agent.invoke({"messages": [{"role":"user","content":"My name is Nitish."}]}, cfg)
out = agent.invoke({"messages": [{"role":"user","content":"What's my name?"}]}, cfg)
print(out["messages"][-1].content)   # -> Nitish

# For production, swap InMemorySaver for PostgresSaver / RedisSaver.
D. Custom middleware โ€” pick model by user level
from dataclasses import dataclass
from typing import Callable
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest
from langchain.agents.middleware.types import ModelResponse

@dataclass
class Context:
    user_expertise: str = "beginner"

class ExpertiseMiddleware(AgentMiddleware):
    def wrap_model_call(self, request: ModelRequest,
                        handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse:
        if request.runtime.context.user_expertise == "expert":
            model = ChatOpenAI(model="gpt-5.4")
        else:
            model = ChatOpenAI(model="gpt-5-nano")
        return handler(request.override(model=model))

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[],
    middleware=[ExpertiseMiddleware()],
    context_schema=Context,
)
E. Structured output (typed answers)
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from pydantic import BaseModel

class Weather(BaseModel):
    temperature: float
    condition: str

@tool
def weather_tool(city: str) -> str:
    """Get the weather for a city."""
    return f"sunny and 70 degrees in {city}"

agent = create_agent(
    "claude-sonnet-4-6",
    tools=[weather_tool],
    response_format=ToolStrategy(Weather),
)
res = agent.invoke({"messages": [{"role":"user","content":"Weather in SF?"}]})
print(res["structured_response"])   # Weather(temperature=70.0, condition='sunny')
F. MCP tools via MultiServerMCPClient
# pip install langchain-mcp-adapters
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

async def main():
    client = MultiServerMCPClient({
        "math": {"command": "python", "args": ["math_server.py"], "transport": "stdio"},
        "docs": {"url": "http://localhost:8000/mcp", "transport": "streamable_http"},
    })
    tools = await client.get_tools()
    agent = create_agent(model="claude-sonnet-4-6", tools=tools)
    out = await agent.ainvoke({"messages": [{"role":"user","content":"What is 12 * 9?"}]})
    print(out["messages"][-1].content)

asyncio.run(main())
G. Deep Agent (the harness)
# pip install deepagents
from deepagents import create_deep_agent

def internet_search(query: str) -> str:
    """Search the web for a query."""
    return "...results..."

agent = create_deep_agent(
    model="claude-sonnet-4-6",
    tools=[internet_search],
    system_prompt="You are an expert researcher. Plan, then work in steps.",
)

# It comes with write_todos, a virtual filesystem, and sub-agents built in.
result = agent.invoke({"messages": [
    {"role":"user","content":"Write a short report on AI agent harnesses."}
]})
print(result["messages"][-1].content)
H. Turn on LangSmith tracing
# pip install -U langsmith
# Set these in your shell or .env โ€” no code change needed:
#   export LANGSMITH_TRACING=true
#   export LANGSMITH_API_KEY=ls-...

from langchain.agents import create_agent

agent = create_agent(model="claude-sonnet-4-6", tools=[])
agent.invoke({"messages": [{"role":"user","content":"hello"}]})
# Open smith.langchain.com to see the full trace, tokens, and cost.
I. Structured output โ€” tool-calling models (ToolStrategy)
from pydantic import BaseModel, Field
from typing import Literal
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy

class ProductReview(BaseModel):
    """Analysis of a product review."""
    rating: int | None = Field(description="1-5", ge=1, le=5)
    sentiment: Literal["positive", "negative"]
    key_points: list[str]

# ToolStrategy works with any model that supports tool calling.
agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[],
    response_format=ToolStrategy(ProductReview),
)
result = agent.invoke({"messages": [
    {"role":"user","content":"Analyze: Great, 5/5, fast shipping but pricey"}
]})
print(result["structured_response"])
# ProductReview(rating=5, sentiment='positive', key_points=['fast shipping','expensive'])

# Native path (OpenAI/Claude/xAI/Gemini): just pass the schema type.
# agent = create_agent("gpt-5.5", response_format=ProductReview)  # auto ProviderStrategy
J. Validate model outputs (auto-retry on bad values)
from pydantic import BaseModel, Field
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy

class ProductRating(BaseModel):
    rating: int | None = Field(description="Rating from 1-5", ge=1, le=5)
    comment: str

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[],
    response_format=ToolStrategy(
        schema=ProductRating,
        # True (default): retry with default message.  "text": fixed retry message.
        # ValueError / (A, B): retry only those.  a function: custom.  False: raise.
        handle_errors="Give a rating from 1 to 5 and a short comment.",
    ),
)
# If the model returns rating=10, Pydantic rejects it, the agent shows the
# error to the model, and it retries until the value is valid (or gives up).
res = agent.invoke({"messages": [{"role":"user","content":"Parse: Amazing, 10/10!"}]})
print(res["structured_response"])  # rating clamped to a valid 1-5
K. Validate tool calls & tool outputs
from collections.abc import Callable
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from langchain.messages import ToolMessage
from langchain.tools import tool
from langchain.tools.tool_node import ToolCallRequest

# 1) Tool INPUTS are validated by the function signature / type hints.
@tool
def divide(a: float, b: float) -> float:
    """Divide a by b."""
    return a / b

# 2) Wrap tool calls so an error becomes a message the model can recover from.
@wrap_tool_call
def handle_tool_errors(request: ToolCallRequest,
                       handler: Callable[[ToolCallRequest], ToolMessage]) -> ToolMessage:
    try:
        return handler(request)
    except Exception as e:
        return ToolMessage(
            content=f"Tool error, please fix your input and retry. ({e})",
            tool_call_id=request.tool_call["id"],
        )

agent = create_agent(
    model="claude-sonnet-4-6",
    tools=[divide],
    middleware=[handle_tool_errors],
)

16. Sources

Official docs first, then background reading. Check the official docs for the newest API details.