LangChain in Production: Patterns That Work and Anti-Patterns That Don't
Lessons from running LangChain in production: the anti-patterns that cause failures, the patterns that work, with code examples and cost optimization strategies.
The Production Gap
Moving LangChain applications from prototype to production reveals a gap between documentation examples and real-world requirements. What works perfectly in development can become costly, slow, or unreliable under production load.
Prototype workloads hide failure modes that only surface at scale: agents that loop on ambiguous inputs, token spend that climbs with every conversation turn, and silent failures that reach you as user complaints. The framework’s abstractions accelerate prototyping but obscure the cost, latency, and reliability levers you need under production load.
The default that holds up: bound every resource explicitly (memory tokens, agent iterations, execution time, output length), wire tracing in before the first chain runs, and drop to the provider SDK wherever the abstraction buys you nothing.
Understanding the Framework Trade-off
LangChain solved early LLM integration complexity by providing standard abstractions for prompts, chains, agents, and memory management. This made prototyping significantly faster. What might take weeks with direct API calls could be done in days.
However, these abstractions introduce their own challenges:
The velocity-control trade-off: Rapid prototyping comes at the cost of transparency. When something goes wrong in production, debugging through multiple abstraction layers becomes significantly harder than debugging a direct API call.
Hidden behaviors: Framework internals make decisions that aren’t always visible: memory trimming strategies, automatic retries, callback execution order. These work fine until they don’t, and diagnosing why requires deep-diving into source code.
Performance overhead: Each abstraction layer adds latency. Memory wrappers, callback systems, and automatic processing all run on every request. Profile them before assuming the LLM call dominates; on short prompts the framework’s own work can rival it.
The framework inflection point occurs when your team spends more time debugging framework behavior than building features. Some teams hit this quickly, others never do. Recognising the crossing matters more than the exact threshold, because past it the abstractions cost you the velocity they were bought for.
The 7 Deadly Anti-Patterns
1. Unbounded Memory Accumulation
The default ConversationBufferMemory stores unlimited conversation history:
from langchain.memory import ConversationBufferMemory
# Anti-pattern: This accumulates unbounded history
# Note: ConversationBufferMemory is deprecated - use LangGraph persistence
# or RunnableWithMessageHistory for new projects
memory = ConversationBufferMemory()
# After 50 exchanges: massive context, slow responses, high costs
Impact: Every request resends the full history, so cost and latency both grow with conversation length. Eventually the context window overflows and requests start failing outright.
Detection: Monitor token usage trends over time. Watch for growing response times as conversations progress.
Solution: Use ConversationSummaryBufferMemory with explicit limits (or migrate to LangGraph persistence):
from langchain.memory import ConversationSummaryBufferMemory
# Note: ConversationSummaryBufferMemory is deprecated
# For new projects, use LangGraph persistence or RunnableWithMessageHistory
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=500, # Keep recent context compact
return_messages=True
)
# Older turns collapse into a summary, so tokens per request stay bounded
2. Agent Without Guardrails
Creating agents without execution controls:
from langchain.agents import AgentExecutor
# Anti-pattern: No limits on execution
executor = AgentExecutor(agent=agent, tools=tools)
# Nothing here stops the agent bouncing between search and summarize forever
Impact: A ReAct loop that never satisfies its stop condition keeps calling tools until something external kills it. Every iteration is a paid LLM call, and the user is staring at a spinner the whole time.
Detection: Set up cost alerts and execution time monitoring before production.
Solution: Explicit controls in configuration:
from langchain.agents import AgentExecutor
from langchain.callbacks import get_openai_callback
executor = AgentExecutor(
agent=agent,
tools=tools,
max_iterations=5, # Prevent infinite loops
max_execution_time=30, # Timeout after 30 seconds
early_stopping_method="generate"
)
# Note: get_openai_callback may not capture costs for newer agent types
# Consider using LangSmith for comprehensive cost tracking
with get_openai_callback() as cb:
result = executor.run(query)
print(f"Tokens: {cb.total_tokens}, Cost: ${cb.total_cost}")
3. Over-Abstraction for Simple Tasks
Using full LangChain abstractions for straightforward operations:
// Anti-pattern: 5 layers of abstraction for simple completion
import { ChatOpenAI } from "langchain/chat_models/openai";
import { ChatPromptTemplate } from "langchain/prompts";
import { StringOutputParser } from "langchain/schema/output_parser";
const chatModel = new ChatOpenAI();
const outputParser = new StringOutputParser();
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful translator."],
["user", "Translate {text} to {language}"]
]);
const chain = prompt.pipe(chatModel).pipe(outputParser);
// Direct API: Same result, no framework overhead
import OpenAI from "openai";
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{ role: "system", content: "You are a helpful translator." },
{ role: "user", content: `Translate ${text} to ${language}` }
]
});
Impact: Unnecessary complexity, harder debugging, team cognitive load for tasks that don’t benefit from abstractions.
Detection: Code review: count abstraction layers for simple operations. If you’re importing 4+ modules for a basic completion, consider direct API usage.
4. Hidden Latency Overhead
Framework components can add significant latency:
from langchain.memory import ConversationBufferWindowMemory
# Anti-pattern: wrapper work on every call, never measured
memory = ConversationBufferWindowMemory(k=5)
# Worth profiling: serialisation and callback dispatch happen per invocation
Impact: Poor user experience, difficulty scaling to higher request volumes.
Detection: Profile with and without framework components. Measure end-to-end latency versus direct API call time.
Solution: For performance-critical paths, implement custom lightweight alternatives:
# Custom trimmed memory - keeps last N messages efficiently
class LightweightMemory:
def __init__(self, max_messages=10):
self.messages = []
self.max_messages = max_messages
def add_message(self, message):
self.messages.append(message)
if len(self.messages) > self.max_messages:
self.messages = self.messages[-self.max_messages:]
def get_context(self):
return self.messages
# No callback dispatch, no serialisation round-trip on the hot path
5. Shipping Development Defaults
Production deployments with development defaults:
# Anti-pattern: Development defaults in production
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI()
# No caching, no output limits, no cost controls
Impact: High operational costs, slow responses, verbose logging filling disk space.
Detection: Baseline cost and latency metrics before production launch.
Solution: Explicit production configuration:
from langchain.chat_models import ChatOpenAI
from langchain.cache import RedisCache
from langchain.globals import set_llm_cache
import redis
# Production-ready configuration
set_llm_cache(RedisCache(
redis_=redis.Redis(host="localhost", port=6379)
))
llm = ChatOpenAI(
model="gpt-4",
temperature=0.7,
max_tokens=512, # Limit output length
request_timeout=30, # Timeout for API calls
max_retries=2 # Controlled retry behavior
)
6. Black-Box Agent Behavior
Deploying agents without observability:
# Anti-pattern: No visibility into agent decisions
executor = AgentExecutor(agent=agent, tools=tools)
result = executor.run(query)
# When this fails silently, you have no idea why
Impact: Silent failures, impossible debugging, discovering issues only through user complaints.
Detection: There is nothing to detect until traces exist, which is precisely the anti-pattern.
Solution: LangSmith tracing from day one:
import os
from langchain.callbacks.tracers import LangChainTracer
# Enable tracing in environment
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-api-key"
# Automatic tracing of all chains, agents, tools
# Track: latency, costs, tokens, failures, decision paths
executor = AgentExecutor(agent=agent, tools=tools)
result = executor.run(query)
# All execution details now visible in LangSmith dashboard
7. Underestimating Data Ingestion
Underestimating RAG pipeline complexity:
# Anti-pattern: Assuming document loading "just works"
from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader("document.pdf")
documents = loader.load()
# Parser choice, encoding and chunking are where RAG projects actually stall
Impact: Wrong PDF parser for your document types, encoding issues with international text, chunking problems that degrade retrieval quality.
Detection: High failure rates in document processing, poor retrieval results.
Solution: Thorough testing of data loaders with multiple strategies:
from langchain.document_loaders import PyPDFLoader, PDFMinerLoader, UnstructuredPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Try multiple parsers, test with real documents
parsers = [
PyPDFLoader,
PDFMinerLoader,
UnstructuredPDFLoader
]
for ParserClass in parsers:
try:
loader = ParserClass("document.pdf")
docs = loader.load()
# Validate output quality
if validate_extraction(docs):
break
except Exception as e:
print(f"{ParserClass.__name__} failed: {e}")
# Thoughtful chunking strategy
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200, # Maintain context across chunks
length_function=len
)
chunks = splitter.split_documents(docs)
Production-Ready Patterns
Pattern 1: LCEL-First Architecture
Modern LangChain applications use LCEL (LangChain Expression Language) for better composability:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
# LCEL: Readable pipe syntax with built-in streaming
chain = (
ChatPromptTemplate.from_template("Analyze: {input}")
| ChatOpenAI(model="gpt-4", streaming=True)
| StrOutputParser()
)
# Supports streaming, batching, async out of the box
for chunk in chain.stream({"input": query}):
print(chunk, end="", flush=True)
Benefits: Clear composition, built-in async support, easier debugging compared to legacy chains.
When to use: Complex workflows requiring multiple LLM calls, transformations, or conditional logic.
Pattern 2: Explicit Resource Controls
Production configuration should make limits explicit:
from langchain.agents import AgentExecutor
from langchain.callbacks import get_openai_callback
# All limits explicit and documented
executor = AgentExecutor(
agent=agent,
tools=tools,
max_iterations=5, # Stop after 5 tool calls
max_execution_time=30, # Hard timeout at 30 seconds
early_stopping_method="generate", # Graceful degradation
verbose=False # Disable debug logging in production
)
# Cost tracking on every request
with get_openai_callback() as cb:
result = executor.run(query)
# Alert if costs exceed threshold
if cb.total_cost > 0.10:
send_alert(f"High cost request: ${cb.total_cost}")
Implementation checklist:
- Token limits on memory and outputs
- Agent iteration caps and timeouts
- Cost budgets and alerts
- Retry limits and exponential backoff
Pattern 3: Multi-Tier Caching Strategy
Caching dramatically reduces costs and latency:
from langchain.cache import InMemoryCache, SQLiteCache, RedisCache
from langchain.globals import set_llm_cache
import redis
# Development: In-memory cache
# set_llm_cache(InMemoryCache())
# Local persistence: SQLite
# set_llm_cache(SQLiteCache(database_path=".langchain.db"))
# Production: Distributed Redis cache
set_llm_cache(RedisCache(
redis_=redis.Redis(
host="redis.production.internal",
port=6379,
db=0
)
))
# Cache configuration
# TTL: 1 year for static content, 1 day for dynamic
# Invalidation: Manual or event-driven for updated content
Trade-off: A cache hit skips the API call entirely, so the payoff tracks how repetitive your traffic actually is. Measure the hit rate before you count the savings, and remember that a stale hit is worse than a slow miss for anything time-sensitive.
Pattern 4: Observability-First Development
Set up tracing before writing your first chain:
import os
from langchain.callbacks.base import BaseCallbackHandler
# LangSmith tracing configuration
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-api-key"
os.environ["LANGCHAIN_PROJECT"] = "production-app"
# Custom callback for business metrics
class ProductionMetricsCallback(BaseCallbackHandler):
def on_llm_start(self, serialized, prompts, **kwargs):
self.start_time = time.time()
def on_llm_end(self, response, **kwargs):
latency = time.time() - self.start_time
tokens = response.llm_output.get("token_usage", {})
# Send to your monitoring system
metrics.record("llm.latency", latency)
metrics.record("llm.tokens", tokens.get("total_tokens", 0))
metrics.record("llm.cost", calculate_cost(tokens))
# Use in all chain executions
callbacks = [ProductionMetricsCallback()]
result = chain.invoke({"input": query}, config={"callbacks": callbacks})
Key metrics to track:
- Performance: QPS, latency percentiles (p50, p95, p99), time-to-first-token
- Cost: Total tokens, cost per request, daily burn rate
- Quality: Error rates, retry counts, user feedback
- Agent behavior: Tool selections, iteration counts, decision paths
Pattern 5: Smart Model Routing
Route requests to appropriate models based on complexity:
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
# Define models with cost/capability trade-offs
cheap_model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
premium_model = ChatOpenAI(model="gpt-4", temperature=0.7)
def route_to_model(query: str):
"""Route based on query complexity"""
complexity_score = analyze_complexity(query)
if complexity_score < 0.3:
return cheap_model # GPT-3.5-turbo: $0.0005/1K input, $0.0015/1K output
else:
return premium_model # GPT-4: $0.03/1K input, $0.06/1K output
# Consider GPT-4o mini for cost-effective option: $0.00015/1K input, $0.0006/1K output
# Dynamic routing in chain
def create_chain(query: str):
model = route_to_model(query)
prompt = ChatPromptTemplate.from_template("{input}")
return prompt | model
# Example complexity analysis
def analyze_complexity(query: str) -> float:
"""Simple heuristic-based complexity scoring"""
score = 0.0
# Length-based scoring
if len(query.split()) > 50:
score += 0.3
# Technical term detection
technical_terms = ["architecture", "algorithm", "performance", "optimization"]
if any(term in query.lower() for term in technical_terms):
score += 0.4
# Multi-step reasoning indicators
if any(word in query.lower() for word in ["compare", "analyze", "explain why"]):
score += 0.3
return min(score, 1.0)
Trade-off: Savings scale with the share of traffic the cheap model can absorb, and every misroute buys that saving with a worse answer. Score the router against a labelled sample before trusting it, and keep the escalation path open so a low-confidence result can be retried on the premium model.
Pattern 6: Structured Outputs with Pydantic
Type-safe outputs reduce post-processing bugs:
from langchain.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
# Define output schema
class ProductAnalysis(BaseModel):
sentiment: str = Field(description="positive, negative, or neutral")
key_features: list[str] = Field(description="list of mentioned features")
price_mentioned: bool = Field(description="whether price was discussed")
confidence_score: float = Field(description="confidence from 0 to 1")
# Parser with schema validation
parser = PydanticOutputParser(pydantic_object=ProductAnalysis)
# Prompt includes format instructions
prompt = PromptTemplate(
template="Analyze this product review:\n{review}\n{format_instructions}",
input_variables=["review"],
partial_variables={"format_instructions": parser.get_format_instructions()}
)
chain = prompt | ChatOpenAI(model="gpt-4") | parser
# Type-safe output
result: ProductAnalysis = chain.invoke({"review": review_text})
print(f"Sentiment: {result.sentiment}, Confidence: {result.confidence_score}")
Benefits: Type safety, automatic validation, clear contracts between LLM and downstream code.
The Migration Decision Matrix
Choosing the right approach depends on your specific requirements:
When to Use LangChain
- Complex multi-agent systems requiring orchestration
- RAG with multiple retrievers and re-ranking
- Teams needing standard abstractions for collaboration
- Rapid prototyping phase with plans for production hardening
- Heavy reliance on LangSmith observability ecosystem
Example: LinkedIn’s SQL Bot uses LangChain chains wrapped in LangGraph nodes for production-grade multi-agent coordination.
When to Use LlamaIndex
- Primary focus on search and retrieval
- Large dataset indexing requirements
- Need for efficient semantic similarity search
- Simpler, more focused use case than general orchestration
When to Use Direct APIs
- Simple chatbot or completion tasks
- Clear, unchanging requirements
- Performance-critical applications where latency matters
- Small team wanting full control
- Minimal external dependencies desired
Example implementation:
from openai import OpenAI
client = OpenAI()
# Clear, explicit, fast
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt}
],
max_tokens=512,
temperature=0.7
)
answer = response.choices[0].message.content
When to Migrate Away from LangChain
Consider migration when:
- Team spends more time debugging framework behavior than building features
- Performance profiling puts framework overhead ahead of the LLM call on the critical path
- Requirements don’t fit LangChain’s patterns and you’re fighting the framework
- Dependency management becomes a maintenance burden
Migration approach: Incremental replacement, starting with highest-impact components. Keep what works, replace what doesn’t.
LangGraph: Production Evolution
LangGraph emerged in 2024 as a production-focused evolution, designed from lessons learned deploying LangChain agents:
Key differences:
- Low-level, controllable framework without hidden behaviors
- No hidden prompts or automatic cognitive architecture
- Durable execution for complex agentic systems
- State management across long-running workflows
Hybrid pattern:
from langgraph.graph import StateGraph, END
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# Define state
class AgentState(dict):
messages: list[str]
current_step: str
# Use LangChain for LLM interactions
analysis_chain = (
ChatPromptTemplate.from_template("Analyze: {input}")
| ChatOpenAI(model="gpt-4")
)
# Wrap in LangGraph nodes for orchestration
workflow = StateGraph(AgentState)
def analyze_node(state: AgentState):
result = analysis_chain.invoke({"input": state["messages"][-1]})
state["messages"].append(result)
return state
workflow.add_node("analyze", analyze_node)
workflow.add_edge("analyze", END)
workflow.set_entry_point("analyze")
# Best of both: LangChain composability + LangGraph control
app = workflow.compile()
When to upgrade: Moving from AgentExecutor to LangGraph, need for multi-agent coordination, state management across long-running workflows, production reliability requirements.
Companies using LangGraph in production: Uber, LinkedIn, Replit, Elastic.
Cost Optimization Strategies
Token Management
Track and control token usage aggressively:
from langchain.callbacks import get_openai_callback
# 1. Track everything
# Note: get_openai_callback has limitations with newer agent implementations
# Use LangSmith for comprehensive tracking across all agent types
with get_openai_callback() as cb:
result = chain.invoke({"input": query})
print(f"Tokens: {cb.total_tokens}, Cost: ${cb.total_cost:.4f}")
# 2. Trim context to last N exchanges
from langchain.memory import ConversationBufferWindowMemory
# Note: ConversationBufferWindowMemory is deprecated
# For new projects, use LangGraph persistence or RunnableWithMessageHistory
memory = ConversationBufferWindowMemory(
k=5, # Keep only last 5 exchanges
return_messages=True
)
# 3. Smart summarization for older context
from langchain.memory import ConversationSummaryBufferMemory
# Note: ConversationSummaryBufferMemory is deprecated
# Migrate to LangGraph persistence for production applications
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=500,
return_messages=True
)
# 4. Explicit output limits
llm = ChatOpenAI(
model="gpt-4",
max_tokens=512 # Concise responses
)
Where the Savings Come From
The four levers compound, and they compound in a specific order. Routing changes the price per token, so apply it first; it has the largest effect and the largest quality risk. Memory trimming and output limits change the token count per request, which multiplies against whatever price the router picked. Caching removes requests entirely, so it sits on top of all three.
Model your own numbers before promising anything: take the published per-token prices for the models you route between, multiply by your measured tokens per request, and only then apply your observed cache hit rate. Savings quoted without a measured hit rate and a measured routing split are guesses.
Monitoring and Observability
Essential Production Metrics
import time
from langchain.callbacks.base import BaseCallbackHandler
class ProductionMetrics(BaseCallbackHandler):
"""Comprehensive production monitoring"""
def on_chain_start(self, serialized, inputs, **kwargs):
self.chain_start = time.time()
def on_chain_end(self, outputs, **kwargs):
duration = time.time() - self.chain_start
metrics.gauge("chain.duration", duration)
def on_llm_start(self, serialized, prompts, **kwargs):
self.llm_start = time.time()
metrics.increment("llm.requests")
def on_llm_end(self, response, **kwargs):
# Performance metrics
latency = time.time() - self.llm_start
metrics.gauge("llm.latency", latency)
# Cost metrics
usage = response.llm_output.get("token_usage", {})
total_tokens = usage.get("total_tokens", 0)
cost = calculate_cost(usage)
metrics.gauge("llm.tokens", total_tokens)
metrics.gauge("llm.cost", cost)
def on_llm_error(self, error, **kwargs):
metrics.increment("llm.errors")
logger.error(f"LLM error: {error}")
def on_tool_start(self, serialized, input_str, **kwargs):
tool_name = serialized.get("name", "unknown")
metrics.increment(f"tool.{tool_name}.calls")
def on_agent_action(self, action, **kwargs):
metrics.increment("agent.actions")
LangSmith Integration
LangSmith provides automatic tracing without code changes:
import os
# Environment configuration
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-langsmith-api-key"
os.environ["LANGCHAIN_PROJECT"] = "production-app"
# Optional: Add metadata for filtering
from langchain.callbacks.tracers import LangChainTracer
tracer = LangChainTracer(
project_name="production-app",
tags=["prod", "version-2.1"]
)
# All chain executions automatically traced
result = chain.invoke(
{"input": query},
config={"callbacks": [tracer]}
)
What LangSmith tracks:
- Execution traces with timing for each step
- Token usage and costs per request
- Agent decision paths and tool selections
- Error rates and failure patterns
- A/B test comparisons with metadata tags
Migration Patterns
From LangChain to Custom Code
Incremental approach minimizes risk:
# Week 1: Profile to find the component with the worst cost/latency ratio
# Week 2: Create custom replacement
class EfficientMemory:
def __init__(self, max_messages=10):
self.messages = []
self.max_messages = max_messages
def add(self, message):
self.messages.append(message)
if len(self.messages) > self.max_messages:
self.messages = self.messages[-self.max_messages:]
def get_context(self):
return "\n".join(self.messages)
# Week 3: A/B test implementations
# Group A: LangChain memory (baseline)
# Group B: Custom memory (test)
# Week 4: Compare on p95 latency, tokens per request, answer quality
# Week 5+: Gradual rollout
# 10% → 50% → 100% over 2 weeks
From Legacy Chains to LCEL
LangChain provides migration tooling:
# Automated migration assistance
langchain migrate --legacy-to-lcel chain.py
Manual migration example:
# Legacy: initialize_agent pattern (deprecated)
from langchain.agents import initialize_agent, AgentType
agent = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION
)
# Modern: LangGraph prebuilt (recommended)
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
model=llm,
tools=tools
)
Benefits: Better composability, built-in streaming, clearer debugging, full control over agent behavior.
Common Pitfalls and Lessons
Pitfall 1: Prototype-to-Production Trap
Pattern: Prototype with defaults works fine in development. Production reveals high costs, slow responses, silent failures.
Lesson: Design for production from day one. Set resource limits, implement caching, add observability before the first production deployment.
Pitfall 2: Unnoticed Framework Lock-In
Pattern: Start with LangChain for rapid prototyping. Six months later, deeply coupled architecture makes migration months of work.
Lesson: Keep framework usage at boundaries. Core business logic should be framework-agnostic. This makes future changes manageable.
Pitfall 3: Leaving Observability for Later
Pattern: Launch without tracing or monitoring. Discover production issues through user complaints with no way to debug what happened.
Lesson: Wire LangSmith or an equivalent in at project start. Retrofitting tracing while a live incident is running is the expensive path.
Pitfall 4: Agent Autonomy Without Guardrails
Pattern: Trust the agent to “figure it out” without controls. The tool-calling loop then runs until the budget or the user’s patience gives out.
Lesson: Treat max iterations, timeouts, and cost budgets as required configuration. An agent is useful precisely because it chooses its own control flow, which is why the bounds have to come from outside it.
Where This Default Holds
Explicit bounds, tracing from day one, and a willingness to drop to the provider SDK cover most LangChain deployments: RAG services, internal assistants, and agent workflows with a handful of tools. The price of the discipline is a few dozen lines of configuration and one observability dependency, paid once.
Override it in two directions. Below the line, a single-prompt service with stable requirements does not need the framework at all; the provider SDK plus a cache is less code to write and less code to debug. Above the line, multi-agent systems with long-running state outgrow AgentExecutor well before they outgrow LangChain itself, and that is the moment to move orchestration into LangGraph while keeping LCEL chains for the LLM calls.
One rule has no override: nothing ships without iteration caps, timeouts, output limits, and traces. The rest is a judgement call you can revisit after the first month of real traffic.
References
- LangChain Documentation - Official LangChain overview covering chains, agents, memory, and retrieval abstractions
- LangSmith Observability - Official guide to tracing, monitoring, and evaluating LangChain applications in production
- LangGraph Overview - The orchestration framework behind durable execution, persistence, and state management for long-running agents
- GitHub - langchain-ai/langchain - Main LangChain repository with source, examples, and community integrations
- LangChain Python API Reference - Full API reference for LangChain Python classes, callbacks, and LCEL expressions
- LangChain Models - Model initialization, timeouts, retries, token limits, and structured output settings
Related posts
A technical guide to production-grade prompt engineering: systematic design, security, observability, and cost optimization for enterprise LLM apps.
An implementation-focused glossary for developers navigating the AI/LLM landscape - from tokens to agents, RAG to fine-tuning, with code examples.
Prompt caching, model routing, token budgets, and semantic caching: how to keep production LLM spend predictable without giving up answer quality.
How systematic database profiling and optimization reduced infrastructure costs significantly. PostgreSQL and MongoDB performance insights and practical patterns.
Multi-environment deployment, performance optimization at scale, cost management, and monitoring with solid incident response patterns.