Building AI Agents with LangChain
A living guide to LangChain, LangGraph, Deep Agents, and LangSmith. Written in plain English.
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.
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.
| Provider | Good for | Install | provider name |
|---|---|---|---|
| Anthropic (Claude) | Strong reasoning, long context, agents | langchain-anthropic | anthropic |
| OpenAI | General use, wide tool support | langchain-openai | openai |
| Groq | Very fast, cheap, open models | langchain-groq | groq |
| Ollama | Runs local, private, free, offline | langchain-ollama | ollama |
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.
MultiServerMCPClientconnects 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
@toolfunctions in the same list.
@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"}})
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:
| Middleware | What it does |
|---|---|
PIIMiddleware | Finds and redacts or blocks private data (emails, phone numbers). |
SummarizationMiddleware | Shrinks long chat history so you stay under the context limit. |
HumanInTheLoopMiddleware | Pauses 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:
| Strategy | How | When |
|---|---|---|
ProviderStrategy | Provider's native structured output. | Most reliable. OpenAI, Claude, xAI, Gemini. |
ToolStrategy | Uses 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.")
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
tasktool 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.",
)
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:
| Pattern | How it works | Library |
|---|---|---|
| Supervisor | One boss agent routes work to specialist agents and collects results. | langgraph-supervisor |
| Swarm | Agents 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"]}}
)
12. RAG / retrieval
RAG (retrieval-augmented generation) feeds the model your own documents so it answers from real facts, not guesses. The flow:
- Load your docs (PDFs, web pages, databases).
- Split them into chunks.
- Embed each chunk into a vector with
init_embeddings. - Store the vectors in a vector store (Postgres/pgvector, Chroma, etc).
- 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.
| Topic | What it covers | Status |
|---|---|---|
| The agent loop | Think โ act โ observe โ repeat | Covered |
| LangChain / LangGraph / Deep Agents / LangSmith | Who does what | Covered |
create_agent | The 1.0 way to build agents | Covered |
| Models | Claude, OpenAI, Groq, Ollama via init_chat_model | Covered |
| Content blocks | One output shape for all providers | Covered |
| Tools | @tool functions | Covered |
| MCP tools | langchain-mcp-adapters, multi-server client | Covered |
| Short-term memory | Checkpointers, threads, resume | Covered |
| Long-term memory | Stores, namespaces, semantic search | Covered |
| Middleware | PII, summarization, custom hooks | Covered |
| Structured output | ToolStrategy, ProviderStrategy, Pydantic | Covered |
| Validation & error handling | handle_errors retries, wrap_tool_call, schema rules | Covered |
| Observability | LangSmith traces | Covered |
| Evals | Offline, online, trajectory, CI gates | Covered |
| Harnesses | Why scaffolding beats raw model power | Covered |
| Deep Agents | Planning, virtual FS, sub-agents | Covered |
| Multi-agent | Supervisor, swarm | Covered |
| Human-in-the-loop | Interrupts and approvals | Covered |
| RAG / retrieval | Embeddings, vector stores, retriever-as-tool | Covered |
| Deployment | LangGraph Platform, self-host | Covered |
| Streaming | Token and step streaming to a UI | Planned |
| Context engineering | What goes in / stays / leaves the context window | Planned |
| Skills (SKILL.md) | Packaged know-how for Deep Agents | Planned |
| Sandboxes & code execution | Safe REPL / shell for agents | Planned |
| Subgraphs | Composing graphs inside graphs | Planned |
| Cost & rate-limit control | Budgets, caching, fallbacks | Planned |
| Guardrails & safety | Input/output filters, prompt-injection defense | Idea |
| Testing fixtures | Mock models and tools for fast tests | Idea |
| Caching | Prompt and embedding caches | Idea |
| Auth & multi-tenant | Per-user keys, isolation, quotas | Idea |
| Voice / multimodal agents | Audio and image in the loop | Idea |
| LangGraph Studio | Visual debugging of graphs | Idea |
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.
- LangChain โ Agents & create_agent (docs)
- create_agent โ API reference
- LangChain & LangGraph reach v1.0 (blog)
- Chat model integrations (Claude / OpenAI / Groq / Ollama)
- LangGraph โ persistence, checkpointers & stores
- LangGraph โ adding memory
- LangChain โ Model Context Protocol (MCP)
- langchain-mcp-adapters (GitHub)
- Deep Agents โ overview (docs)
- deepagents โ "the batteries-included agent harness" (GitHub)
- The Anatomy of an Agent Harness (blog)
- LangSmith โ Observability (docs)
- LangSmith โ Evaluation
- langgraph-supervisor (GitHub)
- langgraph-swarm (GitHub)