Same Agent, Two Frameworks — When to Use What
Introduction
For five lessons you've met the contenders up close — LangGraph's graphs, CrewAI's crews, smolagents' code-writing agents, the OpenAI Agents SDK's primitives. The survey lesson drew the map. This one answers the question that actually keeps a build from starting: which one do I write THIS agent in — and how stuck am I once I do?
Here's the reframe that makes the choice easy. An agent is not a framework. An agent is a prompt, a set of tools, and an eval that says whether it works. A framework is the orchestration shell you run those three things inside. Change the shell and the agent is the same agent. Once you see that, “which framework?” stops being a marriage and becomes a fit decision — reversible, and far lower-stakes than the internet makes it feel.

The Same Agent, Four Shells
Start with the most clarifying exercise in this whole section: take one agent — a tool-using assistant that answers a question with a search() tool — and write it in all four frameworks. Flip between the tabs below and watch the highlighted line.
That line is the same Claude model every single time. The tool is the same. The question is the same. What changes is only the ceremony around them: smolagents hands the model a Python sandbox, the OpenAI SDK gives you Agent + Runner, CrewAI asks you to describe a role, LangGraph hides a state machine behind a one-liner. Same agent; four shells.

This is the whole thesis in one widget. The frameworks disagree about how you describe the loop, not about what the agent is. Keep that in view and the rest of this lesson is just: which description fits your problem — and what does it cost to change your mind later?
Each Framework Makes Something Trivial — and Something Awkward
There is no “best” framework, only best-fit, because each one bends a different way. The honest one-line summary the field has converged on:
The OpenAI Agents SDK makes single-agent loops and handoffs trivial and multi-step branching awkward. LangGraph makes branching and persistence trivial and a dead-simple tool loop verbose. CrewAI makes role-shaped pipelines readable and tight inner loops heavy. smolagents makes code-native tasks trivial and explicit orchestration awkward.
The title of this lesson is literal, so let's make it literal. Here is the same agent — a triage step that hands off to a refund specialist — in the two frameworks that sit at opposite ends of the convenience↔control axis. First, the OpenAI Agents SDK, where a handoff is one list entry:
# OpenAI Agents SDK — a handoff is one list entry. Terse, imperative.
from agents import Agent, Runner
from agents.extensions.models.litellm_model import LitellmModel
claude = LitellmModel(model="anthropic/claude-opus-4-8") # same model
refund = Agent(name="Refund", model=claude, instructions="Process refunds.")
triage = Agent(name="Triage", model=claude,
instructions="Route billing questions to Refund.",
handoffs=[refund]) # <- control transfers here
Runner.run_sync(triage, "Refund order #4471").last_agent.name # -> RefundNow the same routing in LangGraph. It's more to write — but notice what the extra lines buy you: every edge is explicit, and .compile(checkpointer=...) makes the whole run durable, resumable, and inspectable:
# LangGraph — the SAME routing as an explicit graph. More to write,
# but every edge, checkpoint, and retry is now yours to control.
from langgraph.graph import StateGraph, START, END, MessagesState
def triage(s): # an LLM node that picks the route
return {"next": route_with_claude(s["messages"])} # same model
def refund(s):
return {"messages": [process_refund(s["messages"])]}
g = StateGraph(MessagesState)
g.add_node("triage", triage); g.add_node("refund", refund)
g.add_edge(START, "triage")
g.add_conditional_edges("triage", lambda s: s["next"], {"refund": "refund"})
g.add_edge("refund", END)
app = g.compile(checkpointer=saver) # <- durable: resume, time-travelNeither is “better.” If your agent is a couple of handoffs, the SDK version is the right amount of code and the LangGraph version is over-engineering. If your agent must survive a crash mid-run and let a human approve a refund, the SDK version is missing the machinery you need and LangGraph is exactly right. The framework should match the shape of your problem — not the slickness of its landing page.
So… Which One? Climb the Ladder
Here's a procedure instead of a vibe. Almost every real choice is driven by three things: how complex your control flow is, how much durable state you need, and how production-grade it has to be. Score your agent on those and you get a tier — how much framework machinery the problem actually justifies. Try it on something you're building:

