Vai al contenuto principaleVai al footer
PatternscriptbeginnerEseguibileguided-flow

ReAct: reason, act, observe, repeat

The model alternates between explicit reasoning and tool calls, feeding each observation back until it can answer. The reasoning is a first-class inspectable field rather than something that happened inside the model, and that is what separates ReAct from plain tool-calling.

Fatti chiave

Livello
beginner
Runtime
Python • API OpenAI
Pattern
Flusso ispezionabile con confini di sistema visibili
Interazione
Sandbox live • Script
Aggiornato
27 luglio 2026

Naviga questo esempio

Vista rapida del flusso

Come questo esempio si muove tra input, esecuzione e risultato rivedibile

ReAct: reason, act, observe, repeat -> Load environment keys -> Initialize OpenAI client -> Constrain output schema -> Send message payload -> Retry after failure

Trigger

ReAct: reason, act, observe, repeat

Runtime

Load environment keys

Esito

Initialize OpenAI client

Perché esiste questa pagina

Questo esempio è mostrato sia come codice sorgente reale che come pattern di interazione orientato al prodotto, così i discenti possono collegare implementazione, UX e doctrine senza lasciare la libreria.

Flusso visivoCodice realeSandbox o walkthroughAccesso MCP

Relazioni con la doctrine

Queste sono forme di ragionamento: come un agente decide, quando agisce, cosa ricorda. Per comporre l'interfaccia intorno a esso, vedi l'AI Pattern Blueprint. Per trigger, pianificazione, caricamento del contesto e ripristino, vedi Agent runtime architecture.

Strutturale

Questo pattern esprime questi principi direttamente nel codice.

  • P7 · Stabilire fiducia attraverso l'ispezionabilità

    The defining property of ReAct over plain tool-calling: `thought` is a required field on every step, so the reasoning that led to each action is in the transcript rather than inside the model. A reader can re-derive why any given tool was called.

    react_loop.py:Step

Gap predefinito

La forma classica di questo pattern li viola per impostazione predefinita. Ognuno riporta la correzione.

  • P2 · Assicurarsi che il lavoro in background rimanga percepibile

    Default ReAct returns only the final answer. The loop iterates server-side and the caller sees nothing until it finishes, which reads as a hang. Fix, and this implementation does it deliberately: emit the step signal BEFORE the model call, not after, so a step that is still running is visible as running.

    react_loop.py:run

  • P8 · Rendere espliciti i passaggi, le approvazioni e i blocchi

    Every action the model selects executes. There is no approval boundary anywhere in the loop, which is safe here only because both tools are read-only. Fix: compose with the dry-run pattern so any irreversible action is proposed, simulated, and gated before it runs.

    react_loop.py:run

  • P10 · Ottimizzare per la guida, non solo per l'inizio

    Once started the loop runs to an answer or to MAX_STEPS. The operator can abort it but cannot redirect it, because there is no point at which new instruction is accepted. Fix: check a steer channel between iterations and fold any pending instruction into the transcript. This implementation does NOT do that, and is a faithful example of the gap.

    react_loop.py:run

Le relazioni con la doctrine sono analisi di prima parte, non il risultato di una validazione. Esegui il validatore sul tuo codice per ottenere un verdetto con punteggio.

Come dovrebbe essere usato questo esempio nella piattaforma?

Usa prima la sandbox per comprendere il pattern di esperienza, poi ispeziona il sorgente per vedere come il confine del prodotto, il confine del modello e il confine della doctrine sono effettivamente implementati.

UX pattern: Flusso ispezionabile con confini di sistema visibili
Assicurarsi che il lavoro in background rimanga percepibile
Stabilire fiducia attraverso l'ispezionabilità
Rendere espliciti i passaggi, le approvazioni e i blocchi

Riferimenti sorgente

Voce di libreria
pattern-react
Percorso sorgente
content/patterns/tools-actions/react/react_loop.py
Librerie
openai, pydantic, python-dotenv
Requisiti di runtime
OPENAI_API_KEY
Principi correlati
Assicurarsi che il lavoro in background rimanga percepibile, Stabilire fiducia attraverso l'ispezionabilità, Rendere espliciti i passaggi, le approvazioni e i blocchi, Ottimizzare per la guida, non solo per l'inizio

react_loop.py

