Let's be honest about LangChain.
Everyone uses it. It has 100,000 GitHub stars. When you Google "how to build an LLM agent," 90% of results use LangChain.
It's also abstraction hell that makes production debugging impossible.
The pattern plays out the same way across teams:
Week 1: "This is amazing! Look how fast we built an agent!"
Week 4: "Why did the agent call the wrong tool?"
Week 8: "I've been debugging for 3 hours. The stack trace goes through 47 LangChain files. I still don't know what's broken."
Week 12: "We rewrote it in plain Python. It works now."
This isn't about framework preferences. It's about choosing tools you can actually debug when they break at 3am.
Mirascope is what LangChain should have been: clean Python, type-safe, debuggable.
Debugging time: 3 hours → 30 minutes Onboarding time: 1-2 weeks → 2-3 days Maintenance over 3 years: 340 hours → 42 hours
The LangChain Problem: Abstraction Layers That Hide Everything
Here's the standard LangChain journey:
A team starts building an AI agent. LangChain has pre-built components for everything. Chains, Agents, Memory, OutputParsers. Progress is fast.
Then they hit production.
The agent occasionally fails. The error message:
File "langchain/chains/base.py", line 156, in __call__
File "langchain/chains/llm.py", line 92, in _call__
File "langchain/prompts/base.py", line 124, in format
File "langchain/schema/prompt_template.py", line 87, in format
KeyError: 'context'
Which file has the bug? The chain configuration? The prompt template? The memory management? The output parser?
Three hours later: Turns out a nested chain wasn't passing the right variable name. The abstraction layers made it impossible to see what was happening.
What LangChain Actually Does: Hide Complexity With More Complexity
LangChain's value proposition: "We abstract away the complexity of LLM orchestration."
Translation: "We add so many layers that you can't see what's actually happening."
To call an LLM and get structured output in LangChain:
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
class ContactInfo(BaseModel):
name: str = Field(description="The person's name")
email: str = Field(description="The person's email")
llm = ChatOpenAI(model="gpt-4o-mini")
parser = PydanticOutputParser(pydantic_object=ContactInfo)
prompt = ChatPromptTemplate.from_template(
"Extract contact info from: {text}\n{format_instructions}"
)
chain = prompt | llm | parser # What is this syntax?
result = chain.invoke({
"text": "Contact John Doe at john@example.com",
"format_instructions": parser.get_format_instructions()
})
18 lines of code. And you still don't know what parser.get_format_instructions() actually does. Or why you need to pipe things with |. Or what happens when the LLM returns malformed JSON.
The same thing in Mirascope:
from mirascope.core import openai, prompt_template
from pydantic import BaseModel
class ContactInfo(BaseModel):
name: str
email: str
@openai.call(model="gpt-4o-mini", response_model=ContactInfo)
@prompt_template("Extract contact info from: {text}")
def extract_contact(text: str): ...
result = extract_contact("Contact John Doe at john@example.com")
8 lines of code. You can see exactly what's happening. If the LLM returns malformed JSON, Pydantic validation catches it with a clear error.
No chains. No parsers. No format instructions. Just Python.
Why "The Best Engineers Avoid Heavy Frameworks"
From our How to Evaluate AI Engineers post:
"We/I built this with the latest framework (LangChain/LlamaIndex/etc.)"
- What it signals: Keeps up with trends
- What it doesn't signal: Understands fundamentals
- Reality: The best AI engineers often avoid heavy frameworks in favor of direct API calls and custom tooling
LangChain isn't a tool. It's a philosophy that says "you can't handle the complexity, so we'll abstract it away."
But the complexity doesn't disappear. It just gets hidden behind layers of Chains, Agents, Memory, and LCEL.
When it works, it's fast. When it breaks, you're screwed.
The Vendor Lock-In Nobody Talks About
LangChain doesn't lock you in through licensing. It locks you in through cognitive complexity.
Once you've built your system with:
- LLMChain wrapping SequentialChain wrapping RouterChain
- ConversationBufferMemory with VectorStoreRetrieverMemory fallback
- PydanticOutputParser with custom validation hooks
- LCEL expressions you don't fully understand
You can't rewrite it. Not without starting over.
That's lock-in. And it's worse than vendor lock-in because you can't even pay to get out.
The Observability Problem
LangChain's solution: LangSmith (their paid observability service, $39-199/month).
So now you're locked into:
- LangChain's abstractions
- LangSmith's observability
- Their ecosystem forever
With Mirascope: Use any observability tool (Langfuse, custom logging, whatever). Or just add print statements—you can actually see what's happening.
What We Actually Need from an LLM Framework
Let's be specific about requirements:
Core Functionality:
- Call LLMs (OpenAI, Anthropic, etc.)
- Get structured outputs with type validation
- Tool/function calling
- Multi-step workflows
- Streaming
- Error handling
Development Experience:
- Type safety (catch bugs before production)
- Easy to debug when things break
- Clear error messages
- Minimal boilerplate
- Code we can understand in 6 months
Production Requirements:
- Observability (trace what the LLM is doing)
- Reliable error handling
- Testable code
- No vendor lock-in
Philosophical:
- Explicit over implicit
- Simple over comprehensive
- Debuggable over magical
LangChain delivers on "core functionality" but fails spectacularly on everything else.
Mirascope nails all of it.
Mirascope: What LLM Frameworks Should Have Been
Mirascope is what you'd build if you stripped LangChain down to essentials and made it debuggable.
Type Safety Everywhere
Mirascope uses Pydantic for everything:
from mirascope.core import openai, prompt_template
from pydantic import BaseModel
class UserInfo(BaseModel):
name: str
age: int
email: str
@openai.call(model="gpt-4o-mini", response_model=UserInfo)
@prompt_template("Extract user info: {text}")
def extract_user(text: str): ...
result = extract_user("John Doe, 32, john@example.com")
print(result.name) # IDE autocompletes
print(result.age) # Type is int, not str
print(result.phone) # Error: 'UserInfo' has no attribute 'phone'
Your IDE catches bugs at dev time. Type checking works. No runtime surprises.
With LangChain: Loose typing everywhere. Runtime errors in production when you misspell a field.
Explicit Control Flow
Multi-step agent in Mirascope:
from mirascope.core import openai, prompt_template
def search_web(query: str) -> list[str]:
"""Search the web and return results."""
return ["Result 1...", "Result 2...", "Result 3..."]
@openai.call(model="gpt-4o-mini", tools=[search_web])
@prompt_template("Search and summarize: {query}")
def search_agent(query: str): ...
def run_agent(query: str):
# Step 1: Search
response = search_agent(query)
if tool := response.tool:
results = tool.call()
# Step 2: Summarize
@openai.call(model="gpt-4o-mini")
@prompt_template("Summarize: {results}")
def summarize(results: list[str]): ...
return summarize(results).content
return response.content
You can see exactly what's happening. Search → summarize. Add logging between steps. Handle errors at each step. Debug with print statements.
With LangChain: The agent decides when to search and when to summarize. When it makes the wrong decision, you're debugging the agent's decision-making process through abstraction layers. The verbose logs help, but good luck.
Debugging That Actually Works
Mirascope error:
Traceback (most recent call last):
File "your_app.py", line 42, in extract_user
return extract_user(text)
File "mirascope/core.py", line 87, in __call__
return self.response_model(**response)
pydantic.error_wrappers.ValidationError: 1 validation error for UserInfo
age
value is not a valid integer
3 files in the stack trace. You immediately see the problem: age field validation failed.
LangChain error:
Traceback (most recent call last):
File "your_app.py", line 42, in main
result = chain.invoke(input_dict)
File "langchain/chains/base.py", line 156, in invoke
return self._call(inputs)
File "langchain/chains/llm.py", line 92, in _call
return self._generate(prompts)
File "langchain/chains/llm.py", line 112, in _generate
output = self.llm.generate_prompt(prompts)
File "langchain/llms/base.py", line 456, in generate_prompt
return self.generate(prompt_strings)
File "langchain/llms/openai.py", line 523, in generate
response = completion_with_retry(self, **params)
File "langchain/llms/openai.py", line 167, in completion_with_retry
return _completion_with_retry(**kwargs)
... 15 more LangChain files ...
KeyError: 'context'
20+ files in the stack trace. The actual error is buried. Spend an hour understanding which abstraction layer failed.
Tool Calling Without the Magic
With Mirascope:
from mirascope.core import openai, prompt_template
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"Weather in {city}: Sunny, 72°F"
@openai.call(model="gpt-4o-mini", tools=[get_weather])
@prompt_template("What's the weather in {city}?")
def weather_agent(city: str): ...
response = weather_agent("San Francisco")
if tool := response.tool:
result = tool.call()
print(result)
Clean. Explicit. You can see exactly what's happening. No AgentExecutor. No ReAct prompts. No agent scratchpad.
With LangChain:
from langchain.agents import AgentExecutor, create_react_agent
from langchain.chat_models import ChatOpenAI
from langchain.prompts import PromptTemplate
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"Weather in {city}: Sunny, 72°F"
llm = ChatOpenAI(model="gpt-4o-mini")
tools = [get_weather]
prompt = PromptTemplate.from_template(
"Answer the following questions as best you can. You have access to the following tools:\n\n{tools}\n\nUse the following format:\n\nQuestion: {input}\nThought: you should always think about what to do\nAction: the action to take, should be one of [{tool_names}]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Observation can repeat N times)\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n\nBegin!\n\nQuestion: {input}\n{agent_scratchpad}"
)
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)
result = agent_executor.invoke({"input": "What's the weather in San Francisco?"})
print(result["output"])
Significantly more code. You're configuring an AgentExecutor, defining a ReAct prompt template, understanding agent_scratchpad. When it breaks, good luck.
The Ecosystem Trade-Off (And Why It Doesn't Matter)
"But LangChain has 100,000 GitHub stars! Massive ecosystem! Pre-built integrations!"
True. And none of it matters if you can't debug production issues.
What LangChain's Ecosystem Gets You
- 300+ pre-built integrations (document loaders, vector stores, etc.)
- LangSmith observability platform
- Massive community (tutorials everywhere)
- Agent implementations (ReAct, Self-Ask, Plan-and-Execute)
- Every possible abstraction for every possible use case
What You Actually Need
- Call LLMs
- Get structured outputs
- Call functions/tools
- Chain steps together
- Log what's happening
Mirascope does all of this. In 30-40% less code. With type safety. And debuggable stack traces.
When LangChain's Ecosystem Matters
If you're building:
- Complex RAG with 20+ document sources
- Rapid prototypes that won't go to production
- Research projects exploring new agent architectures
And you have:
- A team of 20+ engineers
- Dedicated LLM specialists who know LangChain deeply
- Budget for debugging abstraction layers
Then maybe LangChain's ecosystem is worth the pain.
But that's not most teams.
What About the "Small Ecosystem" Problem?
Mirascope has:
- 2,000 GitHub stars (growing 50% month-over-month)
- 100K+ monthly downloads
- Core LLM functionality
- Clean API for OpenAI, Anthropic, Google, Mistral
What's "missing":
- Pre-built document loaders (you'll write them yourself - it's 10 lines)
- Built-in vector store integrations (use the vector DB's client directly)
- Complex agent patterns (you'll build orchestration explicitly)
Why this is fine:
Document loader:
def load_pdf(path: str) -> str:
import PyPDF2
with open(path, 'rb') as f:
return ''.join(page.extract_text()
for page in PyPDF2.PdfReader(f).pages)
Vector store integration:
# Just use the client
from qdrant_client import QdrantClient
client = QdrantClient("localhost", port=6333)
client.upsert(collection_name="docs", points=embeddings)
results = client.search(collection_name="docs", query_vector=query_embedding)
You don't need abstractions for this. It's clearer without them.
The Cost of Complexity: Quantified
Let's talk maintenance burden over 3 years.
Breaking Changes
LangChain:
- 10-15 breaking changes per year
- Major version bumps (0.0.x → 0.1.x → 0.2.x → 1.0.x) require rewrites
- Community reports: "Upgraded LangChain, spent 3 days fixing broken code"
Mirascope:
- 2-3 breaking changes per year
- Stable API (less abstraction = less to break)
- Community reports: "Upgrades are usually painless"
Maintenance Time (3-Year Projection)
LangChain:
- Breaking changes: 40 × 6 hours = 240 hours
- Debugging complex issues: 20 incidents × 3 hours = 60 hours
- Learning new abstractions: 10 patterns × 4 hours = 40 hours
- Total: ~340 hours over 3 years
Mirascope:
- Breaking changes: 8 × 4 hours = 32 hours
- Debugging: 20 incidents × 0.5 hours = 10 hours
- Learning: Minimal (it's just Python)
- Total: ~42 hours over 3 years
Difference: 298 hours saved = 7.5 weeks of engineering time
At $150K/year salary: $21,500 in opportunity cost per engineer
Onboarding Time
LangChain:
- Read documentation: 8 hours
- Understand abstractions (Chains, Agents, Memory, LCEL): 16 hours
- Build first production feature: 16 hours
- Total: 40 hours = 1 week
Mirascope:
- Read documentation: 4 hours
- Understand patterns (Pydantic + decorators): 4 hours
- Build first production feature: 8 hours
- Total: 16 hours = 2 days
3x faster onboarding.
Real Production Debugging Scenario
A customer support agent occasionally returns empty responses (~5% of the time).
Debugging with LangChain:
- Enable verbose logging (100+ lines per request)
- Add breakpoints in LangChain source code
- Read through agent scratchpad to understand decision process
- Discover memory chain occasionally drops context
- Realize issue is in ConversationBufferMemory configuration
- Fix: Change memory type to ConversationSummaryMemory
Typical time: 4-6 hours.
With Mirascope:
Multi-step agent workflow is explicit Python functions. See exactly where context gets dropped. Add logging between steps. Find the issue in the conversation history function.
Typical time: 30-60 minutes.
What We Use Mirascope For
1. Structured Data Extraction
Extract structured information from unstructured text:
from mirascope.core import openai, prompt_template
from pydantic import BaseModel
class Invoice(BaseModel):
vendor: str
amount: float
date: str
items: list[str]
@openai.call(model="gpt-4o-mini", response_model=Invoice)
@prompt_template("Extract invoice data from: {text}")
def extract_invoice(text: str): ...
# In production
try:
invoice = extract_invoice(pdf_text)
database.save_invoice(
vendor=invoice.vendor,
amount=invoice.amount, # Guaranteed to be float
date=invoice.date,
items=invoice.items
)
except ValidationError as e:
logger.error(f"Failed to extract invoice: {e}")
# Handle gracefully
Type-safe. If the LLM returns invalid data, Pydantic catches it immediately.
2. AI Agents with Tool Calling
Customer support agent that looks up orders, updates tickets:
def lookup_order(order_id: str) -> dict:
"""Look up order details."""
return database.get_order(order_id)
def update_ticket(ticket_id: str, status: str) -> str:
"""Update support ticket."""
return database.update_ticket(ticket_id, status)
@openai.call(model="gpt-4o-mini", tools=[lookup_order, update_ticket])
@prompt_template("""
You are a customer support agent.
History: {history}
User: {message}
""")
def support_agent(message: str, history: str): ...
def handle_message(message: str, history: str):
response = support_agent(message, history)
# Log what the agent decided
logger.info(f"Agent response: {response.content}")
# Execute tools if needed
if tool := response.tool:
logger.info(f"Agent calling: {tool.function}")
result = tool.call()
logger.info(f"Tool result: {result}")
return response.content
Explicit control flow. Clear logging. Easy debugging.
3. Multi-Step Workflows
Research agent that searches → filters → summarizes → generates report:
def research_workflow(topic: str) -> str:
# Step 1: Search
logger.info(f"Searching for: {topic}")
search_results = search_agent(topic)
# Step 2: Filter
logger.info(f"Filtering {len(search_results)} results")
filtered = [r for r in search_results if is_relevant(r, topic)]
# Step 3: Summarize (with caching)
cache_key = f"summary_{hash(filtered)}"
if cached := redis.get(cache_key):
return cached
logger.info("Generating summary")
summary = summarize_agent(filtered)
# Step 4: Generate report
logger.info("Generating report")
report = report_agent(summary, topic)
redis.set(cache_key, summary, ex=3600)
return report
Each step is explicit. Optimize steps independently. Add caching. Handle errors per step.
Observability Without the Lock-In
Mirascope + Langfuse:
from mirascope.integrations.langfuse import with_langfuse
@with_langfuse
@openai.call(model="gpt-4o-mini", response_model=UserInfo)
@prompt_template("Extract: {text}")
def extract_user(text: str): ...
# Langfuse automatically tracks:
# - Input/output
# - Token usage
# - Latency
# - Cost
Or custom logging (trivial):
def logged_summarize(text: str):
start = time.time()
response = summarize(text)
logger.info({
"function": "summarize",
"tokens": response.usage.total_tokens,
"cost": calculate_cost(response.usage),
"latency_ms": (time.time() - start) * 1000,
})
return response
Simple Python functions. Easy to add metrics. No vendor lock-in.
LangChain requires LangSmith ($39-199/month) and understanding their callback architecture.
When You Should NOT Use Mirascope
Let's be honest about limitations.
1. You Need Every Pre-Built Integration
If you're building a system requiring:
- 20+ vector store options
- Support for every obscure LLM provider
- Pre-built integrations with enterprise tools
LangChain has the breadth. You'll spend weeks building those integrations yourself with Mirascope.
But: Most teams need 2-3 integrations. Writing them yourself takes a day and you understand how they work.
2. You Have Dedicated LLM Specialists
If your team includes people who:
- Already know LangChain deeply
- Spend 100% of their time on LLM features
- Can debug abstraction layers quickly
Then LangChain won't slow them down. But that's not most teams.
3. You're Building Throw-Away Prototypes
If you need to demo something in a week and don't care about maintenance, LangChain's pre-built components are faster.
But: That prototype code usually can't go to production. You'll rewrite it anyway.
4. You Love Abstraction Layers
Some engineers love frameworks with lots of abstractions. If that's you, use LangChain.
But: Be honest about the debugging cost when things break in production.
How It Fits Our Unified Stack
Mirascope is part of Grey Haven's unified tech stack:
Frontend & API: TanStack Start Infrastructure: Control Plane Database: PlanetScale LLM Framework: Mirascope Vector Search: TurboPuffer
The Philosophy:
Pick tools once. Get world-class at them. Choose simplicity over ecosystem.
Why Mirascope Fits:
- No abstraction hell: Code we can understand and debug
- Type safety: Catch bugs at dev time, not production
- Fast debugging: 30 minutes instead of 3 hours
- Quick onboarding: 2-3 days vs 1-2 weeks
- Knowledge transfers: It's just Python
Real Impact:
An engineer working on one portfolio company's data extraction pipeline understands Mirascope patterns. They move to another portfolio company building a customer support agent. No framework relearning. They understand immediately because it's Python + Pydantic.
They ship in 3 days instead of 2 weeks.
That's the compounding value of consistency.
Stop Using LangChain
Here's what matters when building AI systems in production:
LangChain is abstraction hell:
- Too many layers hiding what's actually happening
- Debugging takes 6x longer (3 hours vs 30 minutes)
- Vendor lock-in through cognitive complexity
- Breaking changes every few months
- Stack traces that go through 47 files
Mirascope is what LLM frameworks should have been:
- Clean Python with Pydantic
- Type safety catches bugs at dev time
- Explicit control flow you can actually understand
- Debuggable stack traces (3 files, not 47)
- 30-40% less code for the same functionality
The numbers are real:
- 298 hours less maintenance over 3 years
- 3x faster onboarding (2 days vs 1 week)
- 6x faster debugging (30 min vs 3 hours)
- $21,500 per engineer in saved opportunity cost
The ecosystem trade-off is real:
- 50x fewer GitHub stars (2K vs 100K)
- Smaller community
- More manual integration work
- But: code you can actually understand and debug
For small teams building production systems, that's a trade-off worth making.
We're not chasing the biggest ecosystem. We're choosing tools we can maintain for 3+ years without drowning in abstraction hell.
That's the unified stack philosophy in action.
At Grey Haven, we build AI systems across multiple portfolio companies. We've evaluated LangChain and Mirascope for production use. Mirascope's simplicity, type safety, and debuggability make it the clear choice for small teams building reliable AI systems.
Want to learn more about our unified tech stack? Read Part 1: Why We Ditched Next.js for TanStack Start, Part 2: Why We Chose Control Plane for Multicloud Infrastructure, Part 4: TurboPuffer Vector Search, and Part 5: PlanetScale PostgreSQL vs Supabase.
Building AI systems and drowning in LangChain's abstractions? Let's talk.