Agent Frameworks Compared: Claude Agent SDK, LangChain, LlamaIndex

Learn what each major agent framework actually buys you, where each one shines, and when the smartest move is to skip the framework entirely and write the loop yourself.

The Framework Trap I Fell Into

When I first started building agents, I did what most people do: I Googled "best AI agent framework" and immediately got sucked into a rabbit hole comparing LangChain versus LlamaIndex versus AutoGen versus a dozen other options. I spent two days reading docs before I'd written a single line of agent code.

That was a mistake. Not because frameworks are bad — they're genuinely useful — but because I didn't understand what problem each one was actually solving. I was optimizing my toolchain before I understood the job.

So let me give you the comparison I wish I'd had. Not a feature matrix, but an honest breakdown of what each framework actually buys you, where each one gets in your way, and the situations where the right answer is just... writing the loop yourself.

What an Agent Framework Actually Does

Before comparing, let's be clear on what we're comparing. An agent is fundamentally a loop: the model reasons about a task, picks a tool, executes it, gets back a result, and decides what to do next. That loop can be five lines of Python or five hundred. Frameworks exist to handle the boilerplate in between — tool registration, memory management, multi-step orchestration, output parsing, retries.

The question is: how much of that boilerplate does your project actually need?

The Claude Agent SDK: Tight, Purposeful, Model-Native

Anthropic's own SDK (the anthropic Python package) isn't a full framework, but it's grown into a solid foundation for Claude-specific agents. The key feature is native tool use — you define your tools as JSON schemas, and Claude handles the call-and-response cycle cleanly.

python
# Minimal agent loop with the Claude SDK
import anthropic

client = anthropic.Anthropic()
tools = [
{
"name": "search_web",
"description": "Search the web for current info",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}
]

messages = [{"role": "user", "content": "What's the latest on AI regulation?"}]

# The loop — you write this yourself
while True:
response = client.messages.create(model="claude-opus-4-5", tools=tools, messages=messages)
if response.stop_reason == "end_turn": break
# handle tool_use blocks, append results, continue

What you get: clean tool calling, streaming support, vision, extended thinking, and direct access to every model feature the moment Anthropic ships it. What you don't get: memory management, vector stores, multi-agent orchestration, or any of the higher-level abstractions.

When to reach for it: You're building something Claude-specific, you want minimal dependencies, or you're prototyping fast and want zero magic. It's also the right choice when you need bleeding-edge Claude features — frameworks always lag behind the native SDK by weeks or months.

LangChain: The Ecosystem Play

LangChain is the 800-pound gorilla. It launched early, grew fast, and now has integrations for what feels like every LLM, vector store, tool, and data source on the planet. When people say "I want to build an agent," LangChain is usually the first thing they try.

The core value proposition is breadth. You can swap Claude for GPT-4o for Gemini with one line. You can plug in Pinecone, Weaviate, Chroma, or pgvector without rewriting your retrieval logic. There's a community solution for almost every use case you can imagine.

python
# LangChain agent with Claude — the abstracted version
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate

llm = ChatAnthropic(model="claude-opus-4-5")
tools = [search_tool, calculator_tool] # pre-built or custom
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}")
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
executor.invoke({"input": "Research AI regulation and summarize"})

The real cost of LangChain is debugging. When something goes wrong inside a chain or agent executor, the stack traces are genuinely painful. You're often troubleshooting abstractions-of-abstractions. I've spent hours tracking down a bug that turned out to be how LangChain was formatting the tool result before passing it back to the model.

LangChain has also gone through major API changes (LCEL, the Runnables refactor, LangGraph) that have left their docs partially outdated. A tutorial from 12 months ago may use patterns that are now deprecated.

When to reach for it: You need model portability, you want pre-built integrations (dozens of tool connectors, vector stores, document loaders), or your team is already on the LangChain/LangSmith ecosystem. Also consider LangGraph specifically if you're building complex stateful multi-agent workflows — it's the most mature option for that use case.

LangChain vs LangGraph

