LangChain basics: what it solves and when to skip it entirely
Learn what LangChain actually provides, why multi-provider tool calling pushed teams toward it, which packages to install, and when a thin wrapper is the better choice.
This article is published in English.
Sources: [Day 11/100] LangChain 101: Why It Exists, and When You Should (and Should Not) Use It
The fifty-line agent stops being fifty lines
Adding a new tool to a small hand-rolled agent is cheap. You write a function, register it in a dict, describe it in a schema, and the loop itself never changes. That part of agent work feels pleasant, and it fools people into thinking the whole job will stay that small.
Then the real requirements show up. The OpenAI endpoint returns a 503 now and then, so you need retries with backoff. Sometimes the provider is down for twenty minutes, so you want a second vendor as a fallback. A user waiting eight seconds for four chained calls to finish will assume the app froze, so you add token streaming. A downstream service rejects a response because one field came back as a string instead of a number, so now you validate structured output. And once ten people use it at the same time, blocking calls stop being acceptable, so everything has to go async.
Build all of that on top of raw openai and requests and the agent quietly grows to around five hundred lines. Very little of that code is about what your agent actually does. It's JSON parsing, exponential backoff, and translating between message formats that each vendor invented on its own. Every team ends up writing this plumbing, and nobody enjoys writing it a second time.
That plumbing is the product LangChain offers. Below is what the library really is, the problems it was built to solve, and, just as useful, the situations where adding it is a mistake.
What the library is (and isn't)
LangChain doesn't run your agent for you, it doesn't host anything, and it won't make a model reason better. Think of it as a set of shared interfaces plus ready-made implementations for the pieces of an LLM application you'd otherwise code yourself.
If you've already built a bare-bones agent, you've built most of these pieces under different names. The list of messages you pass around maps to ChatMessageHistory. A tool schema becomes a Tool, or a plain function decorated with @tool. The while loop that keeps calling the model until it stops asking for tools is what AgentExecutor used to do. And the direct client.chat.completions.create call turns into a ChatModel. Nothing here is conceptually new. What changes is that these parts now have a common shape that thousands of other projects already use, and the common combinations come pre-wired.
The problems it was built for
Start with tool calling, because it shows the mess clearly. OpenAI attaches tool_calls to the assistant message and wants the result back as a message with role: "tool". Anthropic sends tool_use content blocks and expects tool_result blocks inside the next user turn. Gemini uses function_call and function_response parts. Same idea, three APIs, three shapes that don't fit together.
With a single vendor this never bites you. The day you need a second one, maybe because the first is having an outage or just got more expensive, you either write and maintain three adapters or you lean on code that already handles it. That was the original reason for LangChain: one ChatModel interface where .invoke() and .bind_tools() behave the same regardless of which company produces the tokens. Moving from ChatOpenAI to ChatAnthropic is an import change, not a rewrite of your message handling.
Composition is the second problem. Feeding a prompt into a model, the model into a parser, and the parser into another model means glue code at every boundary, and that glue has to be rewritten whenever the pipeline changes shape. LangChain makes those boundaries uniform. The mechanism for that is LCEL, the LangChain Expression Language, which deserves its own write-up.
The third problem is plain reuse. An agent that does more than call a model needs things somebody has already built: loaders for around forty document formats, clients for a dozen vector databases, retry policies, streaming, batching, tracing hooks. A maintained integration is almost always better than a PDF parser you wrote on a Friday and now have to debug.
What you get without writing it
One model interface. ChatOpenAI, ChatAnthropic, ChatGoogleGenerativeAI, and ChatOllama for local models all expose .invoke(), .stream() and .bind_tools(). Your code no longer cares who trained the model behind the call.
Composable building blocks. With LCEL's pipe operator you write prompt | model | parser and get back one object that acts like each of its components. It can stream, batch and run async without extra work.
Resilience as a method call. Any Runnable has .with_retry() and .with_fallbacks(), so you don't hand-write a backoff loop or a provider switch.
Streaming and async everywhere. Every Runnable gets .stream(), .ainvoke(), .astream() and .batch(), even when the thing you wrapped never supported those natively.
A large integration catalog. Document loaders, vector stores, search tools, code execution, database tools. Many are maintained by the vendors themselves, and they all speak the same Runnable interface.
A straight road to LangGraph. When your agent needs loops, saved state, or a human approval step that a linear chain can't model, the control loop moves into LangGraph. It's built on the same core abstractions, so what you learn in LangChain carries over directly.
When to leave it out
Most introductions skip this part, and it's the part that saves the most grief later.
One provider, one call, forever. If the whole product is "send this prompt to GPT-4o and return the text", a small wrapper of your own is easier to read and debug than a dependency that drags in hundreds of transitive packages.
You need to see every request. Abstractions are handy precisely because they hide detail, and that hiding costs you when something breaks. Following a stack trace through six layers of Runnable wrappers before you reach your actual bug is real time lost. Teams that want to read every line between their code and the HTTP call often stay closer to the raw SDK.
Latency-critical paths. Each layer adds a bit of overhead. Next to a model call that takes 500ms or more, that overhead is usually noise. On a hot path where every millisecond counts, it sometimes isn't.
Unpinned upgrades. LangChain ships breaking changes regularly. A project that floats on the latest version will eventually wake up to a renamed import or a changed default. Lock the version like any other dependency and upgrade when you've decided to, not when pip decides for you.
None of this means you should never use LangChain. It means the decision should be made per project, on purpose, rather than out of habit.
Which package is which
LangChain is split into several packages, and it's worth knowing what each one does before you install anything.
langchain-coreholds the base abstractions:Runnable,BaseChatModel,BaseTool, prompt templates and output parsers. It has few dependencies, and almost everything else builds on it.langchainadds chains, agents and retrieval logic on top of core, without tying you to a provider.langchain-communitycollects the long tail of third-party integrations. Some are excellent and some are abandoned, since anyone can contribute. Look at when an integration was last updated before you rely on it in production.- Partner packages such as
langchain-openai,langchain-anthropicandlangchain-google-genaiare maintained alongside each provider's own SDK. Install only the ones you actually call. langchain-classicis where the pre-1.0 agent constructors went in v1:AgentExecutor,create_tool_calling_agentand the oldcreate_react_agent. Existing code keeps working, but new agents shouldn't start here.langgraphis a separate package on top oflangchain-corethat owns the control loop for anything that isn't a straight line. Sincelangchainv1, the recommended agent builder,langchain.agents.create_agent, is implemented directly on it.
To get going you only need three packages:
uv add langchain langchain-openai python-dotenv
Swapping the raw client for ChatOpenAI
Don't expect anything dramatic at this step. The goal is to replace the plain OpenAI client with LangChain's chat model wrapper and check that you get the same answer back.
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o", temperature=0)
response = llm.invoke("What is the weather in Paris?")
print(response.content)
llm.invoke(...) hands you an AIMessage rather than a raw dictionary, but .content gives you the text just like .choices[0].message.content did. Underneath, ChatOpenAI still hits the same OpenAI endpoint you'd call by hand, and the model hasn't changed at all. The difference is that llm is now a Runnable, so .stream(), .batch() and .ainvoke() are already there and you didn't write any of them.
for chunk in llm.stream("Tell me one fact about Paris."):
print(chunk.content, end="", flush=True)
Run it and the tokens arrive one chunk at a time. You never wrote a streaming handler, and that small loop is pretty much the whole argument for the library.
A quick word on model names: the examples use "gpt-4o" as a stand-in. Provider lineups change faster than any tutorial can keep up with, so read every model="..." string as a placeholder and put in whatever flagship your provider offers when you run the code. The mechanics stay the same.
Try it on your own agent
- Add
langchainandlangchain-openaito your project withuv add. - In your agent's model step, replace the raw
OpenAI()call withChatOpenAIand leave the rest of the loop alone. Check that the agent behaves exactly as before. - Call
llm.stream(...)on a simple question with no tools and print each chunk as it comes in. You now have streaming for a method you never implemented.
Your agent isn't any smarter after this change. What you've gained is a foundation where retries, fallbacks, streaming and provider switching cost a method call instead of another hundred lines of plumbing.