Construyendo un agente ReAct en Python puro: Pensamiento, acción, observación
Escriba un bucle de agente ReAct desde cero en Python: el contrato de Pensamiento, Acción y Observación, un bucle while controlado por PAUSE, retroalimentación de errores y dos modos de fallo en la producción.
Fuentes: Building a ReAct AI Agent from Scratch in Pure Python
Un agente de tipo planificar-y-ejecutar cuenta con un truco ingenioso: el modelo elabora por adelantado una lista numerada de pasos, y Python puro los sigue uno por uno. Eso funciona hasta que el mundo real no coincide con el plan. Un punto de conexión se mueve, una búsqueda devuelve un 404, o el modelo simplemente se equivoca en el segundo paso. El agente sigue avanzando con los pasos tres a diez de todos modos, ya que su diseño no le permite consultar información externa.
ReAct soluciona este problema reemplazando el plan completo por un bucle. A continuación, se muestra cómo ese bucle se construye en Python puro, sin necesidad de ningún framework, antes de analizar las dos formas en que tiende a fallar cuando interactúa con usuarios reales.
De un itinerario fijo a un bucle de retroalimentación
Imagínese dos formas de cruzar un bosque desconocido. Un viajero anota “1 km al norte, luego 500 m al este” y lo sigue al pie de la letra, lo que significa que continúa caminando incluso cuando aparece un acantilado a los 200 m. El otro se detiene cada pocos pasos y razona en voz alta: el camino hacia el norte está bloqueado por un acantilado, así que prueba hacia el oeste como desvío, y sí, allí hay un sendero seguro.
El primer viajero es el agente de planificar y ejecutar. Es económico en términos de recursos y bien organizado, pero frágil: si el segundo paso falla, los pasos del 3 al 10 pierden toda utilidad. El segundo es el agente ReAct (Razonamiento más Acción). Produce un solo paso a la vez y vuelve a revisar su entorno antes de dar el siguiente. Eso consume más recursos, pero resiste mucho mejor cuando las cosas salen mal.
El comentario en tiempo real que mantiene el segundo viajero es un monólogo interno, y es el mecanismo que hace que un agente parezca adaptativo en lugar de seguir un guion preestablecido. Sin embargo, para ser útil, ese monólogo debe poder desencadenar acciones reales.
Llamada a herramientas en un único párrafo
Un modelo de lenguaje solo puede generar texto. Para darle capacidad de acción, se utiliza un diccionario sencillo que asocia nombres con funciones de Python, algo como {"get_weather": get_weather_api}. Cuando la salida del modelo contiene la cadena get_weather, el código lo detecta y llama a la función correspondiente. Las herramientas permanecen exactamente igual en un agente ReAct. Lo que cambia es cómo el modelo decide utilizarlas.
Aquí está la parte ligeramente extraña de todo este diseño: para hacer que un modelo parezca pensar por sí mismo, es necesario restringirlo severamente.
El contrato detrás del monólogo
En lugar de solicitar una respuesta, se obliga al modelo a seguir un formato de salida estricto y no se le permite utilizar ninguna herramienta hasta que haya cumplido con ese formato. El bucle consta de tres etapas:
- Pensamiento. El modelo revisa la pregunta, todo lo recopilado hasta el momento y cualquier error anterior, y luego anota qué hacer a continuación y por qué.
- Acción. Basándose en ese pensamiento, elige una herramienta de la lista y escribe los datos de entrada exactos para ella.
- Observación. Esta es la etapa crucial. El modelo debe detenerse. Tu código en Python toma el control, lee la acción, ejecuta la función real (una consulta meteorológica en tiempo real, una búsqueda en la base de datos, lo que sea) y devuelve el resultado bruto al modelo.
Cada vuelta por el ciclo proporciona al modelo nuevas pruebas a partir de las cuales razonar. El ciclo de hipotetizar, ejecutar y verificar se repite hasta que un pensamiento llega finalmente a una conclusión que el usuario pueda comprender. En resumen, el modelo razona y toma decisiones, mientras que el código ordinario actúa como árbitro, interceptando cada llamada a una herramienta y proporcionando información sobre lo que realmente ocurrió.
Sobre el papel suena sencillo. La pregunta práctica es cómo hacer que un predictor de texto se detenga a mitad de proceso, espere a un script y luego retome su secuencia de razonamiento.
Escribiendo el bucle a mano
Frameworks como LangChain, CrewAI y AutoGen ocultan todo esto detrás de una sola llamada a .run(). Eso es conveniente hasta que el agente da vueltas en círculos o inventa una herramienta que no existe. Depurar una caja negra es frustrante, por lo que vale la pena saber qué hay dentro de ella.
Comience con la instrucción del sistema, que establece las reglas de interacción y define el contrato:
system_prompt = """
You are a dynamic AI Agent. You work in a continuous
loop of
- Thought,
- Action,
- Observation.
- PAUSE
Your available tools are:
- get_weather(location: str): Returns the current weather.
- calculate(expression: str): Evaluates a math expression.
Use the following format exactly:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_weather, calculate]
Action Input: the input to the action
PAUSE
(You will then receive an Observation from the environment)
Thought: I now know the answer
Final Answer: the final answer to the original input question
"""
La línea PAUSE se encarga de la tarea principal, y el comando también especifica los nombres exactos de los campos (Question, Thought, Action, Action Input, Final Answer) para que una expresión regular pueda encontrarlos de manera fiable en la salida de formato libre. Indica al modelo que deje de generar, lo que devuelve el control a Python para que la herramienta pueda ejecutarse y su salida pueda escribirse en el contexto como una Observation.
Los modelos no siguen perfectamente las instrucciones de formato, y lo verá a continuación. Sin embargo, un prompt es solo un libro de reglas; algo tiene que hacer que se cumplan: detectar PAUSE, ejecutar la función y mantener el proceso en marcha. Eso es un bucle while, el corazón del agente. En cada iteración, solicita al modelo que responda, analiza la respuesta, ejecuta cualquier herramienta solicitada y agrega todo el intercambio a la historia, deteniéndose cuando el modelo da una respuesta final o se agota el tiempo asignado para esa turno.
import re
def react_agent(self, user_question, max_turns=5):
# Initialize our conversational memory with the system prompt
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": f"Question: {user_question}"}
]
turn_count = 0
while turn_count < max_turns:
print(f"\n--- Turn {turn_count + 1} ---")
# 1. Get the LLM's response (The Thought + Action)
response = my_react_agent.call_llm(messages)
print(response)
# Add the LLM's generation to the memory
messages.append({"role": "assistant", "content": response})
# 2. Check if the agent has reached a conclusion
if "Final Answer:" in response:
print("\n✅ Task Complete.")
#print(json.dumps(messages, indent=2))
return response.split("Final Answer:")[-1].strip()
# 3. Parse the Action and Action Input using Regex
action_match = re.search(r"Action: (.*)", response)
input_match = re.search(r"Action Input: (.*)", response)
if action_match and input_match:
action = action_match.group(1).strip()
action_input = input_match.group(1).strip()
# 4. The Observation Phase (Python takes control)
print(f"⚙️ System Executing: {action}({action_input})")
try:
action_input = json.loads(action_input)
observation_result = my_react_agent.execute_tool(action, action_input)
except (TypeError, ValueError, json.JSONDecodeError) as error_messgae:
observation_result = error_messgae
# Format the observation and feed it back to the agent
observation_text = f"Observation: {observation_result}"
messages.append({"role": "user", "content": observation_text})
print(observation_text)
else:
# If the LLM breaks the API contract, gently correct it
messages.append({"role": "user", "content": "Error: Invalid format. Please provide an Action and Action Input, or a Final Answer."})
turn_count += 1
return "❌ Agent timed out before reaching a final answer."
Qué te aporta tener el bucle propio
Dado que el bucle es tu propio código, tú decides qué ocurre en caso de error. Un registro de ejecución con una herramienta tipo meteorológica muestra por qué eso es importante.
Por ejemplo, el modelo determina correctamente que necesita una herramienta llamada get_planet_mass, pero escribe la entrada con comillas simples, {'planet': 'Earth'}. Eso no es JSON válido, por lo que el analizador del lado de Python lanza una advertencia estándar: esperaba nombres de propiedades entre comillas dobles.
Un script típico se detendría allí mismo. Este bucle, en cambio, no lanza excepciones. Toma el texto de error sin procesar y lo incluye en la conversación como la siguiente Observation.
En el paso dos, el modelo lee ese mensaje y reconoce en su pensamiento que el formato de entrada era incorrecto y que debe seguir las reglas JSON. Vuelve a realizar la llamada con {"planet": "Earth"}; el enrutador ejecuta la herramienta y la tarea se completa.
Fíjese que no se escribió nada especial para este caso. No existe un manejo de intentos adicionales para JSON mal formado, solo la costumbre de devolver errores como observaciones, y la comprensión lectora del propio modelo hace el resto. Así es como se ve en la práctica un monólogo interno. Un planificador estático habría fallado en el paso uno y necesitaría que una persona lo reiniciara. El bucle ReAct permitió al modelo diagnosticar y reparar su propia sintaxis durante la ejecución. No se limitó a seguir un plan, sino que reaccionó a lo que realmente sucedió.
Dónde falla ReAct en producción
El código anterior parece casi mágico la primera vez que se ejecuta. Antes de enviarlo, debes conocer los dos puntos en los que falla.
La espiral de alucinaciones. A veces un modelo se convence tercamente de que una herramienta defectuosa está a punto de funcionar. Llama a get_weather, ve un error, llama a get_weather de nuevo, ve el mismo error, y sigue intentándolo mientras se agotan los créditos de tu API. La defensa contra esto es la restricción max_turns=5 en el bucle. Un interruptor de circuito tan estricto es indispensable, y ningún agente ReAct debería permitirse ejecutarse sin límites.
Hinchazón del contexto. Cada ciclo agrega sus pensamientos, acciones y observaciones a la conversación. Si una herramienta devuelve un payload JSON de 10,000 líneas, todo ello termina en la ventana de contexto. Para el siguiente turno, el modelo se ve abrumado por los datos, la calidad de su razonamiento disminuye y puede perder de vista la pregunta original. Nunca introduzcas directamente respuestas brutas de API o extracciones completas de la web como observaciones. Incluso un límite aproximado, como los primeros miles de caracteres más una nota indicando que el resto fue cortado, es mejor que saturar al modelo. Coloca una función intermedia pequeña que resuma o recorte los resultados primero, para que el monólogo se mantenga breve y claro.
También existe un límite que ningún mecanismo de protección puede superar. Cuando un único agente debe manejar demasiadas herramientas complejas y una cantidad excesiva de contexto acumulado, su razonamiento se vuelve caótico y se desvía de la tarea. Un agente ReAct es un táctico brillante, pero un ejecutor deficiente.
Combinar ambos estilos
Estos dos enfoques resuelven problemas diferentes: el modelo de planificar y ejecutar proporciona estructura, mientras que ReAct aporta resiliencia. Muchos sistemas en producción terminan utilizando ambos: un planificador diseña los pasos a alto nivel y luego asigna cada subtarea compleja a su propio agente ReAct, el cual se encarga de organizar los detalles sobre la marcha. Coordinar varios agentes de esta manera, desarrollados desde cero sin frameworks, es el siguiente paso lógico después de este ciclo.
Lecturas relacionadas
- Construyendo un agente de investigación bursátil con ReAct y LangGraph — Un agente LangGraph ReAct toma un símbolo bursátil, consulta datos fundamentales, noticias, indicadores técnicos y herramientas de analistas en su propio orden, y escribe un resumen breve, con pausas para la intervención humana.
- Recomendador de K-drama multiagente con sensibilidad a los gustos: BERTopic, RAG y fusión de rango recíproco — Cómo un recomendador de K-drama que tiene en cuenta los gustos combina etiquetas BERTopic, recuperación de datos mediante ChromaDB, un agente para entrevistas, expansión de consultas y fusión de rango recíproco antes de que un LLM realice una nueva clasificación.