Saltar al contenido
Tutorial Por Cesar Nuñez intermedio
60 minutos

Tu primer agente de IA en Python sin frameworks

Python 3.10+ openai SDK (v2.x) python-dotenv

Prerequisitos

  • Python básico (clases, funciones, diccionarios)
  • Cuenta en OpenAI con créditos o método de pago

Un agente no es un chatbot con más funciones: es un programa que decide qué herramientas llamar y en qué orden para completar un objetivo. Entender ese loop desde cero, sin que un framework lo oculte, es lo que separa a quien usa agentes de quien los construye.

La diferencia entre un chatbot y un agente es una sola: el agente decide. Un chatbot genera texto en respuesta a lo que el usuario escribe. Un agente genera texto, llama a funciones, evalúa el resultado y decide qué hacer a continuación —en un loop que continúa hasta que el objetivo está cumplido.

Lo que el video de arriba construye —y lo que este tutorial explica con código— es ese loop desde cero, sin que LangChain, CrewAI o cualquier otro framework lo oculte. La razón es práctica: si no entiendes qué ocurre en cada paso, no puedes depurar cuando algo falla.

Al terminar tendrás un agente funcional capaz de leer y editar archivos de tu sistema desde instrucciones en lenguaje natural.


1. Instalación y configuración

pip install openai python-dotenv

Crea un .env en la raíz del proyecto:

OPENAI_API_KEY=sk-proj-tu-clave-aqui

Nunca pongas la API key en el código directamente. Si la subes a GitHub, OpenAI la detecta y la revoca automáticamente.

from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()  # Lee OPENAI_API_KEY del entorno automáticamente

2. El punto de partida: chatbot con memoria

Antes de construir el agente, el video parte del chatbot más simple posible. La clave es entender que la “memoria” del modelo no es un estado interno —el modelo no recuerda nada entre llamadas. La memoria eres tú manteniéndola en un array y enviándola completa en cada request:

from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()

messages = [
    {
        "role": "system",
        "content": "Eres un asistente de programación. Respondes en español, de forma concisa y directa."
    }
]

print("Chatbot activo. Escribe 'salir' para terminar.\n")

while True:
    user_input = input("Tú: ").strip()
    if user_input.lower() == "salir":
        break

    messages.append({"role": "user", "content": user_input})

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages
    )

    reply = response.choices[0].message.content
    messages.append({"role": "assistant", "content": reply})

    print(f"Bot: {reply}\n")

El modelo “recuerda” porque le enviamos el historial completo en messages en cada turno. El límite es la ventana de contexto —128k tokens para gpt-4o-mini— después de la cual hay que resumir o truncar el historial manualmente.


3. Los tres roles de la API

La API de OpenAI maneja conversaciones como una lista de mensajes con tres roles:

  • system: define el comportamiento del modelo. Se pone al inicio, el usuario no lo ve. Es donde defines qué herramientas tiene disponibles el agente y cómo debe usarlas.
  • user: lo que escribe el usuario o el resultado que devuelve una herramienta.
  • assistant: la respuesta del modelo. También puedes inyectarla manualmente para dar ejemplos de comportamiento esperado.

Para el agente, añadiremos un cuarto rol:

  • tool: el resultado de ejecutar una función que el modelo solicitó. El modelo lo recibe y decide qué hacer con él.

4. Function Calling: darle herramientas al agente

Aquí está el salto conceptual del video. En vez de que el modelo solo genere texto, le damos un catálogo de funciones que puede invocar. El modelo decide cuándo llamar a cada una según lo que necesite para completar la tarea.

Primero defines las herramientas en formato JSON Schema:

tools = [
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Lee el contenido de un archivo del sistema.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Ruta del archivo a leer."
                    }
                },
                "required": ["path"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "edit_file",
            "description": "Escribe o sobreescribe el contenido de un archivo.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Ruta del archivo a modificar o crear."
                    },
                    "content": {
                        "type": "string",
                        "description": "Contenido completo que tendrá el archivo."
                    }
                },
                "required": ["path", "content"]
            }
        }
    }
]

Luego las implementaciones reales en Python:

import json
import os

def read_file(path: str) -> str:
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        return f"Error: el archivo '{path}' no existe."
    except Exception as e:
        return f"Error al leer el archivo: {str(e)}"

def edit_file(path: str, content: str) -> str:
    try:
        os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)
        return f"Archivo '{path}' guardado correctamente."
    except Exception as e:
        return f"Error al escribir el archivo: {str(e)}"

def execute_tool(name: str, arguments: str) -> str:
    args = json.loads(arguments)
    if name == "read_file":
        return read_file(**args)
    elif name == "edit_file":
        return edit_file(**args)
    return f"Herramienta '{name}' no reconocida."

5. La clase Agent: el loop de razonamiento

Esta es la arquitectura central que el video construye. El agente no hace una sola llamada al modelo; corre un loop hasta que el modelo decide que la tarea está completa y responde con texto en vez de solicitar otra herramienta:

from openai import OpenAI
from dotenv import load_dotenv
import json

