The "which LLM framework should I use" question keeps coming up in planning meetings, and the honest answer is usually "more than one, for different parts of the same system." Here's what each of these three actually does well, where they overlap, and how to decide without starting a framework war on your team.
What Each Framework Is Actually For
It helps to stop thinking of these as competitors and start thinking of them as tools with different centers of gravity:
- LangChain — a toolkit of composable primitives: chains, tool wrappers, output parsers, and integrations with dozens of model providers and vector stores. Its strength is breadth of integrations and quick composition of linear-ish pipelines.
- LangGraph — a low-level framework for building agents as explicit state machines/graphs, with first-class support for cycles, conditional branching, and persistence. Its strength is control over complex, stateful, multi-step agent behavior.
- LlamaIndex — purpose-built for data indexing and retrieval: connecting to data sources, chunking, indexing strategies, and query engines. Its strength is depth on the retrieval side specifically.
None of them "replace" the other two — they overlap at the edges but their core competency is different.
LangChain: Chains and Tools
LangChain earns its keep when you need to wire together a provider-agnostic pipeline quickly — swap models, add a tool, plug in a vector store — without hand-rolling every integration.
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
const model = new ChatAnthropic({ model: 'claude-opus-4.6', temperature: 0 });
const prompt = ChatPromptTemplate.fromMessages([
['system', 'Summarize the following support ticket in one sentence.'],
['human', '{ticketBody}'],
]);
const chain = prompt.pipe(model).pipe(new StringOutputParser());
const summary = await chain.invoke({ ticketBody: rawTicketText });
Where LangChain starts to strain is exactly where the task stops being a linear pipeline — once you need loops, conditional retries, or an agent that revisits earlier steps based on new information, forcing that into a chain gets awkward fast. That's the gap LangGraph fills.
LangGraph: Stateful Agent Graphs
LangGraph models an agent as an explicit graph of nodes and edges, with a typed state object that flows through it. This is the right tool once your "agent" needs to loop, branch, or pause for human input mid-execution.
from langgraph.graph import StateGraph, END
from typing import TypedDict
class AgentState(TypedDict):
query: str
retrieved_docs: list[str]
draft_answer: str
needs_more_research: bool
def retrieve(state: AgentState) -> AgentState:
docs = retriever.search(state["query"])
return {**state, "retrieved_docs": docs}
def draft(state: AgentState) -> AgentState:
answer = llm.generate(state["query"], context=state["retrieved_docs"])
needs_more = critic.flags_insufficient_evidence(answer)
return {**state, "draft_answer": answer, "needs_more_research": needs_more}
graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve)
graph.add_node("draft", draft)
graph.add_edge("retrieve", "draft")
graph.add_conditional_edges(
"draft",
lambda s: "retrieve" if s["needs_more_research"] else END,
)
graph.set_entry_point("retrieve")
app = graph.compile()
result = app.invoke({"query": "What's covered under the enterprise SLA?"})
The conditional edge back to retrieve is the whole point — LangChain's linear chains can't express "loop back and try again with different context" without falling back to manual control flow outside the framework. LangGraph also gives you built-in checkpointing, so a long-running agent can persist state and resume after a crash or a human-in-the-loop pause, which matters a lot once agents run for minutes rather than seconds.
LlamaIndex: Data Indexing and Retrieval
LlamaIndex's center of gravity is connecting to and indexing data well, with retrieval strategies that go deeper than a basic top-k vector search.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.retrievers import AutoMergingRetriever
documents = SimpleDirectoryReader("./knowledge_base").load_data()
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=64)
index = VectorStoreIndex.from_documents(documents, transformations=[splitter])
query_engine = index.as_query_engine(
retriever=AutoMergingRetriever(index.as_retriever(similarity_top_k=6)),
response_mode="compact",
)
response = query_engine.query("What's our data retention policy for EU customers?")
print(response.source_nodes) # citations back to source chunks
AutoMergingRetriever is a good example of LlamaIndex's specialty — it retrieves small chunks but automatically merges them into their parent context when enough siblings are retrieved, which is a retrieval-quality trick that would take real effort to replicate by hand in LangChain or LangGraph.
Interoperability Between Them
These frameworks aren't mutually exclusive, and the most common production pattern combines them:
- Use LlamaIndex as the retrieval layer — it does indexing and query engines better than the alternatives
- Wrap that retriever as a LangChain tool for quick integration into a broader toolchain, or call it directly as a node inside a LangGraph graph
- Use LangGraph as the outer orchestration layer once the agent needs branching, retries, or multi-step planning
from langgraph.graph import StateGraph
def retrieve_node(state):
response = llamaindex_query_engine.query(state["query"])
return {**state, "retrieved_context": str(response)}
graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve_node) # LlamaIndex doing the retrieval work
graph.add_node("generate", generate_node) # LangChain-wrapped LLM call
This is the combination we see most often in production: LangGraph for control flow, LlamaIndex for retrieval, and LangChain's model/tool integrations glued in wherever a quick provider-agnostic wrapper is convenient.
Decision Table
| If your project needs... | Reach for |
|---|---|
| A quick linear pipeline: prompt → model → parse | LangChain |
| Provider-agnostic tool/model swapping | LangChain |
| An agent with loops, branching, or human-in-the-loop pauses | LangGraph |
| Long-running agents that need to persist/resume state | LangGraph |
| Deep retrieval over large, heterogeneous data sources | LlamaIndex |
| Custom chunking/indexing strategies beyond basic RAG | LlamaIndex |
| Full control with minimal abstraction overhead | Neither — raw SDK calls |
That last row is worth taking seriously: for a genuinely simple use case (one model call, one tool, no branching), reaching for any of these three adds abstraction you don't need. Frameworks pay for themselves once the complexity they manage exceeds the complexity they add.
Wrapping Up
The real decision isn't "which one wins" — it's matching each framework to the layer of your system it's actually good at: LlamaIndex for retrieval depth, LangGraph for stateful control flow, LangChain for quick composition and integration breadth. Most production systems past a certain complexity end up using two of the three together, and that's not a compromise — it's the intended way to use them.