The ReAct Pattern: Synergizing Reasoning and Acting
How do we teach a language model to use tools systematically?
In 2022, researchers at Princeton and Google Brain published the landmark paper "ReAct: Synergizing Reasoning and Acting in Language Models" (Yao et al.).
The ReAct pattern combines Reasoning (Chain-of-Thought) with Action (Tool Calling) in an interleaved, step-by-step execution trace.
1. Why Pure Reasoning or Pure Acting Fails
flowchart TD
subgraph PureReasoning["1. Pure Reasoning (Chain-of-Thought Only)"]
R1["Question: 'How many rows are in table X?'"]
R2["Model hallucinates row count from pre-trained weights ❌"]
end
subgraph PureActing["2. Pure Acting (Tool Calling Without Thoughts)"]
A1["Calls tools blindly without intermediate planning"]
A2["Gets stuck in infinite loops or misses edge cases ❌"]
end
subgraph ReActParadigm["3. ReAct (Reasoning + Acting)"]
RA1["Thought: 'I need to check if table X exists first.'"]
RA2["Action: list_tables()"]
RA3["Observation: ['table_x', 'table_y']"]
RA4["Thought: 'Table exists. Now I will run COUNT(*).'"]
RA5["Action: execute_sql('SELECT COUNT(*) FROM table_x')"]
RA6["Observation: 4,521,900"]
RA7["Thought: 'I have the answer.'"]
RA8["Final Answer: 'Table X contains 4,521,900 rows.' ✅"]
end
style PureReasoning fill:#7f1d1d,color:#fff
style PureActing fill:#7f1d1d,color:#fff
style ReActParadigm fill:#064e3b,color:#fff
The ReAct Formula:
$$\text{Trace} = \left( \text{Thought}_t \to \text{Action}_t \to \text{Observation}t \right){t=1}^T$$
- Thought: The model plans what to do next, explains its reasoning, and evaluates prior results.
- Action: The model emits a formal command to call an external tool.
- Observation: The runtime environment executes the tool and injects the output back into the prompt.
2. Hands-on: Building a Pure Python ReAct Loop
Understanding how a ReAct loop runs without any third-party framework:
import re
from typing import Dict, Any
# Mock Database & Environment Tools
def tool_list_tables() -> str:
return "Available Tables: ['fct_orders', 'dim_customers', 'dim_products']"
def tool_get_table_schema(table_name: str) -> str:
schemas = {
"fct_orders": "Schema: order_id INT, customer_id INT, amount_usd NUMERIC, status VARCHAR",
"dim_customers": "Schema: customer_id INT, name VARCHAR, tier VARCHAR"
}
return schemas.get(table_name.strip(), f"Error: Table '{table_name}' does not exist.")
def tool_run_sql(query: str) -> str:
if "count" in query.lower() and "fct_orders" in query.lower():
return "Query Result: 1,250,000"
return "Query Result: [(101, 'Acme Corp', 'Enterprise')]"
AVAILABLE_TOOLS = {
"list_tables": lambda arg: tool_list_tables(),
"get_schema": lambda arg: tool_get_table_schema(arg),
"run_sql": lambda arg: tool_run_sql(arg)
}
# Simulated LLM Agent Brain
class SimulatedReActLLM:
def __init__(self):
self.step = 0
def generate(self, prompt: str) -> str:
self.step += 1
if self.step == 1:
return (
"Thought: To find the order count, I must first verify the orders table name.
"
"Action: list_tables[]"
)
elif self.step == 2:
return (
"Thought: The table name is 'fct_orders'. Now I need to check its schema to ensure it has valid rows.
"
"Action: get_schema[fct_orders]"
)
elif self.step == 3:
return (
"Thought: The schema has an 'order_id' column. Now I will execute a COUNT(*) query.
"
"Action: run_sql[SELECT COUNT(*) FROM fct_orders]"
)
else:
return (
"Thought: I have the count result.
"
"Final Answer: The total number of orders in the fct_orders table is 1,250,000."
)
def run_react_agent(user_question: str, max_turns: int = 5):
llm = SimulatedReActLLM()
prompt_history = f"Question: {user_question}
"
print(f"=== Starting ReAct Agent: '{user_question}' ===
")
for turn in range(max_turns):
completion = llm.generate(prompt_history)
print(f"--- Turn {turn + 1} ---")
print(completion)
prompt_history += completion + "
"
# Check if the agent has reached a final answer
if "Final Answer:" in completion:
final_answer = completion.split("Final Answer:")[1].strip()
print(f"
🎯 [Agent Completed]: {final_answer}")
return final_answer
# Parse Action
match = re.search(r"Action:\s*(\w+)\[(.*?)\]", completion)
if match:
tool_name = match.group(1)
tool_arg = match.group(2)
if tool_name in AVAILABLE_TOOLS:
observation = AVAILABLE_TOOLS[tool_name](tool_arg)
else:
observation = f"Error: Tool '{tool_name}' not recognized."
print(f"Observation: {observation}
")
prompt_history += f"Observation: {observation}
"
if __name__ == "__main__":
run_react_agent("How many orders do we have recorded in our warehouse?")
Key Takeaways
- The ReAct pattern alternates between Reasoning (Thought) and Acting (Action).
- Interleaving prevents hallucinations: The model observes real facts from tools before taking its next step.
- Dynamic recovery: If an action fails with a SQL error, the next Thought analyzes the error message and formulates an alternative query.