Home / Articles

An Agent Is a While Loop: Where LangChain create_agent and LangGraph Fit

A tool-calling agent in twenty lines, the four production problems that loop hits, and when to move from LangChain create_agent to LangGraph state graphs.

Publisher: REACTAPP.TOP8 min read1816 words

This article is published in English.

Sources: An Agent Is a While Loop

Tool use gets a lot of attention, but the part that decides when to call a tool, what to do with the output and when to quit is the loop wrapped around the model. People call that loop orchestration, a word that makes it sound bigger than it is. The simplest way to see this is to look at the entire thing first, then look at why it isn't sufficient for production.

A whole agent in about twenty lines

Take the tool-calling exchange, where the model requests a tool, your code executes it and the output returns to the model, and put a while loop around it. The conversation is seeded first:

messages = [system_prompt, user_question]

Then the loop itself:

while True:
    reply = model(messages, tools=TOOLS)    if not reply.tool_calls:
        return reply.text                    # the model answered; done    for call in reply.tool_calls:
        result = TOOLS[call.name](**call.args)   # your code runs it
        messages.append(tool_result(call.id, result))

That's a real agent, not a cartoon of one. The model picks which tool to call. Your code runs it and appends the result. This repeats until the model returns text rather than a tool request.

If it feels a little disappointing that it's so small, that reaction is the point. Anything a framework supplies is something you add once this loop meets production.

One run, step by step

Consider an on-call triage agent asked why overnight carrier syncs have failed with ERR_4412 since the December release. It reaches an answer in three round trips, on one loop with no framework.

Turn 1. The model doesn't answer yet. It emits a request, search_runbooks("ERR_4412"), and the loop executes it. Three results come back, and the top one says the error means an expired carrier auth token. That gets appended to the messages.

Turn 2. Having read that, the model requests get_release_notes("december"). One document returns: the token refresh interval was cut from 24 hours to 12. Appended again.

Turn 3. Holding both results, the model replies in plain text with no tool request. Syncs that run overnight last more than twelve hours, so the token now lapses in the middle of a run. With no request to execute, the loop exits.

The model chose the tools and the loop ran them. The message list grew by two results, and nothing else happened.

What breaks after a week of running it

Four problems tend to appear in production, usually in this order. None concern the model. All concern the runtime around it.

The restart. Turn 2 is stuck on a slow incident system, eight minutes in, when the container is recycled. Since messages lived only in memory, it vanishes and the run begins again from the question. The cure is saving the conversation after each turn and resuming from that save.

The dangerous tool. On another night, at turn 4 the model concludes the fix is rollback_release("dec-2025"). Letting a model do that alone at 3 a.m. is not something anyone wants. The loop has to stop, ask a person, wait, possibly until morning, and then carry on exactly where it paused. A bare while loop can't wait until morning.

The wrong turn. Turn 5 chooses a poor search query, and turns 6 through 16 repeat that search with small rewordings, each one billed. Staring at a sixteen-item message list won't show where it went wrong. What helps is a cap on steps and a recorded trail of each step that can be replayed.

Which way to go. Authentication errors call for one sequence of steps and data errors for another, and when two tools disagree the case should go to a human. A model is decent at picking a tool but shouldn't improvise control flow differently every run. The branches should be written down so they behave the same each time.

Look again at the four. Not one blames a clever or a clumsy model. They all trace to the surrounding code, meaning what it persists, when it halts, what it logs and who picks the route. Orchestration is that surrounding layer, which is why a loop of twenty lines can be perfectly right and still be a place to move past.

Turn the steps into boxes

Draw the same run again with each step as a box joined by arrows: classify, search runbooks, get release notes, propose a fix, approve, then apply or answer. With steps as boxes, three new abilities appear.

  • Each box ends by saving state to durable storage, so after a restart the run picks up at the last finished box and not at the beginning.
  • Any box can pause the entire run and wait for a person for as long as it takes, with state stored safely.
  • Because steps are discrete, you can rewind to any box and run forward with different input to see where things went wrong.

The branching lives in the classify box, with an auth path, a data path and an escalation. You drew those three arrows. The model still chooses tools within a path, but the path itself is yours.

The model didn't change. The loop gained a shape you can see, save and interrupt.

What LangChain is

LangChain is a Python library, with a JavaScript twin, that supplies ready-made pieces for LLM applications so the same plumbing isn't rewritten each time. Three kinds of piece matter.

Model wrappers. One interface across providers. ChatAnthropic, ChatOpenAI and ChatGoogleGenerativeAI all expose the same .invoke(messages), so changing vendors is a one-line edit and not a rewrite of the request code.

Integrations. Adapters for what surrounds the model: vector stores, document loaders, retrievers, embedding models and several hundred outside tools. Here lies most of the saved effort, because each adapter would otherwise need writing and upkeep by you.

A packaged agent loop. Since version 1.0 in October 2025, the front door is create_agent, imported like this:

from langchain.agents import create_agent

and used like this:

agent = create_agent(
    model="anthropic:claude-sonnet-4-6",
    tools=[search_runbooks, get_release_notes],
    system_prompt="You are the on-call triage assistant.",
)
result = agent.invoke({"messages": [("user", "Why is ERR_4412 failing?")]})

In effect the loop from earlier arrives pre-written. Middleware is supported as well: small hooks that fire around a model call or a tool call. Existing ones cover pausing for human sign-off before a tool executes, scrubbing personal data, and condensing a long conversation.

What create_agent leaves out is control of the loop's insides: no look at state between steps, no day-long pause, and a crashed process takes the run down with it.

What LangGraph is

Underneath create_agent sits LangGraph, a different library from the same maker. LangChain hands over a finished loop, while LangGraph hands over the parts for building your own, shaped like the graph drawn above. You define three things.

State. A typed object that travels through the run. For triage it would hold the messages so far, the error classification, the proposed fix and whether it has been approved.

Nodes. Ordinary Python functions that receive the state and return updates. classify, search_runbooks, propose_fix and ask_human are each just a function.

Edges. The order of execution. Plain edges are fixed arrows, and conditional edges are functions that read the state and select the next node, so the auth-versus-data split is expressed there.

The import:

from langgraph.graph import StateGraph

And a graph built from those pieces:

g = StateGraph(TriageState)
g.add_node("classify", classify)
g.add_node("search", search_runbooks)
g.add_node("propose", propose_fix)
g.add_node("approve", ask_human)
g.add_edge("classify", "search")
g.add_conditional_edges("search", route_by_error_type)
g.add_edge("propose", "approve")app = g.compile(checkpointer=PostgresSaver(...))

One line near the end carries the weight: a checkpointer, meaning a place to persist state, passed in when the graph is compiled. With it, state is written after every node. Restart survival, interrupt() for waiting on a person, and rewinding to a node to replay forward all come from that one argument rather than hand-written code. Your contribution is the boxes and the arrows.

So the contrast is this. LangChain gives a loop you can't see into. LangGraph asks you to draw the loop, and in exchange you can inspect it, persist it, halt it and replay it. The four breakages above are the four moments when not seeing inside stops being acceptable.

One stack, two doors

Since create_agent is built on a LangGraph graph, picking LangChain or LangGraph isn't a framework decision. It's a question of which door to use, and the two combine. A create_agent agent is a graph itself, which means it can be placed as a single node inside a bigger StateGraph. The bigger graph takes on classification, approvals and persistence, while the inner agent covers the stretch where the model should decide.

Use create_agent when a working agent is wanted today and its inside doesn't matter. Plenty of agents never need more. Go down to StateGraph once you must look at state mid-run, wait on a person, recover from a crash or rewind, which are the same four problems listed earlier.

The vendor SDKs from Anthropic and OpenAI sit beside this stack rather than inside it. They run the same loop, tied to one model family, with the least friction if you use a single vendor. The price is portability.

Advice for a team

Write the twenty-line loop first, even if it will be thrown away. It takes an hour, and afterward it's clear what any framework is doing on your behalf, which is the only way to debug one.

Switch to LangGraph on the day one of those four problems really hits, and no sooner. A chatbot that calls three tools and answers can do without a graph. An agent expected to outlive a restart, wait for approval or be replayed cannot, and durable execution built from scratch is a project in its own right.

Teams often grab the graph on day one for a problem the simple loop handles, then burn a quarter learning the framework when that time belonged to the problem itself.