The ReAct Agent Pattern in LangGraph: Think, Act, Observe, Repeat
Build ReAct agents in LangGraph: prebuilt create_react_agent vs custom StateGraph loop, with a coding assistant example.
Abstract AlgorithmsTLDR: ReAct = Think + Act + Observe, looped as a LangGraph graph β prebuilt or custom.
π The Single-Shot Failure: Why One LLM Call Isn't Enough for Complex Tasks
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. It has no way to see the test output and try again.
This is the single-shot ceiling β the fundamental limit of one-call agents. The LLM generates a response and the conversation ends. No feedback loop, no correction, no ability to observe what actually happened when the code ran.
Single-shot works fine for tasks where the correct answer is entirely within the model's training distribution: "Summarize this paragraph." "Translate this sentence." "What is the capital of France?" These have deterministic, self-contained answers.
The moment a task requires external feedback β run this code and tell me if it passed, search the web for current prices, read the file that was just created β single-shot collapses. The LLM cannot observe the world outside its context window. It can only generate text based on what it was given at call time.
| Task Type | Single-Shot | Looping Agent |
| Summarize a document | β Works | Overkill |
| Answer from training data | β Works | Overkill |
| Write + run + fix code | β Fails | β Required |
| Multi-step research with live data | β Fails | β Required |
| Iterative data transformation | β Fails | β Required |
The solution is an agent that can act, observe the result, and decide what to do next β repeating as many times as the task demands. That is exactly what the ReAct pattern provides.
π The ReAct Pattern: Think, Act, Observe, and Why It Works
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:
- Think β the LLM reasons about the current state and decides what action to take next.
- Act β it calls a tool (a function, an API, a shell command).
- Observe β the tool's return value is fed back into the LLM's context as a
ToolMessage. - Repeat β the LLM decides whether to call another tool or produce a final answer.
The paper demonstrated that ReAct agents significantly outperform two alternatives:
- Pure reasoning (chain-of-thought, no tools) β the LLM reasons but cannot verify facts or act on the world.
- Pure acting (tools without reasoning) β the agent calls tools mechanically, without the planning quality that reasoning provides.
The combination is what matters. Reasoning improves tool selection. Tool results ground the reasoning and prevent hallucination. Each iteration builds on the previous observations, so the agent gets progressively closer to the goal.
Turn 1 β Think: "I need to write a fibonacci function first."
Turn 1 β Act: write_file("fib.py", "def fib(n): ...")
Turn 1 β Observe: "File written successfully."
Turn 2 β Think: "I should run the tests to verify correctness."
Turn 2 β Act: run_tests("test_fib.py")
Turn 2 β Observe: "FAILED: test_fib.py::test_edge_cases - AssertionError: fib(0) returned 1, expected 0"
Turn 3 β Think: "The base case for 0 is wrong. I need to fix the function."
Turn 3 β Act: write_file("fib.py", "def fib(n): if n == 0: return 0 ...")
Turn 3 β Observe: "File written."
Turn 4 β Think: "Let me re-run the tests."
Turn 4 β Act: run_tests("test_fib.py")
Turn 4 β Observe: "PASSED: 4 tests in 0.1s"
Turn 5 β Think: "All tests pass. I'm done."
Turn 5 β Final Answer: "The fibonacci function is complete and all tests pass."
The critical structural requirement is that all prior observations stay in the context window. The LLM "remembers" every step because every step is a message in the conversation history. This is exactly what LangGraph's MessagesState provides.
βοΈ Two Implementations: create_react_agent vs Building the Loop Manually
LangGraph gives you two paths to a working ReAct agent. The right choice depends on how much you need to customize the loop.
Path 1: The Prebuilt Shortcut with create_react_agent
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def run_tests(test_file: str) -> str:
"""Run pytest on the given test file and return output."""
import subprocess
result = subprocess.run(
["pytest", test_file, "-v", "--tb=short"],
capture_output=True, text=True
)
return result.stdout + result.stderr
@tool
def write_file(path: str, content: str) -> str:
"""Write content to a file and return a confirmation."""
with open(path, "w") as f:
f.write(content)
return f"File '{path}' written ({len(content)} chars)."
llm = ChatOpenAI(model="gpt-4o")
tools = [run_tests, write_file]
# One line β the entire ReAct loop is wired for you
agent = create_react_agent(llm, tools)
result = agent.invoke({
"messages": [("user", "Write a fibonacci function in fib.py and make all tests in test_fib.py pass.")]
})
print(result["messages"][-1].content)
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. It accepts an optional state_modifier argument for injecting a system prompt without polluting message history.
When it's enough: prototyping, simple single-agent flows, standard MessagesState, no custom inter-step logic.
Where it falls short: you cannot add custom nodes between the agent and tool steps, you cannot use a non-standard state schema, and you cannot insert side effects (logging, guardrails, memory writes) mid-loop.
Path 2: Building the Loop Manually as a StateGraph
from langgraph.graph import StateGraph, MessagesState, END
from langgraph.prebuilt import ToolNode
from langchain_core.messages import SystemMessage, AIMessage
from langchain_openai import ChatOpenAI
SYSTEM_PROMPT = SystemMessage(content="""You are a Python coding assistant.
When asked to implement code, write it, run the tests, fix any failures,
and iterate until ALL tests pass. Use your tools to verify every change.""")
def build_react_agent(llm, tools):
llm_with_tools = llm.bind_tools(tools)
def agent_node(state: MessagesState):
# Prepend system prompt on every call β it is NOT stored in state
messages = [SYSTEM_PROMPT] + state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
def should_continue(state: MessagesState) -> str:
last_msg = state["messages"][-1]
# If the LLM issued tool calls, route to the tools node
if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
return "tools"
# Otherwise the LLM produced a final answer β stop
return END
tool_node = ToolNode(tools)
workflow = StateGraph(MessagesState)
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") # always loop back after tool execution
return workflow.compile()
agent = build_react_agent(ChatOpenAI(model="gpt-4o"), tools)
The key difference from create_react_agent is the should_continue function β you own it. You can add early-exit conditions, custom logging, or guardrail checks right there.
π§ 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.
The data flow for one iteration looks like this:
| Step | Object Created | Type |
| User sends request | HumanMessage("Write a fib function...") | HumanMessage |
| LLM decides to call a tool | AIMessage(tool_calls=[ToolCall(...)]) | AIMessage |
ToolNode executes the tool | ToolMessage(content="FAILED: ...", tool_call_id=...) | ToolMessage |
| LLM sees all three messages, decides next action | AIMessage(tool_calls=[...]) | AIMessage |
ToolNode handles the execution machinery. You define tools with the @tool decorator; ToolNode receives the AIMessage, extracts the tool_calls list, dispatches each call to the matching function in your tools list, and wraps the return values in ToolMessage objects with the correct tool_call_id. This ID links each observation back to the specific tool call that generated it β critical for models that can emit multiple parallel tool calls in a single AIMessage.
bind_tools is the bridge between the LLM and your functions. When you call llm.bind_tools(tools), LangChain serializes your tool signatures and docstrings into the format that the model's API expects (OpenAI's tools parameter, Anthropic's tools block, etc.). The LLM never calls your Python functions directly β it emits a structured JSON tool call in its response, which ToolNode then dispatches.
Performance Analysis
Token cost scales with loop depth. Each iteration adds messages to the context. By turn 5, the prompt contains the full conversation so far. For a GPT-4o run with 5 iterations at ~500 tokens per turn, you are paying for roughly 2,500 input tokens on turn 5 alone β compared to 500 on turn 1. This is the context accumulation cost, and it is the primary cost driver for deep loops.
Latency is additive. A 3-turn loop where each LLM call takes 2 seconds adds 6 seconds of LLM latency before the first final answer arrives. Tool execution latency (test runs, API calls, file I/O) stacks on top of that.
Infinite loop risk is real. The should_continue function is your only safety valve. If the LLM consistently hallucinates tool results or gets into a correctionβfailureβcorrection cycle, the loop will run until it exhausts your token budget. The standard mitigation is an iteration counter in custom state:
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}
Using a custom TypedDict instead of MessagesState gives you this kind of extra state field. The trade-off is that you must manage the messages field's merge reducer yourself (hence Annotated[list, add_messages]).
π 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.
graph TD
A([βΆ START]) --> B["π€ Agent Node\n(LLM call with bind_tools)"]
B --> C{should_continue}
C -- "tool_calls present\nin last AIMessage" --> D["π§ ToolNode\n(execute tool functions)"]
D -- "append ToolMessages\nto state" --> B
C -- "no tool_calls\n(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
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. LangGraph's graph model explicitly supports cycles; a plain DAG-based workflow library (like early Airflow or Prefect) would require unrolling this into N duplicated steps. With LangGraph, the cycle is a first-class construct.
The should_continue function is the only decision point. Everything else β state merging, tool dispatch, error wrapping β is handled by LangGraph internals.
π 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.
Automated data analysis pipelines: A data analyst agent receives a natural-language question ("Why did revenue drop 12% in Q3?"), queries a SQL database, inspects the result, runs a Python aggregation, charts the output, and iterates until the answer is coherent. Each query result is an observation that shapes the next query.
Customer support with live system lookup: An agent receives a ticket, looks up the customer's subscription status, checks recent invoices, queries an incident log, and synthesizes a reply. Without the loop, it cannot follow the chain of lookups that depends on what it found in the previous step.
Research and summarization over live corpora: Search β read β decide if more sources are needed β search again β synthesize. The agent terminates when it has enough evidence, not after a fixed number of searches.
In all cases, the same structural requirement applies: the task cannot be completed in one call because the correct next step depends on what the previous step returned.
βοΈ Trade-offs and Failure Modes: Infinite Loops, Token Explosion, and Hallucinated Tool Calls
Performance vs. 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. For high-throughput systems, this cost is non-trivial.
Mitigation: Use a cheaper model (GPT-4o-mini, Haiku) for tool routing turns and reserve the expensive model for the final synthesis turn. LangGraph makes this easy β you can use different LLMs in different nodes.
Infinite Loop Failure Mode
The most dangerous failure mode is an agent that loops without making progress. This happens when:
- The LLM repeatedly calls the same tool with the same arguments (stuck in a retry loop).
- Tool errors are ambiguous and the LLM cannot determine whether to retry or give up.
- The LLM hallucinates a successful tool result and then cannot reconcile it with subsequent failures.
Mitigation: Always implement an iteration cap in custom state (shown in the Deep Dive). Additionally, log every (tool_name, arguments) pair and detect duplicate calls within the same session.
Hallucinated Tool Calls
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. LangGraph's ToolNode will raise a ValueError in this case. If unhandled, this crashes the graph.
Mitigation: Wrap ToolNode execution in a try/except and return a ToolMessage with the error string so the LLM can recover:
from langgraph.prebuilt import ToolNode
# ToolNode has built-in error handling via handle_tool_errors
tool_node = ToolNode(tools, handle_tool_errors=True)
Setting handle_tool_errors=True (available in LangGraph β₯ 0.1.17) catches exceptions from tools and returns the traceback as a ToolMessage, keeping the loop alive so the LLM can attempt a correction.
Token Explosion in Long Conversations
Deep loops on complex tasks can exhaust the context window. A 20-turn loop with verbose tool outputs can easily hit 128K tokens on GPT-4o.
Mitigation: Summarize old tool results before they accumulate. After every N turns, invoke a summarization step that compresses all prior ToolMessage content into a single condensed HumanMessage, then trim the history. This is a custom node you would add between tools and agent in a manual build β another reason to build the loop yourself when operating at production scale.
π§ Decision Guide: create_react_agent vs Custom Loop vs Plan-and-Execute
| Situation | Recommendation |
| Prototype or demo β standard tools, no custom nodes | Use create_react_agent; ship in minutes |
| Production agent with logging, guardrails, or memory writes between steps | Build the loop manually with StateGraph; add nodes between tools and agent |
| Task with a knowable, fixed structure (research 5 papers β synthesize) | Use Plan-and-Execute instead; see Multistep AI Agents: The Power of Planning |
| Multi-agent system where this agent is one node | Build manually; create_react_agent compiles to a CompiledGraph that can be embedded as a node in a parent graph |
| Non-standard state (need to track iteration count, custom fields) | Build manually with a custom TypedDict state |
| LLM is Anthropic, Groq, or Ollama (not OpenAI) | Either approach; bind_tools is provider-agnostic |
π§ͺ 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 deciding what to fix. 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 nothing more to fix.
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).")
What the loop looks like across 3+ turns:
[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."
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.
π οΈ 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)
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,
)
create_react_agent is LLM-agnostic: any model that supports tool calling via .bind_tools() works identically. This includes ChatOpenAI, ChatAnthropic, ChatOllama, and ChatGroq. The graph topology is fixed: agent β conditional β tools β agent.
For inspection, you can render the compiled graph:
from IPython.display import Image
Image(agent.get_graph().draw_mermaid_png())
create_tool_calling_executor (legacy)
create_tool_calling_executor was an earlier LangGraph function (pre-0.1.x) that wired a similar loop but with a less flexible state schema and no state_modifier support. It is now considered deprecated in favour of create_react_agent. If you encounter it in older codebases, replace it with create_react_agent β the interface is nearly identical.
Scratchpad vs MessagesState
The original ReAct paper used a scratchpad approach: all thoughts, actions, and observations were concatenated into a single string that grew with each step. LangGraph uses MessagesState instead: a typed list of BaseMessage objects (Human, AI, Tool). The MessagesState approach is strictly superior for LangGraph use:
| Scratchpad (string) | MessagesState (typed list) | |
| Parsing tool calls | Manual regex | Structured tool_calls attribute |
| Multi-turn history | One long string | Ordered list, each message typed |
| Provider compatibility | Prompt-engineering dependent | Natively supported by all major LLM APIs |
| Streaming | Difficult | Native via .astream() |
π Lessons Learned
1. The system prompt does not belong in state. A common mistake is appending the system prompt as the first message in MessagesState. It then gets re-included in every ToolMessage response, inflating token counts and sometimes confusing tool routing. Always inject it in the agent_node function β not in the initial invoke payload.
2. handle_tool_errors=True is non-negotiable in production. Without it, a single malformed tool call crashes the graph with an unhandled exception. With it, the LLM sees the error as a ToolMessage and can self-correct. The cost is negligible; the resilience benefit is significant.
3. Build manually when you need anything non-standard. The moment you need to log tool call latency, write an observation to a vector store, run a guardrail check, or enforce a different system prompt per iteration, create_react_agent is not flexible enough. Start with the manual build from day one if any of those requirements exist.
4. Deterministic iteration limits prevent runaway costs. Always bound the loop. A hard cap of 8β12 iterations is reasonable for most coding or research tasks. Log when the cap is hit β those cases are diagnostically valuable for identifying prompts or tools that cause the agent to spin.
5. Token cost grows quadratically, not linearly. Turn N sends all N-1 prior messages plus the new input. Budget for this. For GPT-4o at $5/M input tokens, a 10-turn loop with 800 tokens per turn costs roughly 10Γ more than the equivalent single-shot call, not 10Γ more than turn 1 alone β because every turn re-sends the entire history.
π TLDR: Summary and Key Takeaways
TLDR: ReAct = Think + Act + Observe, looped as a LangGraph graph β prebuilt or custom.
- Single-shot LLM calls fail at any task where the correct next step depends on real-world feedback (test output, API responses, file contents).
- ReAct solves this by embedding a reasoning trace interleaved with tool calls in a loop β each tool result becomes an observation that informs the next thought.
- LangGraph implements ReAct as a graph: agent node β conditional edge β ToolNode β back to agent node. The back-edge is what makes it a loop, not a chain.
create_react_agentgives you a working loop in two lines. Use it for prototyping and simple single-agent flows.- The manual
StateGraphbuild gives you full control: custom state fields, extra nodes between steps, per-node model selection, and customshould_continuelogic. - Iteration limits are mandatory in production. Context accumulates with every turn; without a cap, runaway loops are expensive and hard to debug.
bind_toolsis provider-agnostic. The same ReAct graph works with OpenAI, Anthropic, Ollama, and Groq without changing the graph topology.- The memorable rule: if a task requires "act, see what happened, then decide," it is a ReAct task. Wire it as a loop, not a chain.
π Practice Quiz
What is the primary reason a single LLM call cannot complete a "write code β run tests β fix β repeat" task?
- A) LLMs cannot generate Python code
- B) A single call has no mechanism to observe the test output and act on it
- C) Tool calling requires multiple API keys
- D) LangGraph does not support code generation Correct Answer: B
In a LangGraph ReAct agent, what determines whether the loop continues or terminates?
- A) The number of tokens generated by the LLM
- B) A hardcoded turn counter inside
ToolNode - C) The
should_continueconditional edge function, which checks whether the lastAIMessagecontainstool_calls - D) The
MessagesStateschema automatically stops after 5 messages Correct Answer: C
You need to log every tool call's latency to a database and run a safety guardrail check before the result is returned to the agent. Which approach is correct?
- A) Use
create_react_agentwithstate_modifier - B) Build the loop manually with
StateGraphand add a custom node betweentoolsandagent - C) Subclass
ToolNodeand override itsinvokemethod viacreate_react_agent - D) This is not possible in LangGraph Correct Answer: B
- A) Use
Open-ended: A ReAct agent for customer support is completing tasks in 3β5 turns on average, but occasionally spirals into 20+ turn loops when a backend API returns ambiguous error codes. Describe two distinct strategies β one at the graph level and one at the tool/prompt level β to detect and break out of these stuck loops without simply cutting the agent off mid-task.
π Related Posts
- Multistep AI Agents: The Power of Planning β when Plan-and-Execute outperforms a ReAct loop
- AI Agents Explained: When LLMs Start Using Tools β the foundational concepts behind tool-calling agents
- AI Architecture Patterns: Routing, Planning, Memory, and Evaluation β where ReAct fits in a full production AI architecture

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
Software Engineering Principles: Your Complete Learning Roadmap
TLDR: This roadmap organizes the Software Engineering Principles series into a problem-first learning path β starting with the code smell before the principle. New to SOLID? Start with Single Responsibility. Facing messy legacy code? Jump to the smel...
Machine Learning Fundamentals: Your Complete Learning Roadmap
TLDR: πΊοΈ Most ML courses dive into math formulas before explaining what problems they solve. This roadmap guides you through 9 essential posts across 3 phases: understanding ML fundamentals β mastering core algorithms β deploying production models. ...
Low-Level Design Guide: Your Complete Learning Roadmap
TLDR TLDR: LLD interviews ask you to design classes and interfaces β not databases and caches.This roadmap sequences 8 problems across two phases: Phase 1 (6 beginner posts) builds your core OOP vocabulary through increasingly complex domains; Phase...

LLM Engineering: Your Complete Learning Roadmap
TLDR: The LLM space moves so fast that engineers end up reading random blog posts and never build a mental model of how everything connects. This roadmap organizes 35+ LLM Engineering posts into 7 tra
