Skip to main content
Articles/langchain/langgraph-react-agent-pattern
Series: Stateful Graph Agentsβ€’Part 3 of 4
Scenario Article

The ReAct Agent Pattern in LangGraph: Think, Act, Observe, Repeat

The ReAct Pattern: Think, Act, Observe, and Why It Works

Abstract Algorithmsβ€’Mar 28, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

ReAct = Think + Act + Observe, looped as a LangGraph graph β€” prebuilt or custom.

🎯 System Scenario & Requirements

System Goal

ReAct = Think + Act + Observe, looped as a LangGraph graph β€” prebuilt or custom.

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • TLDR: ReAct = Think + Act + Observe, looped as a LangGraph graph β€” prebuilt or custom.
  • <!-- ReAct (Reasoning + Acting) was introduced in a 2022 paper by Yao et al.
  • The core insight is deceptively simple: instead of generating one final answer, make the LLM generate a reasoning trace interleaved with tool calls , each followed by an observation of the tool's output.

Constraints & Assumptions

  • Your agent is supposed to write a function, run the tests, fix the failures, and repeat until green.
  • With a single LLM call, it writes the function and stops.
  • Cost ReAct loops are significantly more expensive than single-shot calls.
  • A 5-turn loop on GPT-4o can consume 10–20Γ— the tokens of a direct answer, because the growing conversation history is re-sent to the model on every iteration.

⚑ Execution Stages & Decision Trace

1

Stage 1 β€” Authoring Notes

Action / Decision:

Core Thesis: The ReAct pattern lifts agents past the single-shot ceiling by wiring a Thinkβ†’Actβ†’Observe loop as a LangGraph StateGraph; use create react agent for speed or build the loop manually when production needs cu…

Component Behavior:

Complexity: intermediate Omitted: Mathematical Model (not applicable to orchestration patterns); πŸ—οΈ Advanced Concepts (content absorbed into Deep Dive subsections) --

Observable Output: Complexity: intermediate Omitted: Mathematical Model (not applicable to orchestration patterns); πŸ—οΈ Advanced Concepts (content absorbed into Deep Dive subsections) --
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

Scenario context

ReAct = Think + Act + Observe, looped as a LangGraph graph β€” prebuilt or custom.

<!--

ReAct (Reasoning + Acting) was introduced in a 2022 paper by Yao et al. The core insight is deceptively simple: instead of generating one final answer, make the LLM generate a reasoning trace interleaved with tool calls, each followed by an observation of the tool's output.

The loop looks like this:

  1. Think β€” the L…

This walkthrough focuses on The ReAct Pattern: Think, Act, Observe, and Why It Works.

Before making a design choice, clarify:

  • ReAct (Reasoning + Acting) was introduced in a 2022 paper by Yao et al.
  • The core insight is deceptively simple: instead of generating one final answer, make the LLM generate a reasoning trace interleaved with tool calls , each followed by an observation of the tool's output.
  • Think β€” the LLM reasons about the current state and decides what action to take next.

Concepts in play: llm-agents-and-langgraph, graph-traversal-algorithms, llm-prompt-engineering, event-driven-architecture

Stage 1 β€” Authoring Notes

  • Core Thesis: The ReAct pattern lifts agents past the single-shot ceiling by wiring a Thinkβ†’Actβ†’Observe loop as a LangGraph StateGraph; use create react agent for speed or build the loop manually when production needs cu…
  • Complexity: intermediate Omitted: Mathematical Model (not applicable to orchestration patterns); πŸ—οΈ Advanced Concepts (content absorbed into Deep Dive subsections) --

Observable outcome: Complexity: intermediate Omitted: Mathematical Model (not applicable to orchestration patterns); πŸ—οΈ Advanced Concepts (content absorbed into Deep Dive subsections) --

Stage 2 β€” Deep Dive: What Happens Inside Each Loop Iteration

  • The Internals MessagesState is the agent's working memory.
  • Every message appended to the messages list persists across loop iterations.
  • After turn 3, the LLM's context window contains the original user request, two AIMessage responses (each with their tool calls ), and two ToolMessage observations.
  • This is how the agent "knows" what it tried before β€” there is no separate memory store; the conversation history IS the reasoning trace.
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class BoundedAgentState(TypedDict):
    messages: Annotated[list, add_messages]
    iterations: int

def agent_node_with_limit(state: BoundedAgentState):
    if state.get("iterations", 0) >= 8:
        return {
            "messages": [AIMessage(content="Iteration limit reached. Stopping.")],
            "iterations": state["iterations"],
        }
    messages = [SYSTEM_PROMPT] + state["messages"]
    response = llm_with_tools.invoke(messages)
    return {"messages": [response], "iterations": state.get("iterations", 0) + 1}

Observable outcome: This is how the agent "knows" what it tried before β€” there is no separate memory store; the conversation history IS the reasoning trace.

