LLM orchestration frameworks: when abstraction helps, when it hurts, and how to choose between them and direct API calls.
As LLM applications grow beyond a single API call, you face recurring problems: chaining prompts, managing conversation memory, connecting to external tools, and retrieving context from documents. Orchestration frameworks provide reusable abstractions for these patterns.
LangChain is the most popular LLM orchestration framework. It provides composable building blocks for chaining LLM calls with tools, memory, and retrieval.
ChatAnthropic, ChatOpenAI, etc.|) composes components: prompt | model | parserConversationBufferMemory, ConversationSummaryMemory, window-based memory for multi-turn conversationsStrOutputParser, JsonOutputParser, Pydantic-based structured outputfrom langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant that explains {topic} simply."),
("human", "{question}")
])
model = ChatAnthropic(model="claude-sonnet-4-20250514")
chain = prompt | model | StrOutputParser()
result = chain.invoke({"topic": "machine learning", "question": "What is overfitting?"})
LCEL is LangChain's declarative composition syntax. Every component implements a Runnable interface with .invoke(), .stream(), .batch(), and .ainvoke() methods.
Input → Prompt Template → Chat Model → Output Parser → Result
Each step transforms the data and passes it to the next. The pipe operator (|) connects them.
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
# Parallel execution: run two chains simultaneously
parallel = RunnableParallel(
summary=summary_chain,
keywords=keyword_chain
)
# Passthrough: keep original input alongside transformations
chain = RunnableParallel(
context=retriever,
question=RunnablePassthrough()
) | prompt | model | parser
# Branching: route based on input
from langchain_core.runnables import RunnableBranch
branch = RunnableBranch(
(lambda x: "code" in x["topic"], code_chain),
(lambda x: "math" in x["topic"], math_chain),
default_chain # fallback
)
LLMChain / SequentialChain API. Always use LCEL for new projects. The old chain classes are deprecated.LlamaIndex (formerly GPT Index) is purpose-built for connecting LLMs to your data. While LangChain is a general orchestration toolkit, LlamaIndex specialises in retrieval-augmented generation (RAG).
SimpleDirectoryReader, PDF, web, database, and API connectors via LlamaHubVectorStoreIndex (semantic search), SummaryIndex (full scan), TreeIndex (hierarchical), KeywordTableIndexcompact, tree_summarize, refine strategies for assembling final answersfrom llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.llms.anthropic import Anthropic
# Load documents, build index, query
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(
llm=Anthropic(model="claude-sonnet-4-20250514")
)
response = query_engine.query("What are the key findings?")
The most important question is not "which framework?" but "do I need a framework at all?" Many production LLM applications use direct API calls and are better for it.
| Criteria | Direct API | LangChain | LlamaIndex |
|---|---|---|---|
| Simple single-call apps | Best choice | Overkill | Overkill |
| Multi-step chains with branching | Manual wiring | Best choice | Not designed for this |
| RAG over documents | Build from scratch | Supported | Best choice |
| Agent with tool use | Manageable | Best choice | Limited support |
| Debugging transparency | Full control | Moderate | Moderate |
| Switching providers | Code changes | Swap one line | Swap one line |
| Dependency footprint | Minimal | Heavy | Moderate |
Start with direct API calls. Introduce a framework only when you find yourself re-implementing patterns it already provides. If your app is primarily RAG, lean toward LlamaIndex. If it involves complex agent workflows, lean toward LangChain.
Both frameworks support Claude, OpenAI, and open-source models through a unified interface. This makes it easy to benchmark providers or fall back between them.
# Claude
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
# OpenAI
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
# Local / Ollama
from langchain_ollama import ChatOllama
llm = ChatOllama(model="llama3")
# Claude
from llama_index.llms.anthropic import Anthropic
llm = Anthropic(model="claude-sonnet-4-20250514")
# OpenAI
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-4o")
# Local / Ollama
from llama_index.llms.ollama import Ollama
llm = Ollama(model="llama3")
Both read API keys from environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY) automatically.
anthropic SDK, then compare code complexity and readability