DEV Community

Leo Han
Leo Han

Posted on

why-we-dropped-langchain

Why We Dropped LangChain: When Abstractions Do More Harm Than Good

A 12-Month Lesson Learned

In early 2023, we put LangChain into production. In 2024, we removed it entirely.

LangChain seemed like the best choice for building LLM-powered applications in 2023. It had an impressive list of components and tools, its popularity was soaring, and it promised to "enable developers to go from an idea to working code in an afternoon." But as our project progressed, the cracks began to show.

LangChain's inflexibility gradually surfaced: we found ourselves constantly diving into LangChain internals to modify lower-level behavior. And because LangChain intentionally abstracts away those internals, doing so was extremely painful. Whenever we needed to do something the framework didn't natively support, we had to "translate" our requirements into LangChain-appropriate solutions — instead of just writing code.

This post shares the real reasons we abandoned LangChain, and why replacing its rigid high-level abstractions with modular building blocks simplified our codebase, made our team happier, and made us more productive.


The Core Problem: The Perils of Being an Early Framework

LLMs are a rapidly changing field, with new concepts and ideas emerging weekly. When a framework like LangChain is built around multiple emerging technologies, designing abstractions that will stand the test of time is nearly impossible.

Crafting well-designed abstractions is hard — even when the requirements are well-understood and stable. But when you're modeling components in such a state of flux, by the time you finish designing the abstraction, the underlying technology has already changed.

This isn't the LangChain team's fault. Anyone attempting to build such a framework at that point in time wouldn't have done any better. Everyone was doing their best.


Problem 1: Simple Tasks Become Complicated

Consider the simplest possible task: a translation app. Using the native OpenAI SDK:

import os
from openai import OpenAI

os.environ["OPENAI_API_KEY"] = "<your_api_key>"

client = OpenAI()
text = "hello!"
language = "Italian"

messages = [
    {"role": "system", "content": "You are an expert translator"},
    {"role": "user", "content": f"Translate the following from English into {language}"},
    {"role": "user", "content": f"{text}"},
]

response = client.chat.completions.create(model="gpt-4o", messages=messages)
Enter fullscreen mode Exit fullscreen mode

Clean, direct, no hidden logic. Any Python developer can understand it at a glance.

Now the same task with LangChain:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model="gpt-4o-turbo", temperature=0)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are an expert translator"),
    ("user", "Translate the following from English into {language}"),
    ("user", "{text}")
])

parser = StrOutputParser()
chain = prompt | model | parser
result = chain.invoke({"language": language, "text": text})
Enter fullscreen mode Exit fullscreen mode

You now need to understand ChatPromptTemplate, StrOutputParser, the pipe operator |, and the invoke method — all LangChain-specific concepts. They don't make your code better; they just make your code more LangChain.

The problem isn't writing a few extra lines. The problem is that every abstraction you introduce adds a layer of cognitive overhead and debugging difficulty. When something breaks, you're not debugging your business logic — you're debugging LangChain's framework code.


Problem 2: The http.client vs. requests Analogy

Imagine you have a choice:

Option A: Use http.client to make a request

import http.client, json

conn = http.client.HTTPSConnection("api.example.com")
conn.request("GET", "/data")
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
Enter fullscreen mode Exit fullscreen mode

Option B: Use requests to make a request

import requests

response = requests.get("https://api.example.com/data")
data = response.json()
Enter fullscreen mode Exit fullscreen mode

Which is better? Obviously B. requests isn't "too much abstraction" — it's the right level of abstraction. It encapsulates the genuine complexity (connection management, encoding) without hiding what you actually care about (URL, response data).

LangChain's problem is that it's often neither A nor B. It neither simplifies the truly hard parts (complex Agent orchestration) nor leaves the simple parts simple.

A Reddit comment captured it perfectly:

"Code quality isn't great and structure is pretty iffy. Really hate the piping language structure. Docs are way outdated, deprecation warnings implemented poorly. And when you need to dig under the surface to fix something, you see the ugly. But it gets the job done. I can see what they want to do, but it's bloated very quickly, probably because of its popularity."


Problem 3: Agents Become Black Boxes