python
"""
ReAct: reason, act, observe, repeat

The loop that sits underneath most working agents. The model does not answer
directly. It alternates between reasoning about what it needs and acting to
get it, feeding each observation back until it can answer.

    Thought -> Action -> Observation -> Thought -> ... -> Answer

What separates ReAct from plain tool-calling is that the reasoning is an
explicit, inspectable field rather than something that happened inside the
model. That is the property worth keeping.

Two things this shape does NOT give you for free, both deliberate here:

  - Nothing stops an action. Every action the model picks runs. Compose with
    the dry-run pattern if any action is irreversible.
  - Nothing steers it. Once started, the loop runs to MAX_STEPS or to an
    answer. You can abort it; you cannot redirect it.

Run: python react_loop.py
"""

import ast
import operator
from typing import Literal

from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field

load_dotenv()

client = OpenAI()

MODEL = "gpt-5.4-mini"
MAX_STEPS = 6

# --------------------------------------------------------------
# Tools
# --------------------------------------------------------------

INVENTORY = {
    "widget": {"stock": 12, "unit_price": 9.99},
    "gadget": {"stock": 0, "unit_price": 24.50},
    "sprocket": {"stock": 340, "unit_price": 1.25},
}


def lookup_item(name: str) -> str:
    item = INVENTORY.get(name.strip().lower())
    if item is None:
        return f"unknown item '{name}'. Known items: {', '.join(INVENTORY)}"
    return f"{name}: stock={item['stock']}, unit_price={item['unit_price']}"


#: The only operations the calculator will perform. Anything outside this map
#: is refused by structure rather than by pattern-matching the input.
_BINARY_OPS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
}
_UNARY_OPS = {ast.UAdd: operator.pos, ast.USub: operator.neg}


def _evaluate_node(node: ast.expr) -> float:
    """Walk a parsed expression, admitting only numbers and the four operators.

    Deliberately not `eval`. A tool an agent can call with model-authored input
    is the last place to hand over an interpreter, and a character allowlist in
    front of `eval` is a filter someone will eventually widen by one character.
    Refusing by structure cannot be widened by accident.
    """
    if isinstance(node, ast.Constant):
        if isinstance(node.value, bool) or not isinstance(node.value, (int, float)):
            raise ValueError(f"not a number: {node.value!r}")
        return float(node.value)
    if isinstance(node, ast.BinOp) and type(node.op) in _BINARY_OPS:
        return _BINARY_OPS[type(node.op)](
            _evaluate_node(node.left), _evaluate_node(node.right)
        )
    if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY_OPS:
        return _UNARY_OPS[type(node.op)](_evaluate_node(node.operand))
    raise ValueError(f"unsupported expression element: {type(node).__name__}")


def calculate(expression: str) -> str:
    """Arithmetic only, over numbers and + - * / with parentheses."""
    if not expression.strip():
        return "refused: empty expression"
    try:
        parsed = ast.parse(expression, mode="eval")
        return str(round(_evaluate_node(parsed.body), 4))
    except ZeroDivisionError:
        return f"could not evaluate '{expression}': division by zero"
    except (SyntaxError, ValueError, TypeError) as exc:
        return f"refused: '{expression}' is not plain arithmetic ({exc})"


TOOLS = {"lookup_item": lookup_item, "calculate": calculate}

TOOL_DOCS = """
lookup_item(name)      -> stock and unit price for one inventory item
calculate(expression)  -> evaluate a plain arithmetic expression
""".strip()


# --------------------------------------------------------------
# One turn of the loop
# --------------------------------------------------------------


class Step(BaseModel):
    thought: str = Field(
        description="What you know so far and what you still need. Always required."
    )
    next: Literal["act", "answer"]
    action: Literal["lookup_item", "calculate"] | None = None
    action_input: str | None = None
    answer: str | None = Field(
        default=None, description="Set only when next == 'answer'."
    )


def think(question: str, transcript: list[str]) -> Step:
    history = "\n".join(transcript) if transcript else "(nothing yet)"
    completion = client.beta.chat.completions.parse(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "You answer questions by reasoning and using tools, one step "
                    "at a time.\n\nTools:\n" + TOOL_DOCS + "\n\n"
                    "State your reasoning in `thought` every step. Set next='act' "
                    "with an action to gather information, or next='answer' when "
                    "the transcript already contains everything you need. Never "
                    "guess a number you could look up."
                ),
            },
            {
                "role": "user",
                "content": f"Question: {question}\n\nTranscript so far:\n{history}",
            },
        ],
        response_format=Step,
    )
    return completion.choices[0].message.parsed


