← Back to feed
ResearchArticle

Why We Chose Mirascope Over LangChain (And You Should Too)

LangChain has 100K GitHub stars and everyone uses it. It's also abstraction hell that makes production debugging impossible. Here's why we chose Mirascope's simplicity—and saved 298 hours of maintenance over 3 years.

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:

  1. LangChain's abstractions
  2. LangSmith's observability
  3. 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:

  1. Enable verbose logging (100+ lines per request)
  2. Add breakpoints in LangChain source code
  3. Read through agent scratchpad to understand decision process
  4. Discover memory chain occasionally drops context
  5. Realize issue is in ConversationBufferMemory configuration
  6. 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:

  1. No abstraction hell: Code we can understand and debug
  2. Type safety: Catch bugs at dev time, not production
  3. Fast debugging: 30 minutes instead of 3 hours
  4. Quick onboarding: 2-3 days vs 1-2 weeks
  5. 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.

Grey Haven
Grey HavenApplied AI Venture Studio