These are distinct. LangChain is the broader ecosystem. LangGraph is the graph-based orchestration layer for multi-step, stateful agents. If you're doing serious agent work with LangChain, you probably want LangGraph — but it adds another learning curve on top.

LlamaIndex: When Your Agent Needs Documents

LlamaIndex started life as a pure RAG library — it was called GPT Index, actually — and that DNA is still its biggest strength. If your agent needs to query documents, ingest PDFs, search a knowledge base, or do anything retrieval-heavy, LlamaIndex has thought harder about that problem than anyone else.

The framework has grown to support agents and multi-step reasoning, but its sweet spot remains the retrieval layer. It has sophisticated chunking strategies, metadata filtering, hybrid search, re-ranking, and document parsing that genuinely outclass what you'd wire up by hand.

python
# LlamaIndex agent over a document corpus
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import QueryEngineTool
from llama_index.llms.anthropic import Anthropic

docs = SimpleDirectoryReader("./my_docs").load_data()
index = VectorStoreIndex.from_documents(docs)
query_engine = index.as_query_engine()

tool = QueryEngineTool.from_defaults(
query_engine=query_engine,
description="Search internal documentation"
)
agent = ReActAgent.from_tools([tool], llm=Anthropic(model="claude-opus-4-5"))
response = agent.chat("What does our refund policy say about digital goods?")
print(response)

Where LlamaIndex is weaker: general-purpose tool calling that isn't retrieval-shaped, and multi-agent orchestration at scale. It's also gone through API changes (the move to llama_index.core) that can trip you up on older tutorials.

When to reach for it: Your agent's primary job is answering questions over documents. Internal knowledge bases, customer support bots over product docs, research assistants that need to cite sources — LlamaIndex is often the right call here.

When Writing the Loop Yourself Is the Right Answer

Here's the thing nobody's framework marketing will tell you: for a surprising number of projects, you don't need a framework at all. The agent loop is just not that complicated to write yourself.

python
# A complete, functional agent loop — no framework needed
import anthropic, json

client = anthropic.Anthropic()

def run_agent(user_message, tools, tool_handlers):
messages = [{"role": "user", "content": user_message}]
while True:
resp = client.messages.create(
model="claude-opus-4-5", max_tokens=4096,
tools=tools, messages=messages
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
return next(b.text for b in resp.content if b.type == "text")
tool_results = []
for block in resp.content:
if block.type == "tool_use":
result = tool_handlers[block.name](**block.input)
tool_results.append({
"type": "tool_result", "tool_use_id": block.id,
"content": json.dumps(result)
})
messages.append({"role": "user", "content": tool_results})

That's roughly 25 lines. It handles multi-turn tool use, it's easy to debug, and it gives you complete visibility into every message in the conversation. When something breaks, you know exactly where to look.

I reach for the bare SDK when: I'm prototyping something new and don't want framework opinions getting in the way; the agent has fewer than five tools; I need fine-grained control over the message history; or I'm building something that needs to stay maintainable by someone who doesn't know LangChain internals.

The Framework Cost Is Real

Every framework adds dependency risk, upgrade pain, and a debugging tax. Before adopting one, ask: what does this give me that I can't write in an afternoon? If the answer is "not much," stay vanilla.

The Decision Cheat Sheet

Here's how I actually pick:

Use the bare Claude SDK if: you're Claude-only, you want maximum simplicity, you're prototyping, or you need immediate access to new Claude features.

Use LangChain/LangGraph if: you need model portability, you want the pre-built integration ecosystem, or you're building complex stateful multi-agent workflows and want an established orchestration layer.

Use LlamaIndex if: your agent's core job is retrieval over documents. It's the strongest framework for anything knowledge-base shaped.

Write it yourself if: your task is focused, you have fewer than a handful of tools, debugging transparency matters, or you simply don't want the maintenance overhead of a framework dependency.

The honest truth? Most beginners reach for a framework too early and end up fighting the abstraction instead of the actual problem. Start simple. The bare SDK will take you further than you expect. Add a framework when you hit a wall that the framework actually fixes — not before.

Keep going

More tutorials in this category, or explore the full field guide.

More Claude Code Tutorials Official Docs ↗