Two rules the ladder encodes. First: climb only as high as you must. A rung below that works beats a rung above you'll fight — power you don't use is just ceremony you pay for in tokens, latency, and debugging. Second: the tier sets the machinery; the shape of the work picks the flavor. At the managed-loop tier, role-shaped work wants CrewAI, code-native work wants smolagents, and a minimal mental model wants the OpenAI SDK — all three are the same height on the ladder.
The Migration Tax — and the Portable Core
“I'll just switch later if it's wrong” is the most expensive sentence in agent engineering. The architectural styles really are different — imperative handoffs vs. an explicit graph vs. role-based crews — so porting between them is not a find-and-replace. A widely-repeated field estimate:
Migrating tool definitions, memory schemas, and observability plumbing between frameworks routinely consumes an engineer for a quarter.
That sounds like an argument for choosing perfectly up front. It isn't — it's an argument for making the choice not matter much. Look again at what was identical in every tab of the switcher: the model, the tool's logic, the prompt. Pull those out of the framework and into plain Python that you own, and the framework shrinks to a thin adapter around your real IP:
# Your IP lives OUTSIDE any framework: a plain function + a prompt + an eval set.
def lookup_order(order_id: str) -> str: # the TOOL: pure Python, zero imports
return orders_db.get(order_id, "not found")
SYSTEM = "You are a refund agent. Look up the order before refunding." # the PROMPT
GOLDEN = [("Refund #4471", "refunded"), ("Refund #0000", "not found")] # the EVALS
# Each framework only ADAPTS these three. Swapping frameworks rewrites the thin
# adapter line below -- never the tool, the prompt, or the evals:
# smolagents: CodeAgent(tools=[tool(lookup_order)], model=claude)
# OpenAI SDK: Agent(tools=[function_tool(lookup_order)], instructions=SYSTEM)
# LangGraph: ToolNode([lookup_order]) wired into your StateGraphNow a migration rewrites the three adapter lines, not the agent. The same discipline applies to everything that tends to get welded to a framework: keep observability on an open standard (OpenTelemetry traces) instead of one vendor's dashboard, and keep your eval set framework-agnostic so it can grade any implementation of the agent. Treat MCP and A2A the same way — as portability standards that let your tools and your agents outlive whichever shell is fashionable this quarter. Your moat is the prompt, the tools, and the evals. The framework is rented.
Anthropic's Take: Start Without One
It's worth hearing the most-cited guidance in the field, because it cuts against the instinct to reach for a framework on day one. From Anthropic's Building Effective Agents, drawn from working with dozens of teams:
The most successful implementations weren't using complex frameworks or specialized libraries. Instead, they were building with simple, composable patterns… If you do use a framework, make sure you understand the underlying code — incorrect assumptions about what's under the hood are a common source of error.
The practical default that follows: start with the model API directly. A surprising number of “agents” are a short loop you can read in one screen — and when you write it yourself, there's no hidden control flow to misunderstand at 2am:
# Often you need NO framework. A tool-using agent is a short, honest loop:
import anthropic
client = anthropic.Anthropic()
msgs = [{"role": "user", "content": "Who won the 2022 World Cup?"}]
while True:
r = client.messages.create(model="claude-opus-4-8", max_tokens=1024,
tools=TOOLS, messages=msgs)
msgs.append({"role": "assistant", "content": r.content})
if r.stop_reason != "tool_use":
break # <- final answer: done
msgs.append({"role": "user", "content": run_tools(r.content)}) # run, then loop
# ~12 lines. You own every token and every decision -- nothing hidden to misunderstand.This is rung zero of the ladder, and it's the right rung more often than the framework marketing suggests. Build the workflow first; reach for an agent framework only when the orchestration genuinely outgrows what you'd want to maintain by hand — real branching, durable state, human checkpoints. A framework you adopt because the problem demanded it is leverage. A framework you adopt to look serious is a layer of someone else's assumptions between you and your bug.
The Honest Take — and Where It's Converging
As of 2026, the reassuring news: all four are production-viable. You will not fail because you picked LangGraph over CrewAI; you'll fail because your prompt, tools, or evals were weak — and those are portable, so the framework was never the risk.
A few patterns worth knowing:
- Prototype fast, harden later. A common real-world path is to validate the architecture in CrewAI or the OpenAI SDK in an afternoon, then migrate the production-critical paths to LangGraph once the shape is proven and you need durability and observability. The portable core is what makes that migration sane.
- The list keeps growing. Microsoft's Agent Framework, Google's ADK, Pydantic AI, and more arrive constantly. That's not a reason to wait for a winner — it's the strongest reason to keep your core framework-agnostic, so a new entrant is a try it in a branch, not a rewrite.
- The shells are converging. Streaming, tool-calling, handoffs, checkpointing, and MCP/A2A support are diffusing into all of them. The differences that remain are about which idiom feels native to your problem, which is exactly the fit question this lesson is about.
So don't agonize. Pick the thinnest shell that fits the shape of your problem, keep your IP portable, and ship.
🧪 Try It Yourself
Take a real agent you want to build — a PR reviewer, a research assistant, a support triage bot, anything. Run it through this lesson's two tools and write down the answers; the goal is a decision you can defend, not a guess:
- Climb the ladder. Open the decision guide above and answer for your agent's control flow, state, and production needs. Note the recommended rung — and notice every rung below it that you were about to skip past. Could the simpler one actually work? (Honestly, for a first version, it often can.)
- Write the first ten lines. Open the four-shell switcher and find the framework your rung points to. Type its opening lines for your agent — just the agent + its one most important tool. Feel which idiom fits your mental model: are you describing a graph, a role, a handoff, or a script?
- Name your portable core. Write down the three things that would not change if you switched frameworks next month: your prompt, your tool(s), your eval set. Then write the one-line adapter that wires them into your chosen framework. That separation — portable core vs. rented shell — is the single habit that makes every later framework decision cheap.
If you can do those three for your own agent, you've internalized the entire section: the framework is a fit decision, not a faith decision.
Mental-Model Corrections
- “The framework is the agent.” No — the framework is the shell. The agent is your prompt + tools + evals, and those run in any of them. Pick (and switch) shells accordingly.
- “The OpenAI Agents SDK only runs GPT.” A persistent myth. It's optimized for OpenAI models but provider-agnostic via LiteLLM — every example in this lesson runs it on Claude (
LitellmModel(model="anthropic/claude-opus-4-8")). - “Pick the most powerful framework to be safe.” Backwards. Power you don't use is ceremony you pay for — in tokens, latency, learning curve, and debugging surface. Climb only as high as the problem forces you.
- “I'll switch later if I'm wrong, no big deal.” Migrations routinely cost an engineer-quarter. The fix isn't to choose perfectly — it's to keep the prompt, tools, and evals portable so the choice is cheap to revisit.
- “More agents / more framework = more capable.” Often the opposite. A clean ~12-line API loop beats a misunderstood graph. Anthropic's own guidance: simple composable patterns first; frameworks only when the orchestration demands it.
Key Takeaways
- An agent is a prompt + tools + evals; a framework is the orchestration shell it runs in. That reframe turns “which framework?” from a marriage into a reversible fit decision.
- The same agent, the same Claude model, runs in all four — only the ceremony differs. smolagents (code), OpenAI SDK (primitives), CrewAI (roles), LangGraph (graph).
- Each makes something trivial and something awkward. SDK: handoffs trivial, branching awkward. LangGraph: branching/persistence trivial, simple loops verbose. CrewAI: role pipelines readable, tight loops heavy. smolagents: code-native trivial, explicit orchestration awkward.
- Choose by the shape of your problem, then climb the ladder only as high as you must: no framework → managed loop → explicit graph → durable orchestration. A rung below that works beats a rung above you'll fight.
- Default to less. Anthropic's field-tested advice: start with the model API and composable patterns; adopt a framework only when orchestration genuinely outgrows hand-rolled code — and understand the code underneath it.
- Keep the core portable. Prompts, tools, evals, and tracing belong in your code on open standards — so a framework swap (an engineer-quarter if you've welded yourself in) becomes a one-line adapter change. Your moat is the IP; the framework is rented.