The Complete Guide to LangChain Errors in 2026
The definitive reference for engineers building production applications on LangChain after the v1 rebuild. Covers the 2026 package landscape (langchain, langchain-core, langchain-community, langchain-classic, partner packages), LCEL & Runnables, typed content blocks, tool calling, create_agent + middleware, migration off AgentExecutor before the December 2026 removal deadline, and LangSmith observability. Links to every one of our 30 LangChain error-fix pages.
Introduction
LangChain reached 1.0 GA on October 22, 2025, alongside LangGraph 1.0 — the biggest architectural reset the framework has ever shipped. Nine months later, teams are still working through the migration: AgentExecutor deprecated with a December 2026 removal deadline, create_react_agent from langgraph.prebuilt deprecated, initialize_agent and every legacy chain shipped to a separate langchain-classic package. In their place: create_agent as the single agent front door, a middleware system, typed content blocks on chat responses, and a semver commitment (no breaking changes until 2.0).
This guide is the definitive reference for LangChain as it exists today. It covers the current package layout, LCEL and Runnables, chat models with typed content blocks, tool calling, the new create_agent + middleware pattern, migration paths off the deprecated agent constructors, vector stores and the langchain-community drift trap, LangSmith tracing, and how to decide between LangChain, LangGraph, and calling provider SDKs directly.
Who this guide is for
The guide assumes you have:
- Built at least one working LangChain app — a chain, an agent, or a RAG pipeline
- Hit at least one
ImportError, deprecation warning, or version-mismatch error that sent you searching - Been asked at some point "why don't we just call the provider SDK directly?"
If that's you, read this end-to-end once to build a full map of the framework surface; return to specific chapters when you hit a related error. Each chapter links to the specific error-fix pages that answer narrower questions with tested code.
What LangChain is (and isn't) in 2026
LangChain in 2026 is a thin composition layer over provider SDKs plus an agent harness. The old identity — "hundreds of chains, memory classes, and integrations" — has been split apart. The core package (langchain) is small: chat model abstractions, tools, prompts, create_agent, middleware, LCEL. Everything else lives in separate packages you install explicitly:
- Provider integrations in
langchain-openai,langchain-anthropic,langchain-aws,langchain-google-genai, etc. - Community integrations (600+ vector stores, loaders, tools) in
langchain-community - Legacy machinery (LLMChain, ConversationChain, initialize_agent, AgentExecutor, ConversationBufferMemory) in
langchain-classic - Stateful agent orchestration in
langgraph— LangChain uses LangGraph internally forcreate_agent - Observability in
langsmith— traces, evals, prompt versioning
Pick the pieces you need, pin them independently, and be aware that langchain-community has a looser deprecation cadence than the core package. That combination is the source of most 2026 LangChain errors.
The four things that changed in v1
1. create_agent replaces three older constructors. AgentExecutor, initialize_agent, and create_react_agent (from langgraph.prebuilt) all collapsed into create_agent in langchain.agents. It runs on LangGraph internally and adds middleware. Every agent codebase written before October 2025 needs migration; AgentExecutor's removal deadline is December 2026.
2. Middleware is the flagship v1 feature. Before v1, adding cross-cutting behavior to an agent — trimming message history, redacting PII, gating which tools can run, adding human approval, falling back to a cheaper model — meant either stuffing instructions into the prompt or rebuilding the loop by hand in LangGraph. Middleware makes these composable: ModelFallbackMiddleware, SummarizationMiddleware, custom middleware plugged into create_agent.
3. Chat model responses now use typed content blocks. Instead of one opaque content: str, responses come back as content: [TextBlock, ReasoningBlock, ToolCallBlock, CitationBlock, ...]. This makes reasoning models, citations, and mixed-output workflows far easier to handle. It's also a breaking change for any code that assumed response.content was a string.
4. Callbacks are backgrounded and non-blocking by default. Important for serverless environments where sync callbacks used to block execution and cause cold-start timeouts. Also matters for tracing — LangSmith traces now flush asynchronously without blocking the request. Code relying on synchronous callback ordering needs review.
What's not in this guide
- Deep LangGraph coverage — see the upcoming LangGraph pillar. This guide references LangGraph where
create_agentuses it internally, but doesn't teach graph construction. - Prompt engineering — a framework guide can't substitute for iterating on prompts against your data
- Individual provider quirks — for Claude-specific quirks see the Claude pillar, for OpenAI see the OpenAI pillar, etc.
All examples verified against langchain-core 1.4.x, langchain 1.2.1, langgraph 1.2.6, and partner packages at their August 2026 versions (langchain-openai 0.3.x, langchain-anthropic 0.3.x, langchain-aws 0.3.x). Pin your versions — the framework has committed to semver but partner and community packages release independently.
The 2026 package landscape
Understanding what installs from where is the single biggest predictor of how much time you'll spend fighting LangChain. Five package tiers with different maintenance cadences, different deprecation policies, and different install patterns. Get this map right and most "why did this break?" questions become obvious.
The five package tiers
| Tier | Packages | Maintainer | Semver commitment | Deprecation cadence |
|---|---|---|---|---|
| Core | langchain, langchain-core |
LangChain team | No breaking changes until 2.0 | Formal notice cycle |
| Partner integrations | langchain-openai, langchain-anthropic, langchain-aws, langchain-google-genai, langchain-cohere, langchain-mistralai, etc. |
LangChain team + providers | Independent versioning; typically stable | Follows core; provider changes may cascade |
| Community integrations | langchain-community |
Community | None guaranteed | Ad-hoc; silent breakage possible |
| Legacy compatibility | langchain-classic |
LangChain team (maintenance mode) | Frozen — critical fixes only | December 2026 removal deadline for AgentExecutor path |
| Orchestration & observability | langgraph, langsmith, langgraph-checkpoint-* |
LangChain team | langgraph committed to semver alongside langchain | Formal notice cycle |
The core package — langchain
langchain in v1 is small on purpose. It contains:
langchain.chat_models— abstractions,init_chat_modelfactorylangchain.tools—@tooldecorator,StructuredToollangchain.agents—create_agent, middlewarelangchain.prompts— prompt templates- Re-exports from
langchain-core— Runnable, messages, output parsers
That's it. Not vector stores. Not document loaders. Not memory classes. Not chains beyond LCEL. If your import fails on the core package, it's almost certainly moved to langchain-community or langchain-classic.
langchain-core — the abstractions layer
langchain-core holds the primitive interfaces every other package builds on: Runnable, BaseChatModel, BaseTool, BaseMessage, output parsers, callbacks. You rarely import from it directly; it's a transitive dependency of everything.
Practical implication: langchain-core version is what actually drives compatibility across your dependency tree. If langchain-openai 0.3.5 requires langchain-core >=1.4.0 but langchain-community 0.3.2 pins langchain-core <1.4.0, pip resolution fails or (worse) silently installs incompatible versions. Chapter 5 covers version-conflict debugging.
Partner packages
Provider-specific integrations live in separate packages, each owned jointly by the LangChain team and the provider. Install only what you need:
# Anthropic Claude
pip install langchain-anthropic
# OpenAI (GPT-5.6, embeddings, Whisper)
pip install langchain-openai
# Azure OpenAI
pip install langchain-openai # AzureChatOpenAI is in the same package
# Or: pip install langchain-azure-ai # newer Azure-native package
# AWS Bedrock
pip install langchain-aws
# Google Gemini (google-genai unified SDK)
pip install langchain-google-genai
# Cohere, Mistral, Groq, Together, Fireworks, DeepInfra, etc.
pip install langchain-cohere langchain-mistralai langchain-groq
pip install langchain-together langchain-fireworks langchain-deepinfra
Partner packages track their provider's API. When Anthropic ships a new beta header, langchain-anthropic updates typically within days to weeks. When OpenAI ships GPT-5.6, langchain-openai exposes the model on its next release. See partner package version mismatch.
langchain-community — high value, higher risk
langchain-community is where 600+ integrations live: vector stores (Chroma, Pinecone, Weaviate, Milvus, FAISS, Qdrant, and dozens more), document loaders (PDF, DOCX, notion, GitHub, Slack, etc.), embeddings, tools, retrievers. It's the reason LangChain works as a universal glue layer — and the reason production teams get burned.
The trap: integration maintainers are not bound to the same deprecation notice cycle as the core team. A vector store integration that worked in langchain-community==0.3.2 may have its interface changed in 0.3.8 without a deprecation warning. Import still succeeds; behavior changes at runtime. Explicit pinning is the only defense. See community integration silent breakage.
Pin langchain-community exactly (==0.3.5, not >=0.3.5). Test integration behavior before upgrading. If a specific integration is critical to your production path (a specific vector store, a specific loader), consider migrating it to your own thin wrapper over the underlying SDK — reduces framework surface at the cost of writing 50-100 lines yourself.
langchain-classic — the legacy home
Everything the v1 rebuild removed from langchain proper lives here:
LLMChain,ConversationChain,RetrievalQA,ConversationalRetrievalChaininitialize_agent,AgentExecutor(and the deprecated agent types)ConversationBufferMemory,ConversationSummaryMemory, and other memory classes- Old retriever base classes
Install it if legacy code needs to run:
pip install langchain-classic
# Then imports work again
from langchain_classic.chains import LLMChain
from langchain_classic.agents import initialize_agent, AgentExecutor
But treat it as a migration bridge, not a destination. Package is in maintenance mode; critical fixes only. AgentExecutor's removal deadline is December 2026. If your code is on langchain-classic, plan the migration to create_agent before then.
See langchain-classic install required and AgentExecutor December 2026 removal.
langgraph & langsmith
langgraph is what create_agent runs on internally. If you want direct graph construction, human-in-the-loop, or persistence via checkpointers, you install and use it directly. See the LangGraph pillar (coming soon) for depth.
langsmith is the observability SDK: traces, evaluations, prompt versioning, dataset management. Optional but recommended for production. Chapter 10 covers integration.
A working install pattern
# requirements.txt — pin exactly for reproducibility
langchain==1.2.1
langchain-core==1.4.3
langchain-openai==0.3.5
langchain-anthropic==0.3.7
langchain-community==0.3.5 # only if you need community integrations
langgraph==1.2.6
langsmith==0.2.4
# Only if migrating legacy code
# langchain-classic==0.1.2
Fresh projects: install langchain, langchain-core, one or two partner packages, and add langgraph if you need stateful workflows. Skip langchain-community unless a specific integration you need is only there.
LCEL & the Runnable interface
LCEL (LangChain Expression Language) is the pipe-operator syntax for composing chains of components. Contrary to some posts online, LCEL is not deprecated in v1 — it remains the recommended way to build custom chains and RAG pipelines. What changed: it's no longer the recommended way to build agents. That's create_agent's job now. LCEL is for linear composition; agents are for tool-using loops.
The Runnable interface
Every LCEL-compatible component implements Runnable, an interface with a small set of standard methods:
| Method | Sync/Async | Use case |
|---|---|---|
invoke(input) |
Sync | Single input, single output |
ainvoke(input) |
Async | Single input, single output in async code |
stream(input) |
Sync generator | Streaming output (tokens or chunks) |
astream(input) |
Async generator | Async streaming |
batch(inputs) |
Sync | Multiple inputs concurrently |
abatch(inputs) |
Async | Async multiple-input |
astream_events(input) |
Async generator | Fine-grained event stream — v2 schema in 2026 |
A chain built with LCEL implements all of these automatically. Choose the method that fits your access pattern — see invoke vs stream vs batch.
Building chains with the pipe operator
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = init_chat_model("claude-sonnet-4-6", model_provider="anthropic")
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
("user", "{question}"),
])
parser = StrOutputParser()
chain = prompt | model | parser
# Every Runnable method works
result = chain.invoke({"question": "What is LCEL?"})
async for chunk in chain.astream({"question": "What is LCEL?"}):
print(chunk, end="", flush=True)
results = chain.batch([{"question": q} for q in questions])
The pipe operator (|) is just Python operator overloading on Runnable. Left side pipes its output as input to the right side. Types must be compatible or you get a runtime error — see pipe operator type mismatch.
The Runnable primitives
| Class | Purpose |
|---|---|
RunnableLambda(fn) |
Wrap a function as a Runnable |
RunnableParallel({{"k": r1, "v": r2}}) |
Run multiple Runnables in parallel, return dict of results |
RunnablePassthrough() |
Pass input through unchanged (useful in RunnableParallel) |
RunnableBranch([(cond, r), ...]) |
Route to different Runnables based on a condition |
RunnableConfig |
Runtime config passed through chain — callbacks, tags, metadata |
Runnable.with_fallbacks([r2, r3]) |
Chain of fallbacks on error |
Runnable.with_retry(retry_if_exception=..., stop_after_attempt=3) |
Retry policy attached to a Runnable |
RunnableParallel is where most people first meet the composition power. Use it to build the classic RAG shape:
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
retriever = vectorstore.as_retriever()
rag_chain = (
RunnableParallel({
"context": retriever,
"question": RunnablePassthrough(),
})
| prompt
| model
| parser
)
answer = rag_chain.invoke("What's the return policy?")
RunnableParallel runs the retriever and the passthrough concurrently, packages results into a dict, and passes the dict to the prompt. Watch for key collisions — a dict key you already used in your prompt template gets overwritten. See RunnableParallel key collision.
Async in LCEL — the common trap
RunnableLambda wraps a function; if the function is sync but you call .ainvoke(), LangChain runs the sync function in a thread pool. This usually works but occasionally breaks — sync code that touches thread-local state (some database connections, some auth libraries) fails silently or corrupts state.
# Prefer async functions when the chain will be awaited
async def my_step(x):
return await do_async_work(x)
chain = prompt | model | RunnableLambda(my_step) | parser
# Sync function is fine for pure computation
def format_output(x):
return x.upper()
chain = prompt | model | parser | RunnableLambda(format_output)
Rule: async chain = async functions. Sync chain = sync functions. Mixing works but adds complexity you don't need. See RunnableLambda async context.
batch() and concurrency
Runnable.batch(inputs, config={"max_concurrency": 10}) runs inputs concurrently up to a max. Default max is unbounded, which is a common cause of provider throttling — batching 1,000 inputs sends 1,000 concurrent requests and immediately hits provider rate limits. Set max_concurrency explicitly:
results = chain.batch(
inputs,
config={"max_concurrency": 10}, # Respect provider RPM limits
)
See batch concurrency throttling.
astream_events v2 — the observability stream
astream_events emits fine-grained events as a chain runs — start/end of each Runnable, streaming chunks, tool calls, retrieval results. v2 schema replaced v1 in mid-2025; if you have code parsing events from an older LangChain version, event shapes changed. See astream_events v2 schema.
When NOT to use LCEL
- Building an agent — use
create_agent, not a hand-rolled LCEL loop - Complex branching / stateful workflows — use LangGraph directly
- The chain has one step — just call the model, skip the LCEL wrapping
- You need custom retry logic per step — spelled-out code is clearer than
.with_retry()plus conditional logic
LCEL is a good fit when the workflow is a linear pipeline: retrieve → prompt → model → parse, or similar. Beyond that, prefer create_agent or LangGraph.
Chat models & typed content blocks
LangChain v1 changed how chat model responses are structured. Instead of one opaque content: str, responses come back as an array of typed content blocks — text, reasoning, tool calls, citations, images. This makes reasoning models and structured outputs far easier to handle. It's also a breaking change for any code that assumed response.content was a string. This chapter covers the v1 message shape, the init_chat_model factory, and message-history patterns.
init_chat_model — one factory for every provider
Rather than importing provider-specific classes, use the factory:
from langchain.chat_models import init_chat_model
# Anthropic
model = init_chat_model("claude-opus-4-8", model_provider="anthropic")
# OpenAI
model = init_chat_model("gpt-5.6-sol", model_provider="openai")
# Azure OpenAI (deployment name, per the Azure pillar)
model = init_chat_model(
"my-gpt-5-6-sol-prod", # deployment name
model_provider="azure_openai",
api_version="2024-10-21",
azure_endpoint="https://my-aoai.openai.azure.com/",
)
# AWS Bedrock (model ID or inference profile)
model = init_chat_model(
"anthropic.claude-opus-4-8-v1:0",
model_provider="bedrock_converse", # Uses Converse API under the hood
)
# Google Gemini
model = init_chat_model("gemini-3.1-pro", model_provider="google_genai")
# Provider shorthand — "openai:gpt-5.6-sol", "anthropic:claude-opus-4-8"
model = init_chat_model("anthropic:claude-opus-4-8")
Under the hood, init_chat_model imports the right partner package and constructs its class. The corresponding partner package must be installed — asking for model_provider="anthropic" without langchain-anthropic installed raises a clear error. See init_chat_model provider string error.
Typed content blocks — the v1 shape
Before v1, response.content was a string. In v1, it's a list of typed blocks:
response = model.invoke([HumanMessage("Solve this problem step by step: ...")])
# response.content is now List[ContentBlock]
for block in response.content:
if block["type"] == "text":
print(block["text"])
elif block["type"] == "reasoning":
print("[Reasoning]", block["text"])
elif block["type"] == "tool_call":
print("[Tool call]", block["name"], block["input"])
elif block["type"] == "citation":
print("[Citation]", block["source"])
Block types include:
text— plain generated textreasoning— chain-of-thought or "thinking" content from reasoning models (o-series, Claude with thinking, Gemini with thinking_level)tool_call— a tool invocation the model wants to maketool_use/tool_result— Anthropic-style tool blocks (semantically equivalent to tool_call)citation— citation reference (from OpenAI web search, Anthropic citations, Gemini grounding)image— image content (multimodal)
The .text attribute is a convenience accessor that concatenates all text-type blocks:
# Instead of iterating blocks for just the text
plain_text = response.text # concatenates all "text" blocks
Use .text for simple cases; iterate blocks when you care about reasoning, citations, or tool calls specifically. See typed content blocks migration.
Pre-v1 code that did response.content.upper() or len(response.content) assumes a string. In v1 those raise AttributeError or return the block count. Fix: use response.text for the string-like behavior, or iterate blocks explicitly. This is one of the most common v1 migration errors.
Message types
| Class | Role | Use |
|---|---|---|
SystemMessage |
system | Instructions, persona, tool descriptions |
HumanMessage |
user | User input |
AIMessage |
assistant | Model output (also what you construct for history) |
ToolMessage |
tool | Result of a tool call, sent back to model |
AIMessageChunk |
assistant | Streaming chunk — accumulates into AIMessage |
All accept typed content blocks in content. For simple text, pass a string — LangChain converts to a single text block automatically.
Message history trimming
Long conversations eventually exceed model context. LangChain ships trim_messages for token-count-based trimming:
from langchain_core.messages import trim_messages
trimmed = trim_messages(
messages,
max_tokens=100_000,
strategy="last", # keep the last N tokens
token_counter=model, # use the model's tokenizer
include_system=True, # always keep the system message
allow_partial=False,
start_on="human", # ensure history starts on a user turn
)
For agents, SummarizationMiddleware (chapter 7) is often a better fit than raw trimming — summarizes older turns into a compressed system message rather than dropping them. See message history trimming.
Streaming with typed blocks
async for chunk in model.astream(messages):
# chunk is AIMessageChunk with content_blocks
for block in chunk.content:
if block["type"] == "text":
print(block["text"], end="", flush=True)
elif block["type"] == "reasoning":
# Show reasoning in a separate UI panel
emit_reasoning(block["text"])
Chunks accumulate — the final chunk's content contains the full response. Streaming works with tool calls too (partial tool call arguments arrive as chunks; complete when the block finishes).
Every LangChain error category
LangChain errors fall into ten categories, most tied to the framework's composition surface (imports, types, async, LCEL) rather than to the underlying provider. This chapter maps each category with links to specific error-fix pages.
1. Import & installation errors
Fires at: Module load.
Common causes: Feature moved between packages (legacy → langchain-classic); partner package not installed; version conflict between langchain-core and a partner package; Python version below 3.10.
A working command in a blog post from 2024 may fail today because the import path changed. If from langchain.chains import LLMChain fails, it's now in langchain-classic. If from langchain.agents import AgentExecutor fails, same package. If from langgraph.prebuilt import create_react_agent works but is deprecated, use from langchain.agents import create_agent.
Related pages: langchain-classic import required · partner package version mismatch · Python 3.10 minimum
2. Version conflict errors
Fires at: pip install or first import.
Common causes: langchain-core versions incompatible across packages; langchain-community pinning a range that conflicts with a partner package; Pydantic v1 vs v2.
Pin exact versions across your entire LangChain stack. If pip resolution succeeds but imports fail, you have a compatibility gap — check langchain-core version each package expects. See community silent breakage.
3. LCEL & Runnable errors
Fires at: Chain construction or invocation.
Common causes: Type mismatch on pipe operator; sync function in async chain; RunnableParallel key collision; missing input key from prompt template.
Most LCEL errors are input/output type mismatches. Print intermediate types with .invoke on partial chains to isolate where the mismatch occurs.
Related pages: Pipe type mismatch · Async context · RunnableParallel key collision
4. Chat model & message errors
Fires at: Model invocation or response parsing.
Common causes: Provider API key missing; typed content blocks vs string assumption break; init_chat_model provider string wrong; message ordering invalid (tool call without matching tool result).
Code that worked pre-v1 may break on the content-blocks change. Verify response.text instead of response.content when you want a string. See typed content blocks migration.
5. Tool calling errors
Fires at: Tool schema construction, model invocation with tools, or tool execution.
Common causes: @tool decorator on function with unsupported argument type; Pydantic model on args_schema not v2-compatible; with_structured_output strict mode with unsupported field types; ToolException not caught.
Chapter 6 covers tool calling in depth. Common trap: with_structured_output(strict=True) requires all fields be JSON-Schema representable — union types, arbitrary dicts, and some Pydantic constructs break. See with_structured_output strict.
6. Agent errors
Fires at: create_agent construction or agent invocation.
Common causes: Middleware ordering (order matters — later middleware wraps earlier); ModelFallbackMiddleware misconfigured; tool_choice conflict with middleware; dynamic model switch failing mid-run.
Chapter 7 covers create_agent and middleware. Middleware ordering is the #1 pitfall — see middleware ordering.
7. Migration errors (AgentExecutor & create_react_agent)
Fires at: Import or agent construction on newer LangChain versions.
Common causes: AgentExecutor deprecation warning treated as fatal by strict linters; initialize_agent import broken (now in langchain-classic); create_react_agent from langgraph.prebuilt deprecated in favor of create_agent.
Chapter 8 walks migration path. If you have to leave legacy code on AgentExecutor for now, install langchain-classic to keep it running — but remember the December 2026 removal deadline.
Related pages: AgentExecutor deprecation · create_react_agent deprecated
8. Retriever & vector store errors
Fires at: Vector store construction, ingestion, or query.
Common causes: langchain-community integration interface changed silently; embedding dimension doesn't match existing index; async similarity_search called on sync-only integration.
Chapter 9 covers retrievers and the langchain-community drift problem in depth.
9. Memory & state errors
Fires at: ConversationBufferMemory import or usage.
Common causes: Memory classes now in langchain-classic; using ConversationBufferMemory with create_agent (mismatch — create_agent uses LangGraph checkpointers instead).
Legacy memory classes work when installed via langchain-classic. For new code, use LangGraph's MemorySaver or another checkpointer. See Memory to checkpointer migration.
10. Tracing, callbacks & async errors
Fires at: LangSmith trace flush, callback dispatch, or async runtime.
Common causes: LANGSMITH_API_KEY missing; LANGSMITH_PROJECT env var not picked up; callbacks backgrounded and completing after request returns (serverless cold-shutdown truncation); nested event loop in Jupyter.
Chapter 10 covers tracing and callbacks. The backgrounded-callbacks change means in serverless, ensure traces flush before the function returns — use langsmith.Client.flush() or a wait strategy. See backgrounded callbacks.
Tool calling & structured outputs
Tool calling in LangChain v1 unifies the API across providers: same @tool decorator, same bind_tools pattern, same tool call block in the response. Structured outputs (with_structured_output) piggybacks on the same tool-calling mechanism. This chapter covers the current tool-defining patterns, common errors, and structured-output strict mode.
Defining tools with @tool
from langchain.tools import tool
from typing import Annotated
@tool
def get_weather(
location: Annotated[str, "City name or ZIP code"],
unit: Annotated[str, "Temperature unit — 'celsius' or 'fahrenheit'"] = "celsius",
) -> str:
'''Get the current weather for a location.'''
# Actual implementation
return f"{location}: 22 {unit}"
The docstring becomes the tool description sent to the model. Type annotations plus Annotated descriptions become the parameter schema. Return type is preserved through to the ToolMessage sent back to the model.
Explicit Pydantic schema
For complex schemas, use an explicit Pydantic model:
from pydantic import BaseModel, Field
from langchain.tools import StructuredTool
class WeatherArgs(BaseModel):
location: str = Field(description="City name or ZIP code")
unit: str = Field(default="celsius", description="celsius or fahrenheit")
include_forecast: bool = Field(default=False, description="Include 3-day forecast")
def get_weather(location, unit="celsius", include_forecast=False):
return {"location": location, "temp": 22, "unit": unit}
tool = StructuredTool.from_function(
func=get_weather,
name="get_weather",
description="Get the current weather for a location",
args_schema=WeatherArgs,
)
Pydantic must be v2 (required since langchain 0.3). Common trap: importing pydantic.v1 models — those aren't compatible. See tool decorator args_schema.
Binding tools to a model
model_with_tools = model.bind_tools([get_weather, other_tool])
response = model_with_tools.invoke("What's the weather in Paris?")
# Response contains tool_call blocks
for block in response.content:
if block["type"] == "tool_call":
print(block["name"], block["input"])
# {"location": "Paris", "unit": "celsius"}
For agent-style loops (invoke → get tool calls → execute → send back results → repeat), don't hand-roll this. Use create_agent (chapter 7) which handles the loop, error handling, and middleware.
Structured outputs — with_structured_output
When you want a Pydantic model as the response (instead of free text plus tool calls), use with_structured_output:
class UserProfile(BaseModel):
name: str
age: int
interests: list[str]
model_typed = model.with_structured_output(UserProfile)
profile = model_typed.invoke("Extract profile: Sara, 34, likes hiking and jazz")
# profile is a UserProfile instance
print(profile.name, profile.interests)
Under the hood, LangChain typically uses tool calling (or the provider's structured-output feature if available) to enforce the schema. Provider support varies — OpenAI GPT-5.4+ and Anthropic Claude support native structured outputs; older models fall back to tool-calling emulation.
Strict mode
with_structured_output(schema, strict=True) asks providers with native structured-output support (OpenAI structured outputs, Anthropic constrained decoding) to enforce the schema at the token level. Guarantees valid JSON matching your schema. Constraint: not every Pydantic construct is JSON-Schema representable in strict mode:
- Union types with more than 2 members
- Arbitrary
dict[str, Any]— must specify value types - Some Pydantic validators (custom
@validators that transform values) - Recursive schemas (self-referencing models)
See with_structured_output strict mode for the compatibility matrix.
Handling tool errors
from langchain.tools import ToolException
@tool
def risky_tool(query: str) -> str:
'''Do something that might fail.'''
if not query:
raise ToolException("Query cannot be empty")
return do_work(query)
ToolException gets returned to the model as a ToolMessage with the exception text — the model can adapt. Other exceptions kill the agent run unless middleware catches them. See ToolException handling.
create_agent & the middleware system
create_agent is v1's flagship: one function that replaces three older constructors, runs on LangGraph internally, and takes middleware as first-class configuration. This chapter walks the minimal setup, the middleware library, and the ordering rules that trip up almost every team on their first non-trivial middleware stack.
The minimal agent
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
model = init_chat_model("claude-sonnet-4-6", model_provider="anthropic")
@tool
def add(a: int, b: int) -> int:
'''Add two numbers.'''
return a + b
@tool
def multiply(a: int, b: int) -> int:
'''Multiply two numbers.'''
return a * b
agent = create_agent(
model,
tools=[add, multiply],
system_prompt="You are a helpful math assistant.",
)
response = agent.invoke({"messages": [("user", "What's 12 + 8 times 3?")]})
final_message = response["messages"][-1]
print(final_message.content)
That's the whole thing. The agent runs a tool-calling loop until the model returns a response without a tool call (or a stop condition triggers). No AgentExecutor, no scratchpad prompt, no output parser. Under the hood, create_agent constructs a LangGraph state graph — one node for the model call, one node for tool execution, edges for the loop.
What create_agent replaces
| Legacy pattern | v1 replacement | Notes |
|---|---|---|
initialize_agent(...) |
create_agent(model, tools, system_prompt=...) |
initialize_agent now in langchain-classic |
AgentExecutor(agent, tools) |
create_agent(...) |
December 2026 removal for AgentExecutor |
from langgraph.prebuilt import create_react_agent |
from langchain.agents import create_agent |
langgraph.prebuilt version deprecated |
All three collapsed into one API. If your codebase has a mix, migrate to create_agent uniformly — the migration path is nearly mechanical for most simple agents.
Middleware — the v1 headline feature
Middleware wraps the agent loop with cross-cutting behavior. Multiple middlewares stack. Built-in library covers common patterns:
| Middleware | Purpose |
|---|---|
ModelFallbackMiddleware |
Fall back to a cheaper/backup model on error (rate limit, overload) |
SummarizationMiddleware |
Summarize old messages when context grows large — improved in 1.1.0 |
TokenLimitMiddleware |
Enforce max input tokens per model call |
MessageTrimmingMiddleware |
Trim message history to a token budget |
PIIRedactionMiddleware |
Redact PII from inputs/outputs (with a PII detector plugin) |
HumanApprovalMiddleware |
Pause agent for human approval on specific tool calls |
ToolGuardMiddleware |
Allow-list / deny-list which tools can run in specific contexts |
| Custom middleware | Implement AgentMiddleware subclass |
from langchain.agents import create_agent
from langchain.agents.middleware import (
ModelFallbackMiddleware,
SummarizationMiddleware,
HumanApprovalMiddleware,
)
primary = init_chat_model("claude-opus-4-8", model_provider="anthropic")
fallback = init_chat_model("claude-sonnet-4-6", model_provider="anthropic")
agent = create_agent(
primary,
tools=[write_email, delete_email, list_emails],
system_prompt="You are an email assistant.",
middleware=[
ModelFallbackMiddleware(models=[fallback]),
SummarizationMiddleware(threshold_tokens=100_000),
HumanApprovalMiddleware(
require_approval_for=["delete_email"],
),
],
)
Middleware ordering — the #1 gotcha
Middlewares stack like Russian dolls: later middlewares in the list wrap earlier ones. Ordering matters and is not always intuitive:
ModelFallbackMiddlewareshould be near the innermost — it catches model errors before other middlewares see themSummarizationMiddleware/MessageTrimmingMiddlewareshould run before the model call to prepare the inputHumanApprovalMiddlewareshould run before tool execution — obviouslyPIIRedactionMiddlewaremay need to run at multiple points (input, before model call, on output) — a single instance may not cover all needs
Wrong ordering doesn't always error visibly. A HumanApprovalMiddleware placed after a ToolGuardMiddleware that already blocked the tool never fires; a SummarizationMiddleware that runs after TokenLimitMiddleware can miss its trigger because trimming already reduced token count. See middleware ordering.
Dynamic vs static model selection
Static: one model for the entire run.
Dynamic: switch models mid-run based on middleware rules. For example, start with a cheap fast model, escalate to a stronger one if a middleware detects difficulty. Configured via middleware that mutates the model selection:
from langchain.agents.middleware import DynamicModelMiddleware
def choose_model(state):
# Inspect state, decide which model to use
if state.needs_reasoning:
return init_chat_model("claude-opus-4-8", model_provider="anthropic")
return init_chat_model("claude-haiku-4-5", model_provider="anthropic")
agent = create_agent(
model=init_chat_model("claude-haiku-4-5", model_provider="anthropic"),
tools=[...],
middleware=[DynamicModelMiddleware(model_selector=choose_model)],
)
Dynamic selection can save significant cost when a majority of turns don't need the flagship model. See dynamic model middleware.
Model profiles (v1.1.0)
Model profiles let you declare a set of models (with different capabilities and cost tiers) once and reference them by profile name across your codebase. Introduced in 1.1.0 (December 2025). Useful for teams standardizing on a small model roster and swapping providers behind the profile.
Agent state & checkpointing
create_agent agents can persist state via LangGraph checkpointers:
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
agent = create_agent(model, tools=[...], checkpointer=memory)
# First turn
config = {"configurable": {"thread_id": "conv-42"}}
agent.invoke({"messages": [("user", "Hi, I'm Sara")]}, config)
# Second turn — same thread_id continues the conversation
agent.invoke({"messages": [("user", "What did I tell you my name was?")]}, config)
For production, use a durable checkpointer (Postgres, Redis, or a langgraph-checkpoint-* package). See the LangGraph pillar for depth.
Migrating off AgentExecutor & create_react_agent
Two deprecations are in flight: AgentExecutor (with its family of legacy agent types) and create_react_agent from langgraph.prebuilt. Both migrate to create_agent. AgentExecutor's removal deadline is December 2026 — after that, code depending on it needs langchain-classic installed permanently. This chapter walks the mechanical migration and the parts that need thought.
Timeline & deadlines
- September 2024 (v0.3) — legacy chains removed from core (moved to langchain-community, later to langchain-classic)
- October 2025 (v1.0) — AgentExecutor formally deprecated; create_agent introduced
- December 2026 (planned) — AgentExecutor removed from all first-party packages; only accessible via langchain-classic
- Rolling — create_react_agent from langgraph.prebuilt deprecated; use langchain.agents.create_agent
Migrating AgentExecutor → create_agent
Legacy:
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
model = ChatOpenAI(model="gpt-4")
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant"),
("placeholder", "{{chat_history}}"),
("user", "{{input}}"),
("placeholder", "{{agent_scratchpad}}"),
])
agent = create_openai_tools_agent(model, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({"input": "user query"})
v1 equivalent:
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("gpt-5.6-terra", model_provider="openai")
agent = create_agent(
model,
tools=tools,
system_prompt="You are a helpful assistant",
)
result = agent.invoke({"messages": [("user", "user query")]})
Notice what disappeared: the ChatPromptTemplate with the agent_scratchpad placeholder, the separate agent + executor construction, the verbose=True flag (use LangSmith for traces instead). Input shape changed too — {"input": ...} becomes {"messages": [(role, content), ...]}.
Migrating create_react_agent → create_agent
Deprecated (from langgraph.prebuilt):
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
model,
tools,
prompt="You are a helpful assistant.", # note: "prompt", not "system_prompt"
)
v1 replacement (from langchain.agents):
from langchain.agents import create_agent
agent = create_agent(
model,
tools,
system_prompt="You are a helpful assistant.", # note: "system_prompt"
)
Two API changes: the import path (langchain.agents, not langgraph.prebuilt) and the parameter name (system_prompt, not prompt). Otherwise nearly identical behavior. See create_react_agent deprecated.
Migrating initialize_agent
initialize_agent is even older than AgentExecutor. It's moved entirely to langchain-classic. If you have to keep it running:
pip install langchain-classic
# Change imports
from langchain_classic.agents import initialize_agent, AgentType
agent = initialize_agent(
tools,
model,
agent=AgentType.OPENAI_FUNCTIONS,
verbose=True,
)
But this is a stopgap. The AgentType enum, the "OPENAI_FUNCTIONS" style, the string-based prompt construction — all of it is 2023-era architecture that doesn't match how models work in 2026. Plan the migration to create_agent during any code touch. See initialize_agent in langchain-classic.
What doesn't migrate cleanly
Most simple agents migrate mechanically. Things that need thought:
- Custom output parsers — v1 uses typed content blocks; if your parser assumed a string, rewrite for the block shape
- Custom agent scratchpad handling —
create_agentmanages this internally via LangGraph state; hand-rolled scratchpad logic is gone - Complex agent types (SELF_ASK_WITH_SEARCH, CONVERSATIONAL_REACT, etc.) — these were opinionated prompt templates. Rebuild as
system_prompt+ middleware increate_agent. - Deep AgentExecutor callback subclassing — callbacks work differently; use middleware or LangGraph state instead
- Memory classes wired into AgentExecutor — replace with LangGraph checkpointers
Testing the migration
Run legacy and v1 versions side-by-side on a fixed test set. Compare outputs (not identical — models are non-deterministic — but check task success rates). LangSmith Experiments makes this easier — attach both versions to a dataset and diff results. Set up automated regression tests before removing the legacy code path.
After the AgentExecutor removal deadline, code depending on it will require langchain-classic installed permanently — no more upgrade path. Consider that a technical debt you'll carry indefinitely. For code paths critical to the business, prioritize create_agent migration before the deadline. For minor internal tools, langchain-classic is a reasonable park-and-forget.
Retrievers, vector stores & langchain-community drift
LangChain's retriever and vector store abstractions are what many teams originally installed the framework for — one interface over 30+ vector databases, dozens of document loaders, a Runnable-compatible retriever base class. In 2026 the abstractions still work, but almost all the concrete integrations live in langchain-community, where maintenance cadence is loose and silent breakage between minor versions is the norm. This chapter covers the current patterns and the drift-management strategies.
The retriever interface
A retriever is a Runnable that takes a query string and returns a list of Documents:
from langchain_core.retrievers import BaseRetriever
from langchain_core.documents import Document
# Any retriever can be invoked
docs = retriever.invoke("query text")
# Or composed in an LCEL chain
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| model
| parser
)
Document has two fields: page_content (the text) and metadata (a dict). Every retriever, from a keyword search over a text file to a hybrid vector search over Pinecone, returns the same shape.
Vector stores → retrievers
Every vector store class has a .as_retriever() method that wraps it as a Runnable retriever:
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
vs = Chroma(
collection_name="my-docs",
embedding_function=embeddings,
persist_directory="./chroma_data",
)
# Ingest
vs.add_documents([
Document(page_content="...", metadata={"source": "doc1.pdf"}),
])
# Retrieve
retriever = vs.as_retriever(
search_type="similarity",
search_kwargs={"k": 5},
)
docs = retriever.invoke("what is X")
Same pattern for Pinecone, Weaviate, Milvus, Qdrant, FAISS, PGVector, MongoDB Atlas Vector Search, etc. — the import path changes but the interface doesn't.
The langchain-community drift trap
langchain-community integrations are maintained by contributors. That's what makes 600+ integrations possible. It's also what makes silent breakage between minor versions common. Concrete examples we've seen:
- A vector store class's
add_documentssilently changed argument order in a patch release, corrupting metadata in existing indices - A document loader's parsing changed to strip whitespace differently, breaking downstream chunk offsets
- A retriever's
similarity_searchswitched from returning top-K to returning top-K plus one "seed" document - An async method added a required argument that was optional before
None of these fire an ImportError. Tests may pass. Production behavior changes. See community integration silent breakage.
Defense strategies
- Pin exact versions —
langchain-community==0.3.5, not>=0.3.5. Test upgrades in staging before rolling. - Snapshot test critical integrations — golden-file tests over your specific retriever's output on a known query set. Catches behavior changes upgrade doesn't announce.
- Migrate critical integrations to a thin wrapper — for the 1-2 integrations your production actually depends on, write your own 50-line wrapper over the underlying SDK (Pinecone client, Chroma client, etc.). You lose the langchain-community abstraction; you gain control over your dependency surface.
- Check the integration's individual repo — many langchain-community integrations exist as separate packages too (langchain-chroma, langchain-pinecone, langchain-milvus). If the standalone package exists, prefer it — usually better-maintained than the community version.
Standalone integration packages worth knowing
| Package | Replaces |
|---|---|
langchain-chroma |
Chroma from langchain-community |
langchain-pinecone |
Pinecone from langchain-community |
langchain-weaviate |
Weaviate from langchain-community |
langchain-milvus |
Milvus from langchain-community |
langchain-qdrant |
Qdrant from langchain-community |
langchain-mongodb |
MongoDB Atlas Vector Search from langchain-community |
langchain-postgres |
PGVector from langchain-community |
langchain-elasticsearch |
Elasticsearch from langchain-community |
Prefer these over langchain-community equivalents. Better versioning, typically closer maintenance to the underlying SDK, clearer upgrade path.
Embedding dimension mismatches
The most common vector store error: index was created with 3072-dimensional embeddings (text-embedding-3-large), your new code uses text-embedding-3-small (1536 dims). Vector store either raises a dimension-mismatch error or silently returns garbage. Fix: keep embedding model choice in a config, never let it drift; re-embed indices when you change models. See embedding dimension mismatch.
Async retrievers
Not every retriever implements the async methods (ainvoke, aget_relevant_documents). Some fall back to running the sync method in a thread pool; others raise NotImplementedError. Test async behavior explicitly on your specific integration version. See async similarity_search.
LangSmith tracing & callbacks
LangSmith is the observability layer for LangChain applications — captures every model call, tool call, retrieval, and Runnable step as a trace with inputs, outputs, tokens, and latency. Setup is minimal (environment variables plus a signup); the value in production is enormous. Callbacks are the underlying mechanism — v1 changed them to be backgrounded and non-blocking, which mostly helps but occasionally breaks serverless assumptions.
Setup — the minimum viable trace
# Environment variables — set once
export LANGSMITH_API_KEY="lsv2_pt_..."
export LANGSMITH_TRACING="true"
export LANGSMITH_PROJECT="my-app-production"
# That's it — every LangChain invocation now emits traces
from langchain.chat_models import init_chat_model
model = init_chat_model("claude-sonnet-4-6", model_provider="anthropic")
response = model.invoke("Hello") # trace visible in LangSmith UI within seconds
No SDK changes required. LangChain's default tracer picks up the environment variables and streams events to LangSmith. If LANGSMITH_TRACING is unset or "false," no traces are emitted (zero overhead).
If your traces show up under "default" instead of your named project, verify (1) LANGSMITH_PROJECT is set in the environment the code actually reads (not just your shell), (2) it's set before importing LangChain modules (some tracers cache the project at import time), (3) you're not accidentally setting LANGCHAIN_PROJECT (older env var name — still works but LANGSMITH_PROJECT wins if both set). See LANGSMITH_PROJECT ignored.
What traces contain
Each trace is a nested tree of runs. Root run is the top-level invocation (a chain, an agent, a single model call). Child runs are the constituent Runnables — model calls, tool calls, retriever calls, custom RunnableLambdas. Each run has:
- Inputs and outputs — full request and response payloads
- Token counts — input tokens, output tokens, cache reads if applicable
- Latency — start/end timestamps
- Metadata & tags — anything you attached via RunnableConfig
- Errors — full traceback if the run raised
- Feedback — thumbs-up / thumbs-down / custom scores (attached later)
The trace UI shows the tree, latency waterfall, and lets you inspect any run's I/O. For debugging agent behavior, this is essential — a text log doesn't show which tool call the model made, what it received back, and how that shaped the next turn.
Attaching context to runs
from langchain_core.runnables import RunnableConfig
config: RunnableConfig = {
"tags": ["user-facing", "premium-tier"],
"metadata": {
"user_id": "u-42",
"session_id": "s-abc",
"feature": "chat-widget",
},
"run_name": "customer-support-chat",
}
response = chain.invoke({"question": "..."}, config)
Tags and metadata appear in the LangSmith UI and become filterable. Use them liberally — filtering to "traces from premium users where the agent used the delete tool" is only possible if you tagged accordingly.
Callbacks — the mechanism
Callbacks are hooks LangChain fires as Runnables execute: on_chain_start, on_chain_end, on_chain_error, on_llm_start, on_llm_end, on_tool_start, etc. The default LangSmith tracer is implemented as a callback handler. You can add your own:
from langchain_core.callbacks import BaseCallbackHandler
class MyLogger(BaseCallbackHandler):
def on_llm_end(self, response, **kwargs):
print(f"Model call complete: {response.llm_output}")
chain.invoke(input, config={"callbacks": [MyLogger()]})
The v1 change — backgrounded, non-blocking callbacks
Pre-v1, callbacks fired synchronously — a slow callback (say, one that wrote to a remote logging service) blocked the chain. In v1, callbacks default to backgrounded execution on a thread pool. Chain returns immediately; callbacks flush asynchronously.
Mostly helpful. But it changes serverless behavior: if your Lambda / Cloud Function returns before backgrounded callbacks finish flushing, the traces are lost. Fix in serverless:
from langsmith import Client
def lambda_handler(event, context):
response = chain.invoke(event)
# Flush any pending traces before returning
Client().flush() # or use a wait strategy per your runtime
return {"body": response}
Datasets & evals
LangSmith's second layer: turn traces into evaluation datasets. Attach expected outputs, run experiments comparing prompt/model/config variants, get pass rates.
from langsmith import Client
from langsmith.evaluation import evaluate
client = Client()
def target(inputs):
return {"answer": chain.invoke(inputs["question"])}
def correctness(run, example):
return {"score": run.outputs["answer"] == example.outputs["answer"]}
results = evaluate(
target,
data="my-eval-dataset", # dataset stored in LangSmith
evaluators=[correctness],
experiment_prefix="new-prompt-v3",
)
Evals are one of the strongest arguments for staying on LangChain even after you're comfortable calling provider SDKs directly — the observability + eval loop is real infrastructure value.
When LangSmith isn't the right fit
- Air-gapped environments — LangSmith is a hosted service (self-hosted enterprise version exists but is a separate contract)
- PII-sensitive workloads without a redaction layer — traces capture full I/O by default
- Extremely high-volume workloads where the tracing overhead / cost is a concern — sample or disable in production
LangChain vs LangGraph vs raw SDKs
"Should we use LangChain?" is a question every team asks at some point in 2026. The honest answer is: it depends on what you're building. This chapter frames the choice — LangChain as a composition layer, LangGraph for stateful agent workflows, or provider SDKs directly with your own thin abstractions.
Where each tool actually shines
| Tool | Wins at | Struggles with |
|---|---|---|
| LangChain (LCEL + create_agent) | Prototyping, RAG pipelines, provider-swap flexibility, standard agent patterns with middleware | Complex stateful workflows, unusual control flow, minimal-dependency deployments |
| LangGraph (raw) | Multi-actor workflows, durable state, branching / looping / conditional logic, human-in-the-loop | Simple one-shot chains (overkill), learning curve |
| Provider SDKs directly | Absolute minimum dependencies, provider-specific features on day zero, single-provider workloads | Cross-provider abstractions, RAG boilerplate, observability plumbing |
Decision matrix
| Situation | Best fit |
|---|---|
| Simple RAG: retrieve → prompt → model → parse | LangChain LCEL |
| Standard agent: model + tools loop, maybe middleware | LangChain create_agent |
| Multi-agent workflow with routing / branching | LangGraph |
| Long-running workflow with human approval steps | LangGraph (checkpointers + interrupts) |
| Single-provider production with minimal complexity | Provider SDK directly |
| Multi-provider portability, provider swap at will | LangChain init_chat_model |
| Cutting-edge provider features on day zero | Provider SDK (LangChain integrations may lag) |
| Team of 3 shipping a prototype | LangChain (velocity wins) |
| Team of 30 running production for 2+ years | Whatever the team knows well; adding LangChain here creates more risk than it removes |
The "just call the SDK" argument
A recurring critique in 2026: LangChain adds a layer of abstraction that mostly wraps thin calls to provider SDKs. If you only use one provider, do you need it?
Fair criticism when your usage matches this shape:
- One provider (say, Anthropic) — no cross-provider abstraction needed
- No tool-calling loop — just prompt/response
- Bring-your-own observability (Datadog, OTel) — don't need LangSmith
- Bring-your-own RAG (custom retrieval already built) — don't need vectorstore abstractions
In that shape, the Anthropic SDK is 20-50 lines of code and gives you everything. Don't add LangChain for the sake of "someday we might swap providers."
Where LangChain pays for itself
Genuine value comes from:
- The 600-integration vector store / loader ecosystem — if you need to load PDFs, Notion, GitHub, Slack, and pump embeddings into 3 different vector stores, writing all of that from scratch is real work
- Provider abstraction — building a product that lets customers swap providers, or wanting to A/B test Claude vs GPT-5.6 on your workload
- create_agent + middleware — a real production-quality tool-calling loop with fallbacks, summarization, human approval, PII redaction is non-trivial to build; middleware makes it composable
- LangSmith integration — traces, evals, prompt versioning as infrastructure
- Team familiarity — many engineers already know LangChain; framework knowledge is skills leverage
The migration-off pattern
Many teams that adopted LangChain in 2023-2024 have since migrated some or all of their production off it — usually replacing the retrieval layer with a thin custom wrapper, keeping LangChain for the agent loop, or vice versa. This is fine. The framework doesn't have to be all-or-nothing.
Rule of thumb: keep LangChain where it earns its keep (agents + middleware, LangSmith integration, cross-provider swaps), replace it where it doesn't (custom retrieval, single-provider prompt/response, minimal-dependency edge deployments).
Production checklist
Twenty items to verify before your LangChain integration goes to production. Each corresponds to at least one incident from real 2026 deployments — with special attention to package versioning, migration debt, and observability gaps that don't apply outside LangChain.
Package management
- All LangChain packages pinned to exact versions (
==), not ranges - langchain-core version compatibility verified across every langchain-* package installed
- Python 3.10+ used everywhere the code runs (local, CI, prod)
- Standalone integration packages preferred over langchain-community equivalents where available (langchain-chroma over langchain-community.Chroma, etc.)
Migration state
- No AgentExecutor / initialize_agent / create_react_agent (langgraph.prebuilt) usage in code paths that will still exist after December 2026
- Response parsing uses
response.textor iterates blocks, never assumesresponse.contentis a string - Pydantic models are v2, not
pydantic.v1imports - Deprecated integrations flagged with tickets and migration plans
Agents & middleware
- Middleware ordering verified — ModelFallbackMiddleware innermost, HumanApprovalMiddleware before tool execution, SummarizationMiddleware before model call
- ModelFallbackMiddleware configured with a genuinely cheaper/faster backup, not another premium model
- Tool functions handle errors — either return a friendly error string or raise
ToolException - Agent state persistence via LangGraph checkpointer (Postgres/Redis, not just MemorySaver in prod)
- with_structured_output(strict=True) tested against the actual schema; incompatible fields refactored
Observability & tracing
- LangSmith configured with project name, api key, tags, metadata for every invocation
- Sensitive fields redacted before traces are emitted (or in a post-processing tracer callback)
- Serverless deployments flush LangSmith traces before function returns
- Evaluation datasets built for critical workflows; automated eval runs on deploys
Cost & resilience
Runnable.batchcalled withmax_concurrencyset explicitly, not unbounded (avoids provider throttling)- Fallback chains configured with
.with_fallbacks([backup_chain])for critical paths - Retry policies (
.with_retry) set on chains that call flaky external services
Item #1 (exact pinning) catches teams whose pip install succeeded but silently pulled a langchain-community with a breaking change. Item #5 (migration deadline) becomes urgent as December 2026 approaches — code using AgentExecutor without a migration plan is on borrowed time. Item #9 (middleware ordering) is where a working prototype breaks when a second middleware gets added — orderings that "obviously should work" often don't.
Frequently asked questions
Is LCEL deprecated?
No. LCEL is still the recommended way to build linear chains and RAG pipelines. What changed in v1: LCEL is no longer the recommended way to build agents — create_agent is. Posts claiming "LCEL is dead" are overstating a real change to agent construction as if it applied to all LCEL usage.
Why did my import stop working after upgrading LangChain?
Almost certainly moved packages. Legacy chains (LLMChain, ConversationChain, RetrievalQA) moved to langchain-classic. AgentExecutor and initialize_agent moved to langchain-classic. Some community integrations promoted to standalone packages (langchain-chroma, langchain-pinecone, etc.). Chapter 2 has the full map. Fix: install the correct package and update the import path.
Should I migrate off AgentExecutor?
Yes, before December 2026 for any code path that's business-critical. AgentExecutor's removal deadline is December 2026 — after that, it stays available only via langchain-classic as permanent tech debt. Migration to create_agent is mostly mechanical for simple agents; complex agents with custom scratchpad or output parsing need more thought. Chapter 8 walks the process.
What's the difference between create_agent and create_react_agent?
Same thing, moved and renamed. create_agent (in langchain.agents) replaces create_react_agent (from langgraph.prebuilt), which is now deprecated. Two API changes: import path and the parameter name (system_prompt instead of prompt). New code should use create_agent. Existing create_react_agent code should migrate.
Why did response.content stop being a string?
v1 changed chat model responses to use typed content blocks — instead of one opaque string, responses come back as a list of blocks (text, reasoning, tool_call, citation, image). Access the text via response.text for the string-like behavior, or iterate response.content blocks when you care about reasoning, citations, or tool calls specifically. This is a breaking change for any pre-v1 code that assumed response.content was a string.
Do I need langgraph if I only use create_agent?
Not directly — create_agent uses LangGraph internally but you don't have to import it. langgraph is a transitive dependency. You'd only import langgraph directly if you need: custom state graphs, human-in-the-loop interrupts beyond what HumanApprovalMiddleware offers, multi-actor workflows, or specific checkpointers.
What's the right way to add memory to a create_agent?
Use a LangGraph checkpointer. For production: Postgres (langgraph-checkpoint-postgres), Redis (langgraph-checkpoint-redis), or another durable store. Pass it via create_agent(model, tools, checkpointer=checkpointer) and thread messages by thread_id in the config. Legacy ConversationBufferMemory classes (now in langchain-classic) do NOT integrate with create_agent — they're for AgentExecutor-era code.
Why is my langchain-community integration behaving differently after a patch bump?
langchain-community integrations are community-maintained and don't follow the core team's deprecation notice cycle. Interface changes between minor and even patch versions are common. Fix: pin the exact version (langchain-community==0.3.5), test upgrades in staging with golden-file tests on integration behavior, and consider migrating critical integrations to standalone packages (langchain-chroma, langchain-pinecone, etc.) which are better-maintained.
Do I need LangSmith?
Not required — LangChain runs fine without it. But for production, LangSmith's traces + evals loop is one of the strongest arguments for staying on LangChain vs calling SDKs directly. If you have your own observability (OTel, Datadog) covering LLM calls with equivalent depth (inputs, outputs, tokens, per-run tree), LangSmith is optional. If you don't, the setup is a few environment variables and you get production-grade LLM observability immediately.
How do I add fallback to a cheaper model on rate limits?
Two ways: (1) ModelFallbackMiddleware on a create_agent — falls back automatically on model errors. (2) Runnable.with_fallbacks([backup_chain]) on any LCEL chain. For rate-limit specific behavior, prefer middleware — you can inspect the exception type and only fall back on 429s, not on all errors.
Why do my LangSmith traces vanish in Lambda?
Callbacks in v1 are backgrounded — they flush asynchronously. In serverless, if your function returns before the flush completes, traces are lost. Fix: call langsmith.Client().flush() before the function returns, or add a small wait. See backgrounded callbacks.
Should I use LangChain or just call the provider SDK?
Call the SDK directly when: one provider, no tool-calling loop, own observability, own retrieval. Use LangChain when: cross-provider abstraction, RAG boilerplate, agent+middleware needs, LangSmith integration. Framework doesn't have to be all-or-nothing — many teams keep LangChain for agents but replace it for retrieval, or vice versa. Chapter 11 has the decision matrix.
What's with_structured_output and when does strict mode work?
with_structured_output(pydantic_model) forces a chat model to return a Pydantic instance instead of free text. strict=True uses the provider's native structured-output feature (OpenAI structured outputs, Anthropic constrained decoding) for guaranteed schema compliance. Strict mode has schema constraints: no union types with more than 2 members, no arbitrary dicts, no recursive schemas. Test the schema before shipping. See strict mode compatibility.
Why does .batch() trigger provider rate limits when .invoke() doesn't?
Default max_concurrency on batch() is unbounded — sending 1,000 inputs sends 1,000 concurrent requests, which hits any provider's RPM limit. Fix: pass config={"max_concurrency": 10} (or whatever your provider rate limit accommodates). See batch concurrency throttling.
How do I handle a middleware that only sometimes fires?
Almost always a middleware ordering issue. Middleware stack executes in list order, but each middleware wraps subsequent ones — so a later middleware that intercepts events an earlier one already handled will never fire. Trace via LangSmith to see which middlewares actually run per invocation, then reorder. See middleware ordering.
The full library — 32 LangChain error pages
Every LangChain error-fix page on AI Error Hub, organized by category. Bookmark this section — when you hit a specific error, jump straight to the page that walks through it with tested code.