def run(question: str) -> str:
    transcript: list[str] = []

    for step_number in range(1, MAX_STEPS + 1):
        # Emitted BEFORE the model call, not after it. A step that is still
        # running is visible as "running", which is the whole point.
        print(f"  [{step_number}/{MAX_STEPS}] thinking...")

        step = think(question, transcript)
        print(f"      thought: {step.thought}")
        transcript.append(f"Thought: {step.thought}")

        if step.next == "answer":
            answer = step.answer or "(model chose to answer but gave none)"
            print(f"      answer : {answer}")
            return answer

        if not step.action or step.action not in TOOLS:
            observation = f"no such tool: {step.action}"
        else:
            print(f"      action : {step.action}({step.action_input or ''})")
            observation = TOOLS[step.action](step.action_input or "")

        print(f"      observed: {observation}")
        transcript.append(
            f"Action: {step.action}({step.action_input}) -> {observation}"
        )

    # Budget exhausted is a real outcome, not a crash. Say so plainly and
    # hand back what was learned rather than raising.
    exhausted = (
        f"Stopped after {MAX_STEPS} steps without reaching an answer. "
        f"What was established:\n" + "\n".join(transcript)
    )
    print("      BUDGET EXHAUSTED")
    return exhausted


if __name__ == "__main__":
    for question in [
        "What does it cost to buy every widget we have in stock?",
        "Can I order 5 gadgets right now?",
    ]:
        print(f"\n> {question}")
        run(question)

Cosa dovrebbe ispezionare il discente nel codice?

Cerca il punto esatto in cui lo scope del sistema è delimitato: definizioni di schema, impostazione del prompt, configurazione di runtime e il punto di chiamata che trasforma l'intenzione dell'utente in un'azione concreta del modello o del workflow.

Cerca i contratti di output e la validazione
Cerca la chiamata di esecuzione esatta
Cerca cosa il prodotto potrebbe esporre all'utente

Come si relaziona la sandbox al sorgente?

La sandbox dovrebbe rendere leggibile l'UX: cosa vede l'utente, cosa sta decidendo il sistema e come il risultato diventa revisionabile. Il sorgente mostra poi come quel comportamento è effettivamente implementato.

Leggi il riepilogo dell'implementazione.
Esplora gli stati utente e di sistema.
Ispeziona il codice sorgente tenendo a mente le decisioni di doctrine evidenziate.
SandboxFlusso ispezionabile con confini di sistema visibili

Guida all'interazione

Usa la sandbox per esplorare l'esperienza visibile all'utente, il lavoro del sistema e la scelta di doctrine che l'esempio sta facendo.

Spiegazione UX

La sandbox spiega cosa dovrebbe vedere l'utente, cosa sta facendo il sistema e dove il controllo o l'ispezionabilità devono rimanere espliciti.

Spiegazione AI Design

La pagina trasforma il codice sorgente in un pattern orientato al prodotto: cosa può decidere il modello, cosa dovrebbe esporre il prodotto e dove il codice deterministico o la revisione devono subentrare.

Guida all'interazione

  1. 1Leggi il riepilogo dell'implementazione.
  2. 2Esplora gli stati utente e di sistema.
  3. 3Ispeziona il codice sorgente tenendo a mente le decisioni di doctrine evidenziate.

Visibile all'utente

The model alternates between explicit reasoning and tool calls, feeding each observation back until it can answer. The reasoning is a first-class inspectable field rather than something that happened inside the model, and that is what separates ReAct from plain tool-calling.

Lavoro del sistema

Il prodotto prepara un task delimitato per il modello o il workflow.

Perché è importante

L'interfaccia dovrebbe rendere il task delegato leggibile prima che avvenga l'automazione.

Usato in corsi e percorsi

Questo esempio attualmente è indipendente nella libreria, ma si connette comunque al sistema dei principi e alla famiglia di esempi più ampia.

Principi correlati

Runtime architecture

Usa questo esempio nei tuoi agenti

Questo esempio è disponibile anche tramite il layer agent-ready del blueprint. Usa la pagina Per agenti per recuperare MCP pubblico, export deterministici e setup per Claude o Cursor.

Definisci trigger, contesto e confini prima di aumentare l'autonomia
Rendi espliciti controllo, osservabilita e recovery nel runtime
Scegli i pattern operativi giusti prima di delegare ai workflow