When we wanted to move from a single sequential Agent architecture to something more complex, LangChain became our biggest obstacle.

We needed to externally observe the Agent's state, dynamically control available tools, and flexibly orchestrate interactions between multiple Agents. But LangChain's Agent abstractions encapsulate all of this behind an opaque surface — it provides no method for externally observing an Agent's state, forcing us to reduce the scope of our implementation to fit into the limited functionality available to LangChain Agents.

In one instance, we needed to dynamically change the availability of tools our Agents could access, based on business logic. In native code, this is an if statement and an append/remove on a list. In LangChain, you're expected to declare tools upfront during the framework's initialization flow, and dynamic modification requires working around layers of encapsulation.

Once we removed LangChain, we no longer had to translate our requirements into LangChain-appropriate solutions. We could just code.


What LangChain's Architecture Diagram Really Shows

LangChain's official architecture reveals its ambition:

        LLMs and Prompts
              |
           CHAINS
              |
          LANGCHAIN
         /          \
  MEMORY              DOCUMENT LOADERS
(Vector DBs)            AND UTILS
Enter fullscreen mode Exit fullscreen mode

The problem: every single module is in constant flux. LLM interfaces change. Prompt best practices evolve. Chain orchestration patterns shift. Memory implementations keep moving. When your framework tries to abstract all of these rapidly changing components at once, the only stable thing is the instability itself.


Do You Really Need a Framework for Building AI Applications?

LangChain's long list of components gives the impression that building LLM-powered applications is complicated and requires a framework. But here's the reality:

  1. LLM Calls: The OpenAI / Anthropic SDKs are already clean enough
  2. Prompt Management: Python f-strings or a Jinja2 template will do
  3. Chains / Orchestration: Pure Python functions and loops, more readable than any DSL
  4. Memory: A dictionary or a database table, under your full control
  5. Vector Stores: Chroma, Pinecone, Qdrant all have clean native APIs
  6. Document Loaders: Mature, independent libraries exist for PDF, web parsing, etc.

LangChain adds one more abstraction layer on top of all of these. And the value of that layer, in most scenarios, falls far short of the complexity it introduces.


Our Alternative: Modular Building Blocks

After dropping LangChain, our tech stack became:

  • OpenAI / Anthropic SDK — LLM calls
  • Chroma / Qdrant — Vector storage (using native APIs directly)
  • Simple custom orchestration — Python functions + type annotations
  • Standard Python logging and monitoring — no framework-specific callback system

The core principle: every component is replaceable, every abstraction is your own, and there are no black boxes.

This approach may require a few dozen more lines of boilerplate than LangChain, but the trade-off is:

  • Fully controllable execution flow
  • Zero framework debugging overhead
  • Team members don't need to learn yet another DSL
  • No lock-in to a framework's version upgrades

When Should You Use LangChain?

To be fair, LangChain still has value in certain scenarios:

  • Rapid prototyping: you want a working RAG demo in 30 minutes
  • Teaching / learning: understanding RAG and related concepts through a structured approach
  • Standardized simple workflows: your requirements happen to perfectly match its Chain pattern

But if you're building a production-grade system, LangChain is more likely to become technical debt than an accelerator.


Conclusion

LangChain did something few dared to do: attempt to provide a unified framework during the most chaotic period of the LLM ecosystem. That courage deserves respect.

But the experience of 2024–2026 has shown: for production AI Agent systems, simple, direct code beats complex framework abstractions. LLMs themselves are already complex enough — you don't need a framework adding another layer of complexity on top.

The biggest feeling our team had after dropping LangChain wasn't "we lost features" — it was "we're finally free." We can write the code we want in the most direct way, instead of figuring out how to make the framework allow us to write it.

If you're starting a new LLM project, my advice is: try going framework-free first. Write code with the native SDKs for a few weeks. Then decide whether you truly need those abstractions. The answer will likely be no.


This article is adapted from the video "Why We Dropped LangChain," drawing on a team's real experience of using LangChain in production for 12 months before removing it, analyzing the cost of framework abstractions and the advantages of a modular building-block approach.

Top comments (0)