Reason → Act → Observe → Repeat — the thinking/action cycle that turns a one-shot responder into an autonomous problem-solver. Six concepts, from what an agent even is to knowing when it should stop.
The 6 concepts of ReAct
Tap any concept to jump to its 5-card cluster.
A chatbot answers; an agent acts
A chatbot is one message in, one response out. It uses only what it already knows, finishes in a single call, and takes no actions in the world. Great for Q&A, summaries, and generation.
An agent takes a goal and runs multiple reasoning/action cycles: it calls real tools (search, code, APIs), observes what comes back, adapts its plan, and continues for as many turns as the task needs. That looping autonomy is exactly what the ReAct pattern gives the model.
Script-reading operator vs. project manager
BEFORE: Picture a help desk where the operator can only answer from a fixed script. You ask "is my order shipped?" and they read back one field. One question in, one answer out, every time.
PAIN: If your order is late, needs rerouting through a different carrier, and the new tracking number needs emailing to you, the operator says "I can't help with that, call three departments." Each has its own single-answer script.
MAPPING: A chatbot is that operator. An agent is a project manager: it takes your request, decides the steps, calls the right departments in sequence, waits for each result, and returns with a complete solution.
What separates them
- TurnsChatbot: exactly one. Agent: as many reasoning/action cycles as the goal requires.
- ToolsChatbot has none. An agent calls real tools — search, code, database, APIs.
- AdaptationAn agent reads each tool result and adjusts its plan; a chatbot can't observe or react.
- FitChatbot for Q&A and generation; agent for research, automation, multi-step workflows.
Chatbot or agent?
IF task is one-shot Q&A, summary, or generation: → a plain CHATBOT call is enough IF task needs live data, real actions, or many steps: → build an AGENT (ReAct loop) IF unsure: → start simple; add the loop only when one call can't finish # agents cost more tokens + latency — # don't loop when you don't have to
Misconceptions
A chatbot responds once with no tools; an agent loops through reason/act/observe cycles using real tools. ReAct is the pattern that makes that loop work.
Match the shape to the task: one-shot → chatbot, multi-step with live data → agent.
Reasoning + Acting, fused
ReAct (Reasoning + Acting) is a pattern where the model alternates between explicit reasoning ("Thought: ...") and tool actions ("Act: ..."), reads the result, and repeats until the task is done.
The key insight from the original research: a model that reasons out loud before acting makes far fewer errors than one that answers blindly or calls tools silently. The thought step isn't decoration — it's the mechanism that improves quality.
Two broken ways to work a case
BEFORE: One detective solves cases purely in their head, never checking a record — that's pure reasoning with no tools. Another blindly pulls files with no theory of the case — that's silent tool use.
PAIN: The head-only detective invents facts they can't verify. The file-puller grabs the wrong records because nothing guided the choice. Both dead-end constantly.
MAPPING: ReAct is the detective who thinks, then acts, then thinks again: "I suspect X, so I'll pull this record" → reads it → "that rules out X, now check Y." Reasoning picks the action; the action informs the next reason.
Why fusing both wins
- Pure reasoning failsAll thought, no tools — the model hallucinates facts it can't check.
- Silent tool use failsTools with no reasoning — the model picks the wrong action with no plan.
- Thought narrows the actionThe "Thought:" step plans, spots gaps, and decides which tool fills them.
- Observation feeds the next thoughtReading the result updates the plan, then the loop continues — informed, not blind.
Pseudocode
# one ReAct step, in the model's voice Thought: what do I know? what's missing? which tool fills the gap? Act: call_tool(name, arguments) Observe: read the tool's result Thought: did that answer it, or do I need another action? # reasoning chooses the action; # the observation shapes the next thought.
Smaller local models benefit most from an explicit Thought step — it keeps their tool choices grounded.
Misconceptions
pip install named "react."ReAct interleaves explicit reasoning with tool actions. Thinking before acting is what makes the difference, not any framework.
A few hundred tokens of thought buys fewer dead-end calls, fewer hallucinations, and traces you can debug.
Four phases that repeat
The loop has four phases. Reason: the model thinks out loud about what it needs next. Act: it calls a tool. Observe: it reads the tool result. Repeat until it has enough to give a final answer.
Understanding each phase is what lets you debug and extend an agent. The single most important detail: the loop continues or ends based on a structured signal from the API (finish_reason), not on guessing from the model's words.
A cook tasting between steps
BEFORE: Imagine a cook who dumps every ingredient in at once and serves without ever tasting — no checking, no adjusting along the way.
PAIN: If the sauce is too salty or the pasta underdone, they only find out when the plate comes back. There's no chance to course-correct mid-cook.
MAPPING: The ReAct loop is a cook who tastes between steps: decide what's needed (Reason), add it (Act), taste (Observe), then decide again (Repeat) — stopping only when the dish is right.
One trip around the loop
- ReasonThe model writes a Thought: what it knows, what's missing, which action helps.
- ActIt calls one tool with specific arguments — the moment it touches the real world.
- ObserveYour code appends the tool result as a
toolmessage; the model reads it next turn. - Repeat or stopIf
finish_reason == "tool_calls"keep going; if"stop", return the final answer.
Pseudocode
history = [ user_goal ] LOOP: reply = model.call(history, tools) IF reply.finish_reason == "stop": RETURN reply.text # done history.append(reply) # the Act FOR each tool_call IN reply: result = run_tool(tool_call) history.append(result) # the Observe # loop back → next Reason
Misconceptions
finish_reason: tool_calls means continue, stop means finished. Text phrasing changes every run.Reason → Act → Observe → Repeat, driven by finish_reason. Your code runs tools and appends results; the model reads them.
Let the structured signal control the loop — never parse natural language for "task complete."
A while-loop over the chat API
ReAct isn't a framework — you implement it directly with the chat API you already know. The core is the agentic loop: a while loop that keeps calling the model until finish_reason == "stop".
Each iteration appends both the model's tool calls and your tool results to a growing history list. That list is the agent's memory: the stateless API becomes stateful because your code keeps the conversation.
A dispatcher with a running logbook
BEFORE: Imagine a dispatcher whose radio wipes its memory after every single call — each transmission arrives with zero context about what came before.
PAIN: If the dispatcher keeps no record, every driver has to re-explain the whole job each time. Nothing accumulates; coordination collapses.
MAPPING: The agent loop is the dispatcher's logbook. Every request and every reply gets written down, and the full log is read aloud before each new call — so the "forgetful radio" (the model) always has the whole story in front of it.
Building the loop
- Define toolsGive each a name, a clear description (drives tool choice), and an input schema.
- Call the modelSend the history plus the tool list; the model reasons and maybe requests tools.
- Check finish_reasonIf
stop, return the answer. Otherwise append the assistant message and continue. - Execute & appendRun each tool, append results as tool-role messages, loop again.
Pseudocode
history = [ user_question ] turns = 0 WHILE turns < max_turns: # safety net turns += 1 reply = model.call(history, tools) IF reply.finish_reason == "stop": RETURN reply.text history.append(reply) FOR call IN reply.tool_calls: TRY: out = run_tool(call) CATCH err: out = {error: err} # never crash history.append(tool_result(out))
Misconceptions
The agentic loop is a while-loop that appends every tool call and result to a growing history. That history is the memory that makes reasoning work.
Write sharp tool descriptions and always return errors as structured results, never exceptions.
Notes the agent leaves itself
Thought traces are the "Thought: ..." paragraphs the model writes before each tool call. They look optional, but they're what keeps multi-step reasoning coherent.
Because each thought becomes part of the conversation history, later steps can reference earlier ones. The model is leaving notes for itself — anchoring every action to the original goal instead of acting in a vacuum.
The researcher's notebook
BEFORE: Picture a researcher who opens a paper, reads a paragraph, closes it, opens the next, reads, closes — taking no notes and drawing no connections between sources.
PAIN: By the third paper they've forgotten the first. They can't synthesize, because nothing they read got linked to anything else. The report is fragments.
MAPPING: Thought traces are the notebook: "After paper 1, I understand X and still need Y." That note travels into every later step, so the final answer can synthesize across everything read.
What thought traces do
- Anchor to intentEach thought restates the goal, so the model doesn't drift across many tool calls.
- Carry context forwardBecause thoughts live in history, step 3 can build on what step 1 concluded.
- Enable debuggingWhen an answer is wrong, the trace shows exactly where — bad tool, bad query, or misread result.
- Provide an audit trailEvery action has a recorded "why" right before it — essential in production.
Pseudocode
# the system prompt asks for this shape: Turn 1: Thought: goal = compare LLMs; I need current benchmark scores first Act: web_search("LLM leaderboard 2025") Turn 2: Observe: got scores, missing pricing Thought: goal was a COMPARISON, so I still need pricing → search it Act: web_search("LLM pricing 2025") # each thought re-anchors to the goal.
Misconceptions
Thought traces are the agent's working memory and audit trail. They keep reasoning anchored to the goal across many turns.
Instruct the model to write a Thought before each act and after each observation — then keep those traces in your logs.
Let the model decide, keep a safety net
The correct primary stop signal is finish_reason == "stop": the model decides it has enough and stops requesting tools. That's the natural terminator of a ReAct loop.
An iteration cap (max_turns) is a safety net, not the control mechanism. Set it high enough that real tasks finish (often 20–50), and handle the cap-exceeded case explicitly instead of silently returning half an answer.
The circuit breaker in your house
BEFORE: Imagine wiring your kitchen so appliances only turn off when the breaker trips — using the emergency cutoff as the everyday on/off switch.
PAIN: You either trip it too early (dinner half-cooked) or the wiring runs hot before it ever trips. The safety device was never meant to run the kitchen.
MAPPING: finish_reason is the normal light switch — the agent stops when the job's done. max_turns is the breaker: it exists for when something goes wrong, not to run the loop day to day.
Stopping, done right
- Primary: finish_reasonWhen it's
stop, the model chose to finish — return its text. - Safety net: max_turnsA high cap that only fires when something is wrong, never as routine control.
- Handle the cap loudlyIf you hit it, log it and notify — don't silently return partial results.
- Investigate repeatsHitting the cap often signals a design flaw: no "enough info" signal or too-open goals.
How should I stop the loop?
IF finish_reason == "stop": → done — return the final answer IF finish_reason == "tool_calls": → run tools, then loop again IF turns >= max_turns: → SAFETY trip — log + surface it, do NOT return silent partial data # never: scan text for "I'm done" # never: use a small cap as main control
Misconceptions
finish_reason instead.Stop on finish_reason == "stop"; treat max_turns as a breaker, not a switch. The model decides completion; the cap only catches runaways.
When the cap trips, surface it — regular trips mean your agent design needs fixing.
Test the core insight
One question per concept. Tap to reveal the answer.
finish_reason.max_turns for?finish_reason == "stop"; max_turns is a safety net. The model decides it's done and stops calling tools. The iteration cap only catches runaways — set it high, handle it loudly, and never use a small cap or text-parsing as the main control.Open the full module on desktop
The desktop version has you implement the full ReAct loop by hand against a local model — tool definitions, the agentic while loop, thought-trace prompting, and correct stop logic — then compare it to the Agent SDK, in Python and TypeScript.
Module 8 of 12 · Open Source Track · Agent Architectures