LangChain and LangGraph
What each library actually does, where the split between them lies, when a framework earns its place in your stack, and when plain code is the better answer.
LangChain is the most widely used and most widely criticised library in the AI ecosystem. Both reputations are earned, and both come from the same source: it tries to abstract over a fast moving field, which is genuinely useful when the abstraction fits and genuinely painful when it does not.
LangGraph came later from the same team, and is the more interesting of the two. It solves a problem plain code handles badly, which is the strongest argument any framework can make for itself.
Here is what each does, where the line sits, and how to decide whether you need either.
What LangChain provides
Three things, of decreasing value.
A uniform interface across providers. Every model provider has its own client library with its own request shape and its own response format. LangChain puts one interface over them, so swapping providers is a constructor change rather than a rewrite.
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-sonnet-5")
response = model.invoke("Summarise this in one line: ...")Whether this matters depends entirely on whether you will ever switch. Many teams never do, in which case the abstraction is cost without benefit.
Composition. The expression language lets you pipe components together, which reads well for linear pipelines:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_template(
"Answer using only this context.\n\n{context}\n\nQuestion: {question}"
)
chain = prompt | model | StrOutputParser()
answer = chain.invoke({"context": docs, "question": "..."})Streaming, batching and async come free across the whole chain, which is the real benefit here rather than the syntax.
Integrations. Loaders for dozens of file formats, connectors for every vector store, wrappers for common tools. This is the largest part of the library by volume and the most genuinely time saving. It is also where quality varies most, because the integrations are maintained at very different levels of attention.
Where LangChain gets a bad reputation
Fairly, in three places.
Abstraction depth. When something misbehaves, you can find yourself several layers from the actual HTTP request, reading library source to work out what was sent. For a component whose behaviour depends closely on exact prompt text, that distance is expensive.
Churn. The library has been through several significant reorganisations. Examples and answers found online frequently target a version that no longer exists, which makes learning it harder than it should be.
It encourages reaching for a chain when a function would do. A single model call wrapped in a prompt template, a chain and an output parser is more code and more indirection than the direct API call it replaces.
The reasonable position is to use the parts that save real work, mostly the loaders, splitters and store integrations, and write your own orchestration where the abstraction is not paying for itself.
What LangGraph provides
LangGraph is not a bigger LangChain. It solves a different problem: control flow that is not a straight line.
A chain runs A then B then C. Real agent behaviour loops. The model calls a tool, sees the result, decides whether to call another, and eventually stops. It may need to retry, branch on a condition, or pause for a person to approve something. That is a state machine, and expressing it as a chain does not work.
LangGraph models it directly. You define state, nodes that transform state, and edges that decide what runs next.
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
class State(TypedDict):
messages: Annotated[list, operator.add]
def call_model(state: State):
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
def call_tools(state: State):
last = state["messages"][-1]
results = [execute(call) for call in last.tool_calls]
return {"messages": results}
def should_continue(state: State):
return "tools" if state["messages"][-1].tool_calls else END
graph = StateGraph(State)
graph.add_node("model", call_model)
graph.add_node("tools", call_tools)
graph.set_entry_point("model")
graph.add_conditional_edges("model", should_continue)
graph.add_edge("tools", "model")
app = graph.compile()That is the standard agent loop: call the model, run any tools it asked for, feed the results back, repeat until it stops asking. Written out as a graph, the flow is visible in the structure rather than buried in a while loop.
The features that justify it
The graph structure on its own is a modest win. Four things built on top of it are the real argument.
Persistence. Attach a checkpointer and the state of every step is saved. A conversation can be resumed hours later on a different process. A crashed run restarts from the last completed node rather than the beginning. For long running agents this is the difference between a toy and something you can operate.
from langgraph.checkpoint.postgres import PostgresSaver
app = graph.compile(checkpointer=PostgresSaver(conn))
config = {"configurable": {"thread_id": "user-123"}}
app.invoke({"messages": [user_message]}, config)
# later, in a different process, same thread_id resumes the stateHuman in the loop. Interrupt before a node, surface the pending action to a person, and resume on approval. Anything that sends an email, moves money or changes production data wants this, and building it by hand around a plain loop is genuinely awkward because it requires the loop to be suspendable.
app = graph.compile(checkpointer=saver, interrupt_before=["tools"])Streaming intermediate state. Users of a multi step agent want to see progress rather than a spinner. Because every node transition is an event, showing what the agent is doing is a matter of subscribing rather than instrumenting.
Time travel. Rewind to an earlier checkpoint, change something, and re run from there. For debugging agent behaviour this is far better than re running the whole thing and hoping to reproduce the problem.
Choosing between them
The split is clean once you see it.
SituationReach forOne model callThe provider's own client libraryFixed pipeline: retrieve, then generatePlain code, or LangChain if you want its loadersLoops, branches, retriesLangGraphState must survive a restartLangGraphA person must approve a stepLangGraphMultiple agents coordinatingLangGraph
They compose. It is normal to use LangChain's document loaders and vector store integrations for ingestion, and LangGraph for the agent that queries it. You are not choosing one library over the other so much as using each where it fits.
When to skip both
Worth saying plainly. If your feature is a prompt, a model call and a parsed response, a framework adds a dependency, an abstraction layer and a version to keep current in exchange for very little.
The provider libraries have improved considerably. Structured output, tool calling and streaming are all first class in the official clients now, and several of them ship their own loop helper for the basic agent pattern. A lot of what people reached for LangChain to get in 2023 is in the client library today.
Reasonable rule: start with the provider's own library. Move to a framework when you hit something it makes genuinely awkward. For most teams that moment arrives with persistence or human approval, which is exactly where LangGraph is strongest, and it arrives for fewer teams than you would guess from the amount written about it.
Practical notes
Pin your versions. Both libraries move quickly. An unpinned dependency will change behaviour on a rebuild.
Keep prompts out of the framework. Store them as files under version control and load them in. Prompts change more often than code and benefit from being diffable on their own.
Cap your loops. An agent graph with no iteration limit can loop indefinitely, and every iteration costs money. Set a maximum and decide what happens when it is reached.
Instrument early. Both libraries integrate with tracing tools such as LangSmith and Langfuse, and OpenTelemetry works fine if you would rather not add another service. A multi step agent without tracing is close to impossible to debug, because the interesting failure is almost always in the middle.
Read the source when something is odd. Both are open and reasonably readable. Ten minutes in the library beats an afternoon of guessing at what it sent.
Worth knowing
LangChain is a toolbox with some excellent tools and some you should leave in the drawer. LangGraph is a focused solution to a real problem, and if you need durable, interruptible, looping agent behaviour it will save you writing a state machine yourself.
Neither is a substitute for understanding what your system actually sends to the model and gets back. Frameworks make the plumbing quicker; they do not make the design decisions for you.
If you are building an agent that needs to run reliably, survive restarts, and let a person approve the consequential steps, get in touch with Eight Mile. AI assistants, RAG systems, workflow automation, backend APIs and system architecture are what we build.