Stage 3 β€” The ReAct Loop as a LangGraph Graph

  • The entire ReAct pattern maps cleanly onto four LangGraph primitives: an entry point, two nodes, a conditional edge, and an unconditional back-edge.
  • The loop: every AIMessage with tool calls routes right; every AIMessage without tool calls routes down to END.
  • ToolNode always routes back up to the agent.
  • What makes this a loop and not a chain is the back-edge from tools β†’ agent .
graph TD
    A([ START]) --> B[" Agent Node (LLM call with bind_tools)"]
    B --> C{should_continue}
    C -- "tool_calls present in last AIMessage" --> D[" ToolNode (execute tool functions)"]
    D -- "append ToolMessages to state" --> B
    C -- "no tool_calls (final answer)" --> E([ END])

    style A fill:#4CAF50,color:#fff,stroke:none
    style E fill:#F44336,color:#fff,stroke:none
    style B fill:#2196F3,color:#fff,stroke:#1565C0
    style D fill:#FF9800,color:#fff,stroke:#E65100
    style C fill:#9C27B0,color:#fff,stroke:#6A1B9A

Observable outcome: What makes this a loop and not a chain is the back-edge from tools β†’ agent .

Stage 4 β€” Real-World Applications: Where ReAct Agents Are Deployed Today

  • ReAct is not a research curiosity.
  • It is the backbone of a wide class of production deployments: Coding assistants with test harnesses (GitHub Copilot Workspace, Devin-style agents): Write code β†’ run linter β†’ fix β†’ run tests β†’ fix β†’ commit.
  • The loop terminates when all checks pass or an iteration limit is reached.
  • This is exactly the pattern this post's coding assistant example implements.

Observable outcome: This is exactly the pattern this post's coding assistant example implements.

Stage 5 β€” Practical Example: Coding Assistant That Iterates Until Tests Pass

  • This example implements the opening scenario end-to-end: a coding assistant that writes a Python function, runs the test suite, inspects failures, and iterates until all tests are green β€” or gives up after 8 attempts.
  • The coding-assistant scenario was chosen because it is the clearest possible demonstration of the ReAct loop's core value: the agent cannot solve the task in one shot because it needs to observe real test output before…
  • As you read through the execution trace, watch the should continue edge β€” every iteration where the result is "tools" is one full Thinkβ†’Actβ†’Observe cycle, and the loop only exits to END when the LLM decides there is not…
  • What the loop looks like across 3+ turns: The agent used 5 turns, observed real test output, diagnosed a recursion failure, and applied a targeted fix β€” none of which is possible in a single shot.
import os
import subprocess
from typing import Annotated
from typing_extensions import TypedDict

from langchain_core.messages import SystemMessage, AIMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode

# ── State ──────────────────────────────────────────────────────────────────
class CodingAgentState(TypedDict):
    messages: Annotated[list, add_messages]
    iterations: int

# ── Tools ──────────────────────────────────────────────────────────────────
@tool
def write_python_file(filename: str, code: str) -> str:
    """Write Python source code to a file. Returns confirmation."""
    with open(filename, "w") as f:
        f.write(code)
    return f"Written {len(code)} chars to '{filename}'."

@tool
def run_pytest(test_file: str) -> str:
    """Run pytest on the given test file. Returns stdout + stderr."""
    result = subprocess.run(
        ["pytest", test_file, "-v", "--tb=short", "--no-header"],
        capture_output=True, text=True, timeout=30
    )
    output = result.stdout + result.stderr
    return output[:4000]  # Guard against extremely verbose output

tools = [write_python_file, run_pytest]

# ── Nodes ──────────────────────────────────────────────────────────────────
llm = ChatOpenAI(model="gpt-4o", temperature=0)
llm_with_tools = llm.bind_tools(tools)

SYSTEM = SystemMessage(content="""You are an expert Python engineer.
When given a coding task:
1. Write the implementation file using write_python_file.
2. Run the test file using run_pytest.
3. If any tests fail, read the error output carefully, fix the code, and run tests again.
4. Repeat until ALL tests pass, then report "All tests pass." as your final answer.
Do not stop until tests pass or you are truly stuck.""")

MAX_ITERATIONS = 8

def agent_node(state: CodingAgentState):
    if state.get("iterations", 0) >= MAX_ITERATIONS:
        return {
            "messages": [AIMessage(content=f"Stopped after {MAX_ITERATIONS} iterations.")],
            "iterations": state.get("iterations", 0),
        }
    messages = [SYSTEM] + state["messages"]
    response = llm_with_tools.invoke(messages)
    return {
        "messages": [response],
        "iterations": state.get("iterations", 0) + 1,
    }

def should_continue(state: CodingAgentState) -> str:
    last = state["messages"][-1]
    if state.get("iterations", 0) >= MAX_ITERATIONS:
        return END
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return END

tool_node = ToolNode(tools, handle_tool_errors=True)

# ── Graph ──────────────────────────────────────────────────────────────────
workflow = StateGraph(CodingAgentState)
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue)
workflow.add_edge("tools", "agent")

coding_agent = workflow.compile()

# ── Run ────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    result = coding_agent.invoke({
        "messages": [(
            "user",
            "Implement a fibonacci function in fib.py. "
            "Tests are in test_fib.py. Make all tests pass."
        )],
        "iterations": 0,
    })

    for msg in result["messages"]:
        role = type(msg).__name__
        content = getattr(msg, "content", "") or ""
        if content:
            print(f"[{role}] {content[:200]}")
    print(f"\nCompleted in {result['iterations']} iteration(s).")
[Turn 1] Agent thinks: "I'll write the fibonacci implementation first."
         Calls: write_python_file("fib.py", "def fib(n):\n    if n <= 1: return n\n    ...")
[Tool]   "Written 89 chars to 'fib.py'."

[Turn 2] Agent thinks: "Now run the tests."
         Calls: run_pytest("test_fib.py")
[Tool]   "FAILED test_fib.py::test_large_input - RecursionError: maximum recursion depth exceeded"

[Turn 3] Agent thinks: "I need an iterative implementation for large inputs."
         Calls: write_python_file("fib.py", "def fib(n):\n    a, b = 0, 1\n    for _ in range(n): a, b = b, a+b\n    return a")
[Tool]   "Written 74 chars to 'fib.py'."

[Turn 4] Agent thinks: "Retry the tests."
         Calls: run_pytest("test_fib.py")
[Tool]   "PASSED: 5 tests in 0.04s"

[Turn 5] Agent produces final answer: "All tests pass. The fibonacci function uses an iterative
         approach and handles inputs up to fib(1000) without recursion errors."

Observable outcome: What the loop looks like across 3+ turns: The agent used 5 turns, observed real test output, diagnosed a recursion failure, and applied a targeted fix β€” none of which is possible in a single shot.

Stage 6 β€” LangGraph Prebuilt Agents: create_react_agent, create_tool_calling_executor

  • LangGraph ships two prebuilt agent constructors.
  • Understanding both helps you choose the right level of abstraction.
  • create react agent (current standard) create react agent is LLM-agnostic : any model that supports tool calling via .bind tools() works identically.
  • This includes ChatOpenAI , ChatAnthropic , ChatOllama , and ChatGroq .
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic  # swap provider freely

# Works identically with ChatOpenAI, ChatAnthropic, ChatOllama, ChatGroq
agent = create_react_agent(
    model=ChatAnthropic(model="claude-3-5-sonnet-20241022"),
    tools=tools,
    # System prompt injected before messages on every call, not stored in state
    state_modifier=SYSTEM,
)
from IPython.display import Image
Image(agent.get_graph().draw_mermaid_png())

Observable outcome: This includes ChatOpenAI , ChatAnthropic , ChatOllama , and ChatGroq .

End-to-end outcome

After working through the stages, you should be able to explain:

  • The system prompt does not belong in state.
  • A common mistake is appending the system prompt as the first message in MessagesState .

Alternatives considered

LangGraph gives you two paths to a working ReAct agent. The right choice depends on how much you need to customize the loop.

LangGraph gives you two paths to a working ReAct agent.

Path 1: The Prebuilt Shortcut with create_react_agent

create react agent handles everything: it binds your tools to the LLM, creates the agent node, creates the ToolNode , wires the conditional edge, and compiles the graph.

Path 2: Building the Loop Manually as a StateGraph

The key difference from create react agent is the should continue function β€” you own it.

Decision Guide: create_react_agent vs Custom Loop vs Plan-and-Execute

Decision Guide: create_react_agent vs Custom Loop vs Plan-and-Execute

Failure and degraded modes

The Single-Shot Failure: Why One LLM Call Isn't Enough for Complex Tasks

  • Detection: Your agent is supposed to write a function, run the tests, fix the failures, and repeat until green.
  • User or operator impact: Your agent is supposed to write a function, run the tests, fix the failures, and repeat until green.

Performance vs. Cost

  • Detection: ReAct loops are significantly more expensive than single-shot calls.
  • User or operator impact: ReAct loops are significantly more expensive than single-shot calls.

Infinite Loop Failure Mode

  • Detection: The most dangerous failure mode is an agent that loops without making progress.
  • User or operator impact: The most dangerous failure mode is an agent that loops without making progress.

Hallucinated Tool Calls

  • Detection: The LLM may emit a tool call that references a function that does not exist in your tools list, or passes arguments with the wrong type.
  • User or operator impact: The LLM may emit a tool call that references a function that does not exist in your tools list, or passes arguments with the wrong type.

Token Explosion in Long Conversations

  • Detection: Deep loops on complex tasks can exhaust the context window.
  • User or operator impact: Deep loops on complex tasks can exhaust the context window.
Series Navigation β€’ Part 3 of 4

Stateful Graph Agents

View Full Series Overview β†’

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit