7.2 KiB
ReAct pattern (Reasoning + Acting)
ReAct is the most fundamental agentic pattern. The agent alternates between thinking (reasoning about what to do next) and acting (calling a tool), using the observation from each action to inform the next thought. It was introduced in the paper ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al., 2022).
How it works
The ReAct loop has three phases that repeat until the task is complete:
┌──────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ │
│ │ THOUGHT │ "I need to find the population │
│ │ │ of Tokyo. Let me search." │
│ └────┬─────┘ │
│ │ │
│ ┌────▼─────┐ │
│ │ ACTION │ search("Tokyo population 2024") │
│ │ │ │
│ └────┬─────┘ │
│ │ │
│ ┌────▼──────────┐ │
│ │ OBSERVATION │ "Tokyo metro: 13.96M..." │
│ │ │ │
│ └────┬──────────┘ │
│ │ │
│ └──── Loop back to THOUGHT ───────────────┘
- Thought: The LLM reasons about the current state and decides what to do next. This is generated as text (often in a structured format).
- Action: The LLM calls a tool — search, calculate, read a file, run code, etc.
- Observation: The tool returns a result. This result is appended to the conversation, and the LLM generates the next thought.
The loop terminates when the LLM decides it has enough information to answer, or when a maximum iteration count is reached.
Python pseudocode
def react_agent(question: str, tools: list, max_steps: int = 10):
messages = [
{"role": "system", "content": REACT_SYSTEM_PROMPT},
{"role": "user", "content": question},
]
for step in range(max_steps):
# THOUGHT + ACTION: LLM decides what to do
response = llm.chat(messages, tools=tools)
if response.is_final_answer:
return response.content
# ACTION: Execute the tool call
tool_name = response.tool_call.name
tool_args = response.tool_call.arguments
observation = execute_tool(tool_name, tool_args)
# OBSERVATION: Append result and continue
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "tool", "content": observation})
return "Max steps reached without a final answer"
REACT_SYSTEM_PROMPT = """You are a helpful agent. For each step:
1. Think about what you need to do next
2. Call a tool if needed
3. Use the tool's result to inform your next step
4. When you have the final answer, respond directly
Always explain your reasoning before taking an action."""
Real libraries that implement ReAct
| Library | How it implements ReAct | Language |
|---|---|---|
| LangGraph | create_react_agent() — built-in ReAct graph with tool nodes |
Python, TS |
| Pydantic AI | Default agent loop uses ReAct under the hood | Python |
| LangChain | AgentExecutor with ReAct prompt template |
Python, TS |
| Semantic Kernel | AutoFunctionCallingFilter for tool use loops |
Python, C# |
| AutoGen | AssistantAgent with tool registration |
Python |
When to use ReAct vs. plan-and-execute
| Factor | ReAct | Plan-and-execute |
|---|---|---|
| Task structure | Unclear, exploratory | Clear, decomposable |
| Tool dependency | Each step depends on previous result | Steps can be planned upfront |
| Latency | Higher (sequential LLM calls) | Lower (plan once, execute fast) |
| Error recovery | Natural — just reason about the error | Requires explicit re-planning |
| Best for | Q&A, research, debugging | Multi-file code changes, data pipelines |
Use ReAct when you don't know upfront what tools you'll need or in what order. The agent discovers the path as it goes.
Use plan-and-execute when the task has a clear structure that can be decomposed before execution begins.
Common failure modes and fixes
1. Infinite loops
Symptom: The agent repeats the same tool call over and over.
Cause: The observation doesn't provide enough new information to change the agent's reasoning.
Fix: Track previous actions and observations. If the same action is repeated, force the agent to try a different approach or terminate.
if (tool_name, tool_args) in previous_actions:
messages.append({
"role": "system",
"content": "You already tried this action. Try a different approach."
})
2. Premature termination
Symptom: The agent gives a final answer after just one tool call, even when the answer is incomplete.
Cause: The LLM is biased toward giving answers quickly, or the system prompt doesn't emphasize thoroughness.
Fix: Add explicit instructions to verify the answer before finalizing. Include a "confidence check" step.
3. Tool selection errors
Symptom: The agent calls the wrong tool for the task (e.g., using a calculator when it should search).
Cause: Tool descriptions are ambiguous, or there are too many tools (>15) for the LLM to differentiate.
Fix: Write clear, non-overlapping tool descriptions. If you have many tools, use a two-stage approach: first select the relevant tool category, then select the specific tool.
4. Observation overflow
Symptom: Tool returns too much text, filling the context window and causing the agent to lose track.
Cause: Tool returns full web pages, large files, or verbose API responses.
Fix: Truncate or summarize tool observations before appending them. A search result doesn't need the full page — a 500-character snippet is usually enough.
observation = execute_tool(tool_name, tool_args)
if len(observation) > 2000:
observation = llm.summarize(observation, max_tokens=500)
5. Reasoning quality degradation
Symptom: The agent's reasoning gets worse as the conversation gets longer.
Cause: The context window fills up with previous thoughts, actions, and observations. The signal-to-noise ratio drops.
Fix: Periodically summarize the conversation history, keeping only the most relevant information. Or use a sliding window that keeps the last N steps plus a compressed summary of earlier steps.