Building an AI Agent With LangGraph
Most LangGraph agent tutorials start you at the hardest place. This one shows the one-line prebuilt path first — and exactly when to graduate to a hand-built graph.
from langgraph.prebuilt import create_react_agent
# tools are plain functions; the docstring becomes the description
agent = create_react_agent(model, tools=[search_docs, lookup_order])
result = agent.invoke(
{"messages": [{"role": "user", "content": "Where is order ORD-5?"}]}
)
print(result["messages"][-1].content)Here's a stat that should change how you approach this: the overwhelming majority of LangGraph agents that reach production for standard tool-calling never hand-build a graph at all — they use the prebuilt create_react_agent in a line or two. Yet nearly every LangGraph agent tutorial opens by walking you through nodes, edges, and state by hand, which is the hardest possible on-ramp for the most common case. This one goes the other way.
Let me show it through a team that learned the ordering the hard way.
The Problem: A LangGraph Agent Tutorial That Skipped the Prebuilt Path
A three-person team wanted an agent that answered support questions using two tools: a docs search and an order lookup. Standard stuff — a model that calls tools in a loop until it can answer. They followed a popular tutorial that built everything manually: a StateGraph, node functions, edges wiring the model to the tools and back, a conditional edge to decide whether to loop or finish.
It took them three days and about two hundred lines. It worked, eventually. Then they discovered that LangGraph ships create_react_agent, a prebuilt that does exactly the standard tool-calling loop, and realized their two hundred lines reproduced — less reliably — what a one-liner already did.
Every one of those lines was correct, which is what made it frustrating. They hadn't done anything wrong in the tutorial's terms. They'd just followed a tutorial that answered a question — how do LangGraph's primitives work — that wasn't the question their task was asking, which was simply how to ship a tool-calling agent.
The Wrong Approach
Their mistake wasn't using LangGraph. It was starting at the wrong level of the framework. LangGraph has a deliberate two-level design: high-level prebuilts for common patterns, and the low-level StateGraph primitive for when you need custom control. They'd reached for the primitive to build something the prebuilt already handled.
The manual graph gave them nothing their task needed. A standard tool-calling agent is a solved pattern; reimplementing it by hand just meant more code to maintain and more places for their version to diverge from the well-tested one.
There's a deeper cost too. By hand-building the standard loop, they'd taken on maintenance of something LangGraph's team maintains for them. Every upgrade that improved the prebuilt agent passed them by, because they were effectively running their own fork of the pattern. Reimplementing a solved problem doesn't just cost the initial days — it opts you out of every future improvement to the thing you reimplemented.
⚡ Pro tip: For any standard tool-calling agent, start with create_react_agent. Reach for a hand-built StateGraph only when you need control the prebuilt doesn't give you — not before, because the primitive is the expert tool, not the beginner one.
The Correct Pattern
They deleted the two hundred lines and replaced them with the prebuilt.
[object Object], langgraph.prebuilt ,[object Object], create_react_agent
,[object Object],
agent = create_react_agent(model, tools=[search_docs, lookup_order])
result = agent.invoke(
{,[object Object],: [{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]}
)
,[object Object],(result[,[object Object],][-,[object Object],].content)What this does: it builds a complete tool-calling agent — the model, the loop, the tool dispatch, the stop condition — in one call. create_react_agent wires the ReAct pattern (reason, act, observe, repeat) for you, so you write the tools and nothing else. This is the same agent their two hundred lines built, minus the two hundred lines.
Notice what you still write: the tools. create_react_agent handles the orchestration, but the tools — and their docstrings — are yours, and they're where the agent's real behavior comes from. The prebuilt saves you the plumbing, not the thinking about what your agent should be able to do.
⚡ Pro tip: In LangGraph, a tool is a normal Python function and its docstring becomes the description the model reads. Everything you know about writing sharp tool descriptions applies directly to the docstring — that text does the heavy lifting.
Results and What Changed
The rewrite cut their agent code by roughly 90% and made it more reliable, because the prebuilt is battle-tested across thousands of deployments while their hand-rolled loop was tested across three days.
The reliability gain is easy to underrate. A prebuilt used across thousands of production agents has had its edge cases found and fixed by other people's incidents. Your three-day version has had its edge cases found by your customers. Standing on well-worn code is a feature, not a compromise.
Then, weeks later, a real requirement arrived that finally justified the graph: before issuing any refund, a human had to approve. That's not the standard loop — it's a branch that pauses for outside input. This time, dropping to StateGraph was the right call, because now they needed control the prebuilt didn't offer.
[object Object], langgraph.graph ,[object Object], StateGraph, START, END
builder = StateGraph(State)
builder.add_node(,[object Object],, decide_node)
builder.add_node(,[object Object],, approval_node)
builder.add_node(,[object Object],, execute_node)
builder.add_edge(START, ,[object Object],)
builder.add_conditional_edges(,[object Object],, route, {,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],})
builder.add_edge(,[object Object],, ,[object Object],)
builder.add_edge(,[object Object],, END)
graph = builder.,[object Object],()What this does: it builds a custom flow where a routing function sends risky actions through a human-approval node and safe ones straight to execution. This is exactly what the graph model is for — cycles, branches, and human-in-the-loop that the prebuilt can't express. The complexity is now buying something.
This is the payoff of understanding the two levels. The team didn't fight the framework or over-build up front; they used the simple thing until a real requirement demanded the powerful thing, then reached for exactly the powerful thing they needed. That sequencing — simple until proven insufficient — is the whole art of using LangGraph well.
⚡ Pro tip: The signal that you've genuinely outgrown create_react_agent is a requirement the standard loop can't express — a human approval step, a branch, a cycle with custom routing. Until you hit one of those, the prebuilt is not a limitation, it's a gift.
How to Apply This to Your Situation
Start every LangGraph project with create_react_agent and real tools. Get it answering correctly first. That alone covers a large share of agent use cases, and you'll have working software in an hour instead of three days.
Concretely, this ordering fits most teams. A startup shipping a support agent uses the prebuilt and moves on. A fintech team adds a StateGraph only around the approval gate their compliance team requires. A data team building a multi-step research pipeline reaches for the graph because the steps genuinely branch. Prebuilt for the common case, graph for the specific need — the same rule across very different projects. The instinct to build the general thing first is natural for engineers, but with frameworks that ship strong defaults, it's usually the expensive path.
When a requirement appears that the standard loop can't handle — approval gates, multi-step branching, resumable long-running flows — then drop to StateGraph for that specific need. You'll understand exactly why you're adding the complexity, which makes the graph easier to design and debug.
And when you do build a graph, keep the state definition small. The State TypedDict is the data flowing between nodes; the leaner it is, the easier the graph is to reason about.
A good habit: add a field to State only when a node actually needs to read or write it. State that exists "just in case" is state every future node has to account for and every bug has to be checked against — lean state is faster to build and far faster to debug.
⚡ Pro tip: Keep your graph's State as minimal as the task allows. Every field is something each node might read or write, so a bloated state object makes every node harder to understand and every bug harder to trace.
Next Steps
The right LangGraph agent tutorial ordering is prebuilt first, primitive second. Use create_react_agent for standard tool-calling agents and drop to StateGraph only when a real requirement — a branch, a cycle, a human approval step — needs control the prebuilt can't give. Starting at the primitive is the single most common way teams turn an hour of work into three days. The framework isn't the enemy and neither is simplicity — the enemy is mismatching the two, reaching for power you don't need or hand-rolling a pattern that already exists and is better maintained than yours ever will be.
⚠️ Common mistake: Hand-building a StateGraph for a standard tool-calling agent because that's what the tutorial showed, ending up with hundreds of lines that reproduce, less reliably, what a prebuilt one-liner already does. Match the level of the framework to the complexity of your task, not to the length of the tutorial you happened to read.
Whether you use the prebuilt or a custom graph, the tool docstrings and system instructions that steer the agent are the same reusable assets. PromptABCD keeps them versioned in one place, so when you graduate from create_react_agent to a hand-built graph, your carefully written tool descriptions carry over untouched instead of being retyped into node functions.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