load_dotenv()
client = OpenAI()

MAX_ITERATIONS = 10  # Límite de seguridad para evitar loops infinitos

class Agent:
    def __init__(self, system_prompt: str):
        self.messages = [{"role": "system", "content": system_prompt}]

    def chat(self, user_message: str) -> str:
        self.messages.append({"role": "user", "content": user_message})

        for iteration in range(MAX_ITERATIONS):
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=self.messages,
                tools=tools,
            )

            message = response.choices[0].message

            # Si el modelo no quiere llamar a ninguna herramienta, terminó
            if not message.tool_calls:
                self.messages.append({
                    "role": "assistant",
                    "content": message.content
                })
                return message.content

            # El modelo quiere usar herramientas: las ejecutamos y devolvemos resultados
            self.messages.append(message.model_dump())

            for tool_call in message.tool_calls:
                result = execute_tool(
                    tool_call.function.name,
                    tool_call.function.arguments
                )
                self.messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result
                })

        return "Se alcanzó el límite de iteraciones sin completar la tarea."

El loop funciona así:

  1. El modelo recibe la tarea y el historial
  2. Si necesita información o quiere hacer algo, devuelve tool_calls en vez de texto
  3. Ejecutamos la herramienta y añadimos el resultado al historial como rol tool
  4. Volvemos al paso 1 con el historial actualizado
  5. Cuando el modelo tiene suficiente contexto, responde con texto y el loop termina

6. Prueba completa del agente

agent = Agent(system_prompt="""
Eres un agente de programación que puede leer y editar archivos.
Cuando el usuario te pida crear o modificar código, usa las herramientas
disponibles para hacerlo directamente. Confirma siempre qué archivos
modificaste y por qué.
""")

# El agente leerá el archivo, entenderá el código y creará el nuevo
respuesta = agent.chat(
    "Lee el archivo 'src/utils.py' y crea un archivo 'src/utils_test.py' "
    "con tests unitarios básicos para todas las funciones que encuentres."
)

print(respuesta)

El agente ejecutará varios pasos sin intervención: leerá utils.py, analizará las funciones, generará los tests y escribirá el archivo. Si algo falla en algún paso, el resultado de la herramienta se lo indica y puede corregir su enfoque en la siguiente iteración.


7. Tokens y costes reales

gpt-4o-mini tiene un precio de $0.15/M tokens de entrada y $0.60/M de salida —uno de los más bajos disponibles para un modelo con capacidad de function calling.

Un loop de agente típico con 3-4 llamadas a herramientas consume aproximadamente 2,000-4,000 tokens en total. A esos precios, son menos de $0.001 por tarea completa. Puedes correr miles de tareas por dólar.

Para estimar antes de lanzar en producción:

import tiktoken

encoding = tiktoken.encoding_for_model("gpt-4o-mini")

def count_tokens(messages: list) -> int:
    total = 0
    for msg in messages:
        content = msg.get("content") or ""
        total += len(encoding.encode(content))
    return total

# Útil para implementar ventanas de contexto: si el historial supera
# un umbral de tokens, resumir las conversaciones más antiguas
print(f"Tokens actuales en el historial: {count_tokens(agent.messages)}")

8. El patrón que escala

Lo que construiste es la base de cualquier sistema de agentes, por complejo que sea. Los frameworks como LangChain o CrewAI implementan exactamente este patrón —loop + memoria + herramientas— con más abstracciones encima para manejar múltiples agentes, flujos condicionales y observabilidad.

Entenderlo desde cero tiene dos ventajas prácticas:

  • Depuración: cuando un agente falla, puedes imprimir agent.messages y ver exactamente qué ocurrió en cada paso. Sin framework de por medio, no hay magia que oculte el estado.
  • Control: puedes modificar el loop para añadir lógica específica —timeout por herramienta, validación del output, logging— sin depender de que el framework lo soporte.

El video termina mencionando los sistemas multiagente como siguiente paso natural: múltiples instancias de Agent, cada una con herramientas distintas, coordinadas por un agente orquestador. El patrón es el mismo; la complejidad está en la coordinación, no en el loop.


Próximos pasos

Una vez que el agente básico funciona, estos son los temas que más valor añaden en el orden en que el video los sugiere:

  • Más herramientas: conectar el agente a APIs externas (búsqueda web, base de datos, servicios propios) siguiendo el mismo patrón de tools + execute_tool
  • Streaming: mostrar la respuesta del modelo palabra a palabra con stream=True para mejor experiencia en terminal
  • Sistemas multiagente: un agente orquestador que delega subtareas a agentes especializados según el tipo de herramientas que necesitan
  • Responses API: OpenAI recomienda migrar a la Responses API para nuevos proyectos, especialmente si el agente necesita herramientas integradas como búsqueda web o intérprete de código sin implementarlas manualmente

El repositorio openai-cookbook en GitHub tiene ejemplos de producción para cada uno de estos casos.

#python#openai#api#programacion#ia#tutorial#agentes#function-calling

Notas de la comunidad

¿Te fue útil este contenido?

Tamaño de lectura