Three names, two of them from the same company, and a choice most teams make by vibes. They are not three answers to one question — they are three different layers of the same stack.
Every few weeks someone opens the discussion the same way: “are we going LangChain or LlamaIndex? What about LangGraph?” The comparison feels natural because the names look alike and the READMEs overlap. It is still the wrong comparison, and it produces a predictable outcome — a codebase that adopted one of them for a job it does not do, wrapped in glue that exists to work around the mismatch.
Sort them by the job they own and the choice mostly makes itself.
LangChain is an integration layer. LlamaIndex is a data layer. LangGraph is a control layer. Asking which is best is like asking whether requests, SQLAlchemy or Celery is the best Python library — the answer depends entirely on which part of the program you are looking at, and a real system usually has all three parts.
The three jobs, drawn as a stack
The stack is also the honest version of “do I need a framework at all?” The bottom layer is not a fallback for toy projects. A great deal of shipped LLM software is a provider SDK, a database and some ordinary code.
What each one is actually best at
LangChain — breadth of integrations, composed
Its core value is that a hundred model providers, vector stores, loaders and tools present roughly the same interface, and that you can compose them into a pipeline. That matters most when the set of components is unsettled: you are still choosing a model, evaluating two vector stores, or building something that has to run against whatever a customer already owns. It matters least when you have made those choices and they are not changing.
LlamaIndex — documents into an answerable index
Its center of gravity is everything between a messy corpus and a good retrieval result: connectors, parsing, node/chunk construction, index types, retrievers, postprocessors, query engines. If your hard problem is 40,000 PDFs with tables in them, this is the library whose defaults were chosen by people staring at that exact problem. See document processing and chunking for what it is doing on your behalf.
LangGraph — state and control flow you can inspect
A typed state object, nodes that transform it, and edges that decide what may run next — plus checkpointing, so a run can be resumed, replayed or paused for a human. It is the layer you reach for when the interesting part is not any single call but the branching, retrying and gating between calls. The mental model is graph engineering; the framework is one implementation of it.
LangGraph comes from the LangChain team and builds on langchain-core, which is why people assume adopting one means adopting both. You can run a graph whose nodes call a provider SDK directly and never touch a LangChain chain, retriever or agent. Conversely, plenty of LangChain codebases have no graph in them at all. Treat them as separable, because they are.
The decision tree
Three questions, asked about the part of the system that is actually hard. Not the demo — the part that will still be difficult in six months.
Side by side
| LANGCHAIN | LLAMAINDEX | LANGGRAPH | |
|---|---|---|---|
| Job it owns | Integrations and composition | Corpus → retrievable index | Control flow and state |
| Core abstraction | Composable components in a pipeline | Index, retriever, query engine | Typed state, nodes, edges |
| What you write | Wiring between components | Ingestion and retrieval config | Node functions and routing functions |
| State between steps | Threaded through the pipeline | Per query, largely stateless | One explicit object, checkpointed |
| Resume after a crash | Build it yourself | Build it yourself | Built in — replay from the last node |
| Pause for a human | Build it yourself | Build it yourself | First-class interrupt |
| Strongest when | The components are still changing | The documents are messy and plentiful | The path branches, loops or needs a gate |
| Overkill when | One model, one store, one path | Your “corpus” is one table or API | The flow is a straight line |
| If you rip it out | You rewrite the wiring | You rebuild ingestion and re-index | Your graph shape survives; the API does not |
The bottom row is worth more than the rest of the table. Two of these frameworks own code; LlamaIndex also owns data you have already paid to process, which is the more expensive kind of commitment.
Each has grown into the others’ territory — LlamaIndex ships an event-driven workflow engine, LangChain ships retrievers, LangGraph examples do plenty of retrieval. The overlap is real, and it is not where any of them is strongest. Choose on the center of gravity, not on the feature matrix; the feature matrix is where a project ends up using the third-best implementation of everything.
Three scenarios, worked
1 · “Chat over 40,000 internal PDFs”
The control flow here is three steps long: retrieve, answer, cite. Nothing branches. What is genuinely hard is that a third of the PDFs are scanned, half the useful content is inside tables, and the useful chunk boundary is not the paragraph boundary.
LlamaIndex owns this, and the orchestration stays as plain functions. Reaching for a graph here adds a state schema to a problem that has one path; reaching for LangChain adds an abstraction over a vector store you already chose. Your time goes into parsing, chunking and an eval set — and if you find yourself asking whether to fine-tune instead, that is a different decision entirely: see RAG vs fine-tuning.
2 · “A refund agent that needs approval over $50”
One call decides intent. Some paths need a lookup, some need a policy check, refunds over a threshold must reach a human, and a crash halfway through must not re-issue a refund on retry.
LangGraph owns this. Every requirement in that paragraph is a topology statement, and a topology you can draw is a topology you can audit — “approval cannot be skipped” becomes an edge, not a sentence in a system prompt that usually works. Human-in-the-loop and persistence are the two features doing the load-bearing work here, and both are miserable to rebuild by hand.
3 · “Classify a ticket, summarize it, post it to Slack”
No framework. Three calls in a straight line, one file, no state to persist and nothing to decide. A framework here does not reduce the code much and it does make the stack trace worse.
# The version that needs no framework -- and stays readable in a year.
ticket = fetch(ticket_id)
category = client.messages.create(model=MODEL, messages=classify_prompt(ticket))
summary = client.messages.create(model=MODEL, messages=summarize_prompt(ticket))
slack.post(channel=ROUTE[category], text=render(summary))
The honest test: if you can write the whole flow as a numbered list and no number says “go back to step 2” or “wait for a person”, you do not have an orchestration problem yet.
How they combine in a real stack
The common production shape does not pick a winner. It is LlamaIndex owning retrieval, exposed to LangGraph as a single node, with the provider SDK underneath.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from llama_index.core import VectorStoreIndex
# --- data layer: LlamaIndex owns parsing, indexing and retrieval ---
index = VectorStoreIndex.from_documents(documents)
retriever = index.as_retriever(similarity_top_k=6)
class State(TypedDict):
question: str
chunks: list[str]
answer: str
# --- and shows up upstairs as exactly one node ---
def retrieve(state: State) -> dict:
nodes = retriever.retrieve(state["question"])
return {"chunks": [n.get_content() for n in nodes]}
def enough_evidence(state: State) -> str:
return "answer" if state["chunks"] else "escalate"
# --- control layer: LangGraph owns what runs next, and durability ---
builder = StateGraph(State)
builder.add_node("retrieve", retrieve)
builder.add_node("answer", answer)
builder.add_node("escalate", escalate)
builder.add_edge(START, "retrieve")
builder.add_conditional_edges("retrieve", enough_evidence, ["answer", "escalate"])
builder.add_edge("answer", END)
graph = builder.compile(checkpointer=saver)
Note what the seam buys you. Swapping the retriever for a plain SQL query changes one function. Adding a reranker changes one function. Neither touches the graph, because the graph never knew how retrieval worked — and that is the property to design for, whichever libraries you end up with.
The abstraction tax
What a framework buys
Integrations you would otherwise write and maintain. Defaults chosen by people who have seen more corpora than you have. Durability, retries and interrupts that are genuinely tedious to build. A vocabulary your next hire may already speak, and examples to copy at 2am.
What it costs
A stack trace that goes through six files you did not write. A prompt you can no longer see without turning on debug logging — which is a real problem, because what is in the window is the thing you most need to inspect. Upgrade work on someone else’s schedule, and an abstraction that has to be fought the first time your requirement is slightly unusual.
The tax is worth paying at the layer where your problem is hard, and not at the layers where it is not. That is why the stack diagram matters more than the comparison table: a system can take LlamaIndex for retrieval, keep its own thirty-line loop, and be entirely coherent.
Five checks before you adopt any of them
- Can you print the exact prompt that was sent? If it takes a debugger to answer that, you have lost the thing you tune most often.
- Can you see the state between steps? Anything you cannot log, you cannot debug at 3am.
- What happens on a crash mid-run? If the answer is “start over”, be sure that is acceptable for side-effecting steps — refunds, emails, writes.
- How do you test one step alone? A node or component you cannot call from a unit test will not be tested.
- What is the rip-out cost in month nine? Wiring is cheap to redo. A processed corpus is not.
Do not pick a framework; pick a layer. Name the part of the system that is genuinely hard — documents, control flow, or an unsettled set of providers — and adopt only the layer that owns it. Systems that chose this way tend to survive both the framework’s next major version and their own change of mind, because each layer can be replaced without the others noticing.
The shape outlives the framework
Retrieval will still be parse → chunk → embed → search → rerank in five years, whatever imports it. A branch with an approval gate will still be a branch with an approval gate. The libraries around those shapes will be renamed, rewritten, merged and deprecated — two of the three here have already been through at least one API era.
Which is the practical reason to learn the shapes rather than the APIs: graph engineering for control flow, loop engineering for the layer above it, and retrieval for the data layer. Then the framework question stops being a decision and becomes an implementation detail — which is what it always was.