ReAct Pattern: Interleaving Reasoning and Action for LLM Agents › Code Implementation
Language AI HandbookAll chapters ReAct Pattern: Interleaving Reasoning and Action for LLM Agents › Code ImplementationOne section · Sections in this chapter6 / 14 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement a minimal but complete ReAct system. We'll create a simple question-answering agent with access to a search tool and a calculator. This shows the core loop mechanics. The goal here is not to build a production agent, but to make the mechanics of the loop concrete and observable.
The implementation has five parts: the tools themselves, the agent class that manages the loop, a simulated LLM for demonstration, a function to run the agent, and the visualization of its trajectory.
Setting Up the Tools
First, we define the tools available to the agent. In production, these would be real API calls; here they use a hardcoded knowledge base for reproducibility:
import ast
import operator
from typing import Callable, Dict
def search(query: str) -> str:
"""Simulated search tool with hardcoded knowledge for demo purposes."""
knowledge_base = {
"capital of france": "Paris",
"population of paris": "2.1 million (2020 estimate)",
"area of paris": "105 square kilometers",
"eiffel tower height": "330 meters",
"eiffel tower year built": "1889",
"author old man sea": "Ernest Hemingway wrote 'The Old Man and the Sea' in 1952",
"ernest hemingway birthplace": "Oak Park, Illinois",
"population oak park illinois": "54,583",
"author the old man and the sea": "Ernest Hemingway wrote The Old Man and the Sea in 1952",
}
query_lower = query.lower()
for key, value in knowledge_base.items():
if all(word in query_lower for word in key.split()):
return value
return f"No results found for: {query}"
# Safe arithmetic evaluator using ast module
_SAFE_OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.USub: operator.neg,
}
def _safe_eval(node):
"""Recursively evaluate a parsed arithmetic AST node."""
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
elif isinstance(node, ast.BinOp) and type(node.op) in _SAFE_OPS:
left = _safe_eval(node.left)
right = _safe_eval(node.right)
return _SAFE_OPS[type(node.op)](left, right)
elif isinstance(node, ast.UnaryOp) and type(node.op) in _SAFE_OPS:
return _SAFE_OPS[type(node.op)](_safe_eval(node.operand))
else:
raise ValueError(f"Unsupported expression type: {type(node)}")
def calculate(expression: str) -> str:
"""Safe arithmetic calculator that parses and evaluates mathematical expressions."""
try:
tree = ast.parse(expression.strip(), mode="eval")
result = _safe_eval(tree.body)
return str(result)
except (ValueError, ZeroDivisionError, SyntaxError) as e:
return f"Error: {str(e)}"
def finish(answer: str) -> str:
"""Terminal action that returns the final answer."""
return f"FINAL_ANSWER: {answer}"
# Tool registry
TOOLS: Dict[str, Callable] = {
"search": search,
"calculate": calculate,
"finish": finish,
}The tool registry is a dictionary mapping tool names to callable functions. This design makes it easy to add or remove tools without changing the agent's core logic. In production, you might also store metadata about each tool (description, parameter schema, usage examples) alongside the callable, which is essentially what the OpenAI function-calling API does.
The calculator uses Python's ast module to parse the expression into a syntax tree and then evaluate only the numeric operations explicitly listed in _SAFE_OPS. This is safer than passing arbitrary strings to eval because it only accepts a restricted grammar of arithmetic operations.
The ReAct Agent Class
Now we implement the agent class that manages the thought-action-observation loop:
import re
from typing import Any, Callable, Dict, List, Tuple
class ReActAgent:
def __init__(self, tools: Dict[str, Callable], max_iterations: int = 10):
self.tools = tools
self.max_iterations = max_iterations
self.trajectory: List[Dict[str, str]] = []
def parse_action(self, text: str) -> Tuple[str, str]:
"""Parse 'Action: tool[params]' format from model output."""
action_match = re.search(r"Action:\s*(\w+)\[(.*?)\]", text, re.DOTALL)
if not action_match:
return None, None
tool_name = action_match.group(1)
params = action_match.group(2)
return tool_name, params
def format_prompt(self, question: str) -> str:
"""Format the full prompt with examples and current trajectory."""
few_shot_examples = """You are a helpful assistant that answers questions by thinking step-by-step and using tools.
Available tools: search[query], calculate[expression], finish[answer]
Example 1:
Question: What is 15 multiplied by 7?
Thought: I need to calculate 15 * 7.
Action: calculate[15 * 7]
Observation: 105
Thought: I have the answer.
Action: finish[105]
Example 2:
Question: What is the capital of France?
Thought: I need to search for the capital of France.
Action: search[capital of France]
Observation: Paris
Thought: I have found the capital.
Action: finish[Paris]
Now answer the following:"""
# Build trajectory context
context = ""
for step in self.trajectory:
context += f"\nThought: {step['thought']}"
context += f"\nAction: {step['action']}"
context += f"\nObservation: {step['observation']}"
prompt = f"{few_shot_examples}\nQuestion: {question}{context}\nThought:"
return prompt
def step(self, question: str, llm_response: str) -> Tuple[str, bool]:
"""Execute one step of the ReAct loop.
Returns: (observation, is_finished)
"""
# Parse thought (everything before Action)
thought_match = re.match(r"(.*?)Action:", llm_response, re.DOTALL)
if thought_match:
thought = thought_match.group(1).strip()
if thought.startswith("Thought:"):
thought = thought[8:].strip()
else:
thought = llm_response.strip()
# Parse action
tool_name, params = self.parse_action(llm_response)
if not tool_name:
return (
"Error: Could not parse action. Please use format 'Action: tool[params]'",
False,
)
if tool_name == "finish":
return self.tools[tool_name](params), True
if tool_name not in self.tools:
return f"Error: Unknown tool '{tool_name}'", False
# Execute tool
observation = self.tools[tool_name](params)
# Record in trajectory
self.trajectory.append(
{
"thought": thought,
"action": f"{tool_name}[{params}]",
"observation": observation,
}
)
return observation, False
def run(
self, question: str, llm_simulator: Callable[[str], str]
) -> Dict[str, Any]:
"""Run the full ReAct loop.
Args:
question: The question to answer
llm_simulator: Function that takes prompt and returns LLM response
"""
self.trajectory = []
for i in range(self.max_iterations):
prompt = self.format_prompt(question)
response = llm_simulator(prompt)
# Add the generated thought/action to prompt for parsing
full_response = "Thought: " + response
observation, is_finished = self.step(question, full_response)
if is_finished:
return {
"success": True,
"answer": observation.replace("FINAL_ANSWER: ", ""),
"trajectory": self.trajectory,
"steps": i + 1,
}
return {
"success": False,
"answer": None,
"trajectory": self.trajectory,
"steps": self.max_iterations,
"reason": "Max iterations reached",
}The parse_action method uses a regular expression to extract the tool name and parameters from the model's output. The regex (\w+)\[(.*?)\] matches patterns like search[query text] or calculate[15 * 7]. This is deliberately simple. Production systems often need more reliable parsers that handle multi-line parameters, special characters, or JSON-formatted arguments.
The format_prompt method constructs the full context window contents at each step. Notice that it always starts from the full few-shot examples and then appends the complete trajectory. This means the model sees the same examples every time, which is important for maintaining format consistency. The trajectory context appended at the end grows with each iteration, which is the source of the linear token growth discussed in the limitations section.
The run method implements the outer loop with a maximum iteration guard. The guard is needed: without it, a confused or stuck agent would consume API credits indefinitely. Hitting the maximum iteration limit without a finish action is a detectable failure mode that the calling system can handle, for example by returning a "could not answer" response or escalating to a human.
Simulating LLM Responses
For demonstration purposes, we simulate the LLM responses to show exactly what a correct trajectory looks like:
def simulate_llm(
prompt: str, step_responses: List[str], step_counter: List[int]
) -> str:
"""Simulate LLM responses for demo purposes."""
current_step = step_counter[0]
step_counter[0] += 1
if current_step < len(step_responses):
return step_responses[current_step]
return "finish[Unable to complete]"
# Define the response sequence for our multi-hop question
responses = [
# Step 1: Search for author
"I need to find who wrote 'The Old Man and the Sea'.\nAction: search[author The Old Man and the Sea]",
# Step 2: Search for birthplace (after seeing observation)
"The author is Ernest Hemingway. Now I need to find where he was born.\nAction: search[Ernest Hemingway birthplace]",
# Step 3: Search for population
"He was born in Oak Park, Illinois. Now I need the population of Oak Park.\nAction: search[population Oak Park Illinois]",
# Step 4: Finish
"I have found that Oak Park, Illinois has a population of 54,583. I can now provide the final answer.\nAction: finish[54,583]",
]Running the Agent
Now let's run the agent and observe its trajectory:
# Initialize agent and run
agent = ReActAgent(TOOLS, max_iterations=10)
step_counter = [0] # Use list to allow mutation in closure
result = agent.run(
"What is the population of the city where the author of 'The Old Man and the Sea' was born?",
lambda p: simulate_llm(p, responses, step_counter),
)Success: True Answer: 54,583 Steps taken: 4 Full Trajectory: Step 1: Thought: I need to find who wrote 'The Old Man and the Sea'. Action: search[author The Old Man and the Sea] Observation: Ernest Hemingway wrote 'The Old Man and the Sea' in 1952 Step 2: Thought: The author is Ernest Hemingway. Now I need to find where he was born. Action: search[Ernest Hemingway birthplace] Observation: Oak Park, Illinois Step 3: Thought: He was born in Oak Park, Illinois. Now I need the population of Oak Park. Action: search[population Oak Park Illinois] Observation: 54,583
The agent successfully completes the multi-hop reasoning task in four steps. This shows how the ReAct pattern chains together disparate pieces of information into a coherent solution. Each thought explicitly references the previous observation. This creates an auditable reasoning chain that traces the path from question to answer.
Visualizing the Trajectory
The trajectory diagram below makes the iterative structure concrete. Each column represents a phase (Thought, Action, Observation), and each row represents one iteration:

The diagram makes one important feature visible that is easy to overlook in the code: the cumulative context arrows on the left side. Each new thought has access to the immediate previous observation and to the entire trajectory of prior thoughts and actions plus their observations. This accumulation is what allows the agent to refer back to earlier findings even several steps later.