Manual ReAct Trace: Walking Through One Query Step by Step
~20 min read
Running the ReAct loop manually — calling the agent, reading its Thought/Action output, executing the tool yourself, and feeding back the Observation — builds full visibility into how the loop actually behaves before automating any of it.
Before automating anything, it's worth manually driving the ReAct loop one step at a time. This is exactly what this course's walkthrough does first: by running the logic cell-by-cell (or, outside a notebook, one Python statement at a time), you get full visibility and control over the agent's thinking process, which makes it far easier to debug and validate its behavior at each individual step before trusting an automated controller to drive it unattended.
The manual process for a question like 'What is India's population divided by 2?' looks like this: you instantiate the Agent with the ReAct system prompt, then call it with the user's question. The model responds with a Thought (recognizing it needs to look up India's population) followed by PAUSE — and then stops, waiting for you.
At this point, you call the agent again, but with a blank message (an empty string) to let it continue reasoning from where it left off. It responds with an Action line — 'Action: lookup_population: India' — followed by another PAUSE. Now it's genuinely waiting on you: you, the human controller in this manual version, actually run the lookup_population('India') tool function yourself, get back a population number, and call the agent again — this time passing in 'Observation: 1417492000' as the message, exactly matching the format the system prompt told the model to expect.
The agent then continues from that observation, does the division, and outputs the final 'Answer: 708746000' line, at which point the loop is done. Running through this by hand — reading the printed output at every single step — is what makes the mechanics of the ReAct loop concrete before you hand control over to an automated agent_loop() function, which is exactly what happens next.
💻 Code example
def math(expr: str) -> float:
return eval(expr) # fine for a controlled demo; never eval untrusted input in prod
def lookup_population(country: str) -> int:
return {"India": 1_417_492_000, "USA": 341_814_000}.get(country, 0)
# --- manual, step-by-step ReAct session ---
agent = Agent(system=REACT_SYSTEM_PROMPT)
print(agent("What is India's population divided by 2?"))
# -> "Thought: I need to look up India's population first.\nPAUSE"
print(agent("")) # blank input lets the agent continue to the next stage
# -> "Action: lookup_population: India\nPAUSE"
# The human (controller) actually runs the tool here:
result = lookup_population("India")
print(agent(f"Observation: {result}"))
# -> "Thought: Now I can compute half of that.\nAction: math: 1417492000 / 2\nPAUSE"
result = math("1417492000 / 2")
print(agent(f"Observation: {result}"))
# -> "Answer: 708746000.0"
💬 Deep Dive with AI
Key points
- •Running the loop manually first gives full visibility into the agent's reasoning before trusting an automated controller
- •A blank ('') message tells the agent to continue reasoning from wherever it paused, without adding a new user question
- •The human plays the role of the controller in this manual version — actually calling the tool function and formatting the result as 'Observation: ...'
- •The Observation message must match the exact format the system prompt led the model to expect
- •This step-by-step trace is what the automated agent_loop() (next subtopic) replaces with code — same mechanics, no human in the loop