How an agent actually thinks and acts: ReAct, tool calling, and the loop that powers every agent
In the first post, we described an agentic system as software that can plan, reason, use tools, and execute multi-step workflows. This sentence hides the most important idea in the whole field. What is actually happening when an agent “thinks”? The honest answer is surprisingly simple, and once you see it, every agent framework on the market stops looking like magic and starts looking like a single design pattern repeated with variations.
The pattern is a loop. A plain language model answers your prompt once and stops. An agent wraps that same model in a loop (its output is fed as its next input and so on) that keeps running - reasoning, taking an action, observing the result, and reasoning again - until the goal is met. That loop is the heartbeat of every agent you will ever build.
From one-shot answers to a loop
A chatbot is a function: text in, text out, done. The model never finds out whether its answer was any good. An agent breaks that single shot into a cycle. After each step it gets feedback from the real world and uses that feedback to decide what to do next. This is the difference between a student who writes an exam answer and walks away and a mechanic who turns a bolt, checks whether the engine starts, and adjusts.
The most influential way to structure that cycle is the ReAct pattern, introduced by Yao et al. in 2022. ReAct stands for Reason + Act, and it interleaves three things on every turn:
- Thought - a short chain-of-thought where the model reflects on the situation and decides what to do.
- Action - a concrete step, usually calling a tool or an API.
- Observation - the result that action produced, fed straight back into the model.
By forcing the model to reason in the open and ground each decision in a real observation, ReAct reduces hallucination: the model cannot quietly invent a fact when the next line of the transcript is the actual output of a search it just ran.
A traced example
Suppose the goal is "What was the closing price of the stock the CEO of Tesla most recently tweeted about?” A ReAct trace might look like this:
Thought: I need to find Elon Musk's most recent tweet about a stock.
Action: search_tweets(user="elonmusk", topic="stock")
Observation: "Just bought more $X ..." (posted 2h ago)
Thought: The ticker is X. I need today's closing price.
Action: get_quote(ticker="X")
Observation: { "close": 41.80, "currency": "USD" }
Thought: I now have the answer.
Action: finish("The closing price was $41.80.")
Notice that no single model call could have answered this. The agent had to act, look at what came back, and only then decide its next move. That is the entire value proposition of the loop.
Building the loop from scratch
Stripped of any framework, an agent loop is about a dozen lines. The model returns a structured decision; your code executes it and feeds the result back:
def run_agent(goal, tools, max_steps=8):
history = [system_prompt(goal, tools)]
for _ in range(max_steps):
decision = llm(history) # model reasons + picks an action
if decision.action == "finish":
return decision.answer
tool = tools[decision.action]
result = tool(**decision.args) # YOUR code runs the tool
history.append(decision)
history.append(f"Observation: {result}")
return "Stopped: step budget exhausted."
Three details in that snippet matter more than they look. First, the loop has a hard step budget (max_steps). Without it, a confused agent will happily call tools forever and burn through your API bill. Second, the model never executes anything - it only names an action and arguments; your code decides whether and how to run it. Third, every observation is appended to history, so the model’s context grows with what it has learned.
ReAct text vs. native tool calling
There are two ways to get that decision out of the model. The original ReAct approach asks the model to write its thoughts and actions as text, which you then parse. Modern models offer native tool calling (also called function calling): you hand the model a set of typed function schemas, and it returns a structured JSON object naming the function and its arguments - no fragile text parsing required.
Native tool calling: the model returns structured intent, not prose.
tools = [{
"name": "get_quote",
"description": "Get the latest market quote for a ticker symbol.",
"parameters": {
"type": "object",
"properties": {"ticker": {"type": "string"}},
"required": ["ticker"]
}
}]
response = client.chat(messages=history, tools=tools)
call = response.tool_calls[0] # {name: "get_quote", args: {ticker: "X"}}
The trade-off is real and worth understanding. Text-based ReAct is transparent and model-agnostic - you can read the reasoning, and it works on any model, even ones without a tool-calling API. Native tool calling is more efficient and reliable - structured output, fewer round trips, and the model can request several tools at once - but the reasoning behind a call is less visible. In practice most production agents use native tool calling and recover transparency through tracing and logging.
When one loop isn’t enough: plan-and-execute
Pure ReAct decides its next step one observation at a time, which is flexible but can wander on long tasks. A common evolution is plan-and-execute: the agent first drafts a full plan and then executes the steps, replanning only when reality diverges. Separating slow, expensive planning from fast tactical execution tends to handle complex multi-step workflows more efficiently than reacting from scratch every turn. You can think of ReAct as improvising and plan-and-execute as working from a checklist you’re allowed to revise.
Where loops go wrong
The loop is powerful precisely because it is open-ended, and that is also where the failure modes live. Agents get stuck repeating the same failing action, loop until the budget runs out, or call an expensive tool dozens of times. The defenses are unglamorous but essential: a hard step limit, detection of repeated identical actions, a cost ceiling, and clear tool error messages so the model can recover rather than retry blindly. Treat every observation as something the model might misread, and design your tools to fail loudly and informatively.
The takeaway
Every agent - from a customer-support bot to GitHub Copilot’s agent mode - is, at its core, this loop: “reason, act, observe, repeat, until done." "Frameworks add memory, multiple agents, and orchestration on top, but they are all variations on the same heartbeat. Once you can write the loop yourself, you understand agents from the inside, and every framework becomes a convenience rather than a mystery.
Next in the series, the Act step deserves its own post. We’ll go deep on tools, function calling, and the Model Context Protocol - how agents actually reach into the world without becoming a security liability.
Top comments (0)