Production Agent Architecture: The Agentic Loop (Part 1)
Lessons learned while preparing for the Claude Architect Certification
What is an agentic loop?
When people hear the word “agent,” they often imagine an AI that can think and solve problems on its own. But many apps that call themselves “AI agents” are really just one prompt and one response. Let’s look at an example.
Suppose we want to build a customer support assistant. It should answer questions about orders.
A user asks:
“My package hasn’t arrived yet. Can you check what’s happening?”
Our first version is simple. The app sends the user’s message to the model, together with a system prompt like this:
You are a helpful customer support assistant. Answer the user’s questions.
The model replies:
“I’m sorry your package hasn’t arrived. Please contact our support team.”
Technically, this works. The model wrote a fluent, polite answer. The user got a reply. But did it actually solve the problem? Not really.
The model has no way to check the order status. It cannot contact the shipping company. It cannot find out if the package was delivered, delayed, or lost.
It cannot even ask the user for the missing order number. Most importantly, it cannot decide what information it needs before it answers. It just writes text from what it already knows.
This is the limit of a single model call. The model can only reason over the context you give it, no matter how capable it gets. If it needs more information, or needs to talk to another system, it gets stuck.
This is exactly where an agentic loop becomes useful.
Instead of producing one response, the app lets the model work in repeated steps. On each step, the model can:
- look at the current situation
- decide what information is missing
- call a tool to get that information
- check the result
- and continue until the task is done
We call this repeating cycle an agentic loop.
The loop doesn’t make the model smarter. It gives the app a clear, structured process. The model gathers information, makes decisions, and reacts to new information, one step at a time, until the task is complete.
A pseudocode example
messages = [user_message]
loop:
response = call_model(messages, tools)
if response.stop_reason == "end_turn":
return response.text
if response.stop_reason == "tool_use":
result = run_tool(response.tool_call)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [tool_result(response.tool_call.id, result)]})
continue
raise UnhandledStopReason(response.stop_reason)
This pseudocode is close to the real thing. It’s not a simplified version made just for the article.
Here is run_agentic_loop() from the example project. It uses one tool, get_weather, instead of order lookups. The logic stays the same:
def run_agentic_loop(user_message: str, max_iterations: int = DEFAULT_MAX_ITERATIONS) -> str:
messages: list[dict[str, Any]] = [{"role": "user", "content": user_message}]
for _ in range(max_iterations):
response = call_model(messages)
if response.stop_reason == "end_turn":
return extract_final_text(response)
if response.stop_reason != "tool_use":
raise RuntimeError(f"Unhandled stop_reason: {response.stop_reason}")
tool_results = run_requested_tools(response)
append_tool_turn(messages, response, tool_results)
raise RuntimeError(f"Exceeded max_iterations ({max_iterations}) without end_turn")
These four branches always run in the same order. stop_reason decides what happens next — nothing else does.
call_model, run_requested_tools, and append_tool_turn each do one small job. You can find the full file on GitHub.
Model-driven decisions, not a fixed pipeline
There’s one more distinction worth making explicit: nothing in this loop forces the model to call a tool. On every single call, the model looks at the conversation and decides for itself whether it needs get_weather at all.
Ask the loop “What’s the weather in Budapest?” and it calls the tool, because it needs to. Ask it “What’s 2 + 2?” and it answers directly — stop_reason comes back end_turn on the very first call, with no tool_use block at all. Same loop, same tool available, different question, different path through the code.
Compare that with a hardcoded pipeline: a fixed sequence like “always call get_weather, then always format the result.” That kind of pipeline doesn’t reason about whether the step is needed — it just runs it, every time, regardless of what was actually asked. The example project’s run_hardcoded_pipeline() does exactly that, so you can see the difference side by side.
This is what “model-driven decision-making” means in practice. The loop’s code doesn’t encode a sequence of steps. It encodes a rule: call the model, see what it wants, do that, repeat. What actually happens on any given run is up to the model, not the control flow.
Critical design decisions
Building the loop itself is usually not the hard part. In a real project, nobody asks an architect “what is an agentic loop?” They ask for a loop that works reliably in production.
The hard part is different. You need to decide when the loop should keep going, when it should stop, and how it should recover when something breaks. At what point should the agent stop acting on its own and hand the task to a human?
A well-designed agentic system needs clear rules for:
- Termination: How does the agent know it’s done? This is the point the exam checks most closely.
stop_reasonis the only signal you should trust — not the text content, not a guess. You can still usemax_iterationsas a safety net, but only as a last resort. It should never be the normal way to decide a conversation is over. - Retries: Which failures are worth trying again?
- Error handling: How does the workflow recover safely?
- State management: What information should stay between steps, and what should get dropped?
- Context management: What information does the model actually need to carry forward?
- Human in the loop: When should a person take over?
Consequences
Skipping these decisions doesn’t make the loop simpler. It just moves the problem somewhere less visible.
- No termination rule -> the loop stops too early, or never stops at all.
- No retry strategy -> one small, temporary failure (a network blip, a rate limit) kills the whole task. A second try might have worked.
- No state or context management -> the message history keeps growing until you hit the context limit. Or the model loses track of the original goal.
- No human-in-the-loop rule -> the agent keeps acting on its own, even in cases where a person should sign off first, like a payment or another action you can’t undo.
This is the real gap between a demo that works on a good day and a system an architect is willing to stand behind. None of these ideas are complicated. You just have to decide them on purpose, instead of by accident.