Agentic AI / Agent architectures, tool use and MCP / The ReAct pattern as a loop of function calls

The ReAct pattern as a loop of function calls

Every agent framework is, underneath, the same short loop: ask the model what to do, do it, show the model what happened, repeat until it answers. ReAct is the 2022 paper that named the loop. This note explains the pattern, then shows exactly where each of its three steps lands when you implement it with a modern API's function calling, with the wire format for both common conventions and a full loop in pseudocode.

The short version

ReAct interleaves reasoning and acting: a thought about what to do next, an action that calls a tool, an observation of the result, and round again. With function calling, the thought is the text or thinking the model emits before a call, the action is a structured tool-call block with a name and JSON arguments, and the observation is a tool-result message your code appends. The loop ends when the model stops without requesting a tool. Your application owns the loop, executes every tool, and decides when to stop.

01Where ReAct came from

ReAct, for Reasoning and Acting, was proposed by Yao and colleagues in late 2022. At the time there were two separate lines of work. Chain-of-thought prompting had the model reason in text before answering, which helped on multi-step problems but left the model reasoning from memory alone, free to make up facts. Action-only agents had the model emit commands against an environment, which grounded it in real results but gave it no room to plan between steps.

ReAct combined them. The prompt showed the model examples in a fixed text format:

Thought: I need to find out when the Apollo 11 mission launched.
Action: Search[Apollo 11 launch date]
Observation: Apollo 11 launched on July 16, 1969, from Kennedy Space Center.
Thought: I have the date. I can answer.
Action: Finish[July 16, 1969]

The model generates a thought and an action, the harness stops generation at the action, runs it, pastes the observation into the prompt, and lets the model continue. The reasoning steers the actions and the observations correct the reasoning. On question answering and fact verification it beat both chain-of-thought and action-only prompting, and it reduced hallucination, because claims could be checked against observations rather than invented.

02The loop

Stripped to its structure, the pattern is a loop with one exit:

        ┌──────────────────────────────────────────────┐
        │  1. thought    model reasons about the state    │
        │       │                                       │
        │       ▼                                       │
        │  2. action     model names a tool + arguments  │
        │       │                                       │
        │       ▼                                       │
        │  3. observation  your code runs the tool and    │
        │                 returns the result to the model│
        │       │                                       │
        └───────┘  repeat while the model keeps acting

           exit:  4. final answer  model responds with no tool call

In the paper this was all one text stream, and the harness had to parse it. The same loop with function calling keeps the structure and replaces the parsing with typed messages. The rest of this note takes the steps one at a time.

03Thought: where the reasoning lives

You begin by declaring the tools: a name, a description, and a JSON schema for the arguments. These go in the request alongside the system prompt. The model reads the descriptions and decides, for each turn, whether any tool is needed.

{
  "name": "get_weather",
  "description": "Current weather for a city.",
  "input_schema": {
    "type": "object",
    "properties": {
      "location": {"type": "string", "description": "City name"},
      "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
    },
    "required": ["location"]
  }
}

The thought step is the model's reasoning before it commits to a call. With a current API it appears in one of two places, and often both:

  • A text block before the tool call in the same response. "I need the current temperature in Hyderabad; I will check the weather tool." This is the direct descendant of the paper's Thought: line, and it is visible to you and, if you show it, to the user.
  • A thinking block, where the model reasons at length in a channel the API returns separately, summarised or hidden depending on the model. Models with extended thinking do most of their deliberation here and keep the visible text short.

Nothing requires the thought to be visible for the loop to work. It is also not something your code acts on. Its job is to make the action that follows a better one.

04Action: the structured tool call

When the model decides to act, the response carries a structured block rather than a line of text. Two conventions dominate.

Anthropic Messages APIOpenAI-style chat completions
Where the call appearsA tool_use content block in the assistant messageA tool_calls array on the assistant message
Call fieldsid, name, input as a JSON objectid, function.name, function.arguments as a JSON string
How you know to loopstop_reason is "tool_use"finish_reason is "tool_calls"
// Anthropic: the assistant's response content
[
  {"type": "text", "text": "Let me check the weather."},
  {"type": "tool_use", "id": "toolu_01", "name": "get_weather",
   "input": {"location": "Hyderabad", "unit": "celsius"}}
]
// stop_reason: "tool_use"

// OpenAI-style: the assistant message
{"role": "assistant", "content": null,
 "tool_calls": [{"id": "call_01", "type": "function",
   "function": {"name": "get_weather",
                "arguments": "{\"location\":\"Hyderabad\",\"unit\":\"celsius\"}"}}]}
// finish_reason: "tool_calls"

The stop reason is the signal that drives the loop. It tells your code that the model has paused mid-task and is waiting for a result, as opposed to having finished. The id on each call is what you will use to pair the result with the request, which matters when the model issues several calls at once.

Note what the model has not done: it has not run anything. A tool call is a request to your application. The model cannot reach the weather service, the database or the file system. Everything that touches the world is on your side of the boundary, which is the property that makes the pattern safe to deploy.

05Observation: the tool result message

Your code reads the call, dispatches to the matching function, and appends the outcome to the conversation as a new message that references the call's id.

// Anthropic: a user message holding tool_result blocks
{"role": "user", "content": [
  {"type": "tool_result", "tool_use_id": "toolu_01",
   "content": "{\"temperature\": 26, \"condition\": \"Clear\"}"}
]}

// OpenAI-style: a message with the tool role
{"role": "tool", "tool_call_id": "call_01",
 "content": "{\"temperature\": 26, \"condition\": \"Clear\"}"}

Three things about this step are easy to get wrong.

  • The assistant's own message goes in first. The conversation must read: user asks, assistant calls tool, result arrives. If you drop the assistant message that contained the call and append only the result, the result refers to a call that is not in the history and the request is rejected.
  • Errors are observations too. If the tool throws, send the error text back, flagged as an error where the API has a field for it. The model will usually recover: retry with corrected arguments, try another tool, or tell the user. Swallowing the error and sending nothing leaves the loop stuck.
  • The result is just text or structured content to the model. It has no special authority. If the tool fetched a web page containing "ignore your instructions", that text is now in the model's context as an observation, which is why tool outputs are the main channel for prompt injection into agents.

06Final answer: the loop's exit

You send the whole conversation back, including the new result, and the model continues. Either it decides it needs another tool, in which case the stop reason is again tool_use and you go round once more, or it writes a response to the user with no tool call, and the stop reason is end_turn (or stop). That is the exit. In the paper's terms it is Finish[...]; here it is just an ordinary message.

07The whole loop in code

Provider-neutral pseudocode, in the Anthropic message shape. Every framework's agent executor is a version of this with more options.

messages = [{"role": "user", "content": question}]

for step in range(MAX_STEPS):
    response = model(messages, tools=TOOL_SCHEMAS)        # one LLM call
    messages.append({"role": "assistant", "content": response.content})

    if response.stop_reason != "tool_use":
        return text_of(response)                           # 4. final answer

    results = []
    for call in tool_use_blocks(response):                 # 2. action(s)
        try:
            out = TOOL_FUNCTIONS[call.name](**call.input)
            results.append({"type": "tool_result",
                            "tool_use_id": call.id, "content": str(out)})
        except Exception as e:
            results.append({"type": "tool_result",
                            "tool_use_id": call.id, "content": str(e),
                            "is_error": True})
    messages.append({"role": "user", "content": results}) # 3. observation(s)

raise StepLimitReached

The thought step is not a line of code because it happens inside the model call. Everything else is explicit: the call, the dispatch, the result, the exit, and the one thing the paper did not need, a cap on steps.

08Why structured calls beat parsing text

Text-format ReActFunction calling
ParsingRegular expressions over Action: Search[...]A typed block in the response; no parsing
Argument validityWhatever the model wrote; brackets and quotes breakJSON validated against the tool's schema, with a strict mode on most APIs
StoppingStop sequences on Observation:, which the model sometimes writes itselfA stop reason set by the API
Several actions at onceNot expressibleSeveral call blocks in one response, each with an id
Prompt costFew-shot examples of the format in every requestTool schemas, which the model was trained to read
TrainingIn-context imitation onlyModels are post-trained on tool-call format and when to use it

The deeper difference is that with text ReAct the format is a convention between you and the model that either side can break. With function calling the format is enforced by the API, and the model has been trained to produce it. The loop stops being a parsing problem and becomes a control-flow problem.

09What the loop needs that the paper did not cover

  • Parallel calls. A model may issue several tool calls in one response when they are independent. Run them concurrently and return all the results in a single message, each paired by id. Returning them one at a time across several messages works but teaches the model, through the history it sees, to stop batching.
  • A step limit. A model that keeps acting without converging burns money and time. Cap the iterations, and on hitting the cap either return what you have or ask the model to answer with no tools.
  • History growth. Every observation is appended and resent on every call, so context grows with each step and large tool outputs dominate it. Truncate or summarise big results before appending, and clear or compact old results on long runs. Prompt caching makes the resent prefix cheap but does not make it smaller.
  • Side effects need a gate. Reading is safe to run automatically; sending, paying, deleting and writing are not. Put an approval step between the call and its execution for those, and make tools idempotent where you can so a retried call does not act twice.
  • Observations are untrusted. Treat tool output as data, never as instructions, and never let a tool's output alone authorise another tool's side effect.
  • Log every turn. The sequence of calls and results is the agent's trace. Store it; it is how you debug a wrong answer and how you build an evaluation set later.

10Takeaways

  1. ReAct is reasoning and acting in alternation, so observations ground the reasoning and reasoning directs the actions.
  2. Thought is the text or thinking before a call. It is for the model, not for your code.
  3. Action is a structured tool call with an id, a name and schema-validated arguments, signalled by the stop reason.
  4. Observation is a result message you append, paired by id, after the assistant message that asked for it.
  5. The exit is a response with no tool call. Everything else loops.
  6. Your application runs every tool. The model only ever asks.
  7. Add what the paper left out: parallel results in one message, a step cap, history management, approval gates, and a trace.