DEV Community

Cover image for Cómo crear un agente de IA desde cero. Open source, local y gratis.
Hazel Saenz for AWS

Posted on • Originally published at builder.aws.com

Cómo crear un agente de IA desde cero. Open source, local y gratis.

De un modelo que solo responde a uno que busca, razona y recuerda con código que puedes leer, forkear y contribuir.


Todos hemos usado ChatGPT, Claude, Google Gemini. Les preguntas algo, te responden. Les pides que resuman un texto, lo resumen. Les dices que escriban un correo, lo escriben. Pero hay un límite. Un modelo de lenguaje puede hablar de música. Puede hablar de géneros. Puede hablar de artistas.

Pero no puede buscar en tu biblioteca de canciones.
No puede analizar el mood de una playlist.
No puede recordar que la semana pasada te armó una playlist de hip-hop para el gym y te encantó.

Un modelo solo habla.
Un agente hace cosas.

Y esa diferencia es más importante de lo que parece.

¿Qué es un agente de IA?

Si ya usaste la API de OpenAI o jugaste con modelos en Hugging Face, sabes que un LLM recibe texto y genera texto. Punto. No ejecuta código, no consulta bases de datos, no llama APIs.

Un agente agrega dos cosas encima de eso:

  1. Herramientas (tools): funciones que el modelo puede invocar cualquier función de Python que le expongas
  2. Un agent loop: un ciclo donde el modelo razona, decide si necesita llamar una herramienta, la ejecuta, recibe el resultado, y vuelve a razonar hasta tener una respuesta final

No es magia. Es un while loop con tool dispatch.

Imagina que le preguntas a un agente DJ: "Armame una playlist para una cena tranquila con amigos."

Sin tools, el modelo genera texto basado en su entrenamiento, no conoce tu biblioteca, no sabe qué se lanzó la semana pasada (su conocimiento tiene fecha de corte), y no tiene idea de tus gustos. Con tools, hace esto:

  1. Razona: "Necesito buscar canciones con energía baja"
  2. Invoca: buscar_canciones(mood="chill", genero="jazz")
  3. Recibe el resultado: una lista de canciones reales de tu biblioteca
  4. Razona de nuevo: "Debería verificar que la energía sea consistente"
  5. Invoca: analizar_energia(canciones=[...])
  6. Genera la respuesta final: una playlist curada con datos reales

El modelo decide qué herramientas invocar y en qué orden. Tú no escribes la orquestación, el modelo la resuelve en runtime. Eso es el enfoque model-driven, y es lo que diferencia a un agente de un pipeline hardcodeado con if/else.

Agent Loop — el ciclo de razonamiento de un agente

Por qué open source importa (de verdad)

Cuando hablamos de agentes de IA, hay muchas opciones. Frameworks propietarios, APIs cerradas, SDKs que solo funcionan con un proveedor específico.

El problema no es que existan. El problema es lo que pierdes cuando los usas:

  • No puedes leer la implementación del agent loop, si el modelo toma una secuencia de tool calls inesperada, no puedes entender por qué ni cambiar el comportamiento
  • No puedes modificar el comportamiento del loop cuando algo no funciona como esperas
  • Si tu proveedor de modelo no está soportado, no puedes contribuir la integración tú mismo ni aprovechar una que la comunidad ya haya creado
  • No puedes contribuir fixes o mejoras que beneficien a otros

Open source no es solo "código gratis". Sí, requiere más responsabilidad de tu parte: tú debuggeas, tú actualizas, tú decides. Pero esa responsabilidad viene con control total. Es transparencia. Es poder abrir el source del agent loop y ver que es un while loop con tool dispatch. Es poder poner un breakpoint en el ciclo de razonamiento cuando tu agente hace algo inesperado.

Y eso es exactamente lo que vamos a usar.

El stack: Python + Strands Agents + Ollama

Todo lo que vas a ver en este artículo corre con tres cosas:

  • Python: el lenguaje (licencia PSF, open source)
  • Strands Agents: un SDK open source (licencia Apache 2.0) para construir agentes de IA
  • Ollama: un runtime open source (licencia MIT) para correr modelos de lenguaje localmente

Todo open source. Todo en tu laptop. Sin API keys. Sin vendor lock-in. Sin datos saliendo de tu máquina.

Strands Agents toma un enfoque model-driven: tú defines las herramientas como funciones de Python, escribes un system prompt, y el agent loop se encarga de la ejecución. Sin definiciones de pasos, sin grafos de workflow. Solo código.

Las 4 capas que vamos a construir

Ollama: modelos de IA en tu laptop

Antes de construir el agente, necesitamos un modelo de lenguaje corriendo localmente. Y aquí es donde entra Ollama.

Ollama es un runtime open source para correr LLMs en tu máquina. Si conoces Docker, la analogía es directa: ollama pull descarga un modelo, ollama serve lo expone como API REST en localhost:11434. Sin cuentas, sin API keys, sin datos saliendo de tu red.

Instalación

En macOS y Linux:

curl -fsSL https://ollama.com/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

En Windows, descarga el instalador desde ollama.com/download.

Descargar y correr un modelo

Para este artículo vamos a usar llama3.1:8b — tiene buen soporte para tool-calling (que es lo que necesitamos para que el agente invoque funciones) y maneja español razonablemente bien:

ollama pull llama3.1
Enter fullscreen mode Exit fullscreen mode

Son unos 4.9 GB. Solo necesitas hacerlo una vez.

Ahora levanta el servidor:

ollama serve
Enter fullscreen mode Exit fullscreen mode

Nota: Si instalaste la app de macOS, el servidor ya está corriendo en segundo plano y no necesitas ejecutar ollama serve. Solo es necesario si instalaste con Homebrew o en Linux.

Ollama queda escuchando en http://localhost:11434. Puedes verificar con un curl http://localhost:11434/api/tags para ver los modelos disponibles.

Ollama soporta muchos modelos: Llama, Mistral, Phi, Gemma, y más. Si tu máquina tiene GPU, los corre acelerados automáticamente. Si no, usa CPU (más lento, pero funciona).

Lo importante: el modelo corre en tu máquina. Tus datos no salen de tu laptop. Y el código de Ollama es open source.

Capa 1: Un agente que solo habla

Empecemos por lo más simple. Un agente que es solo un modelo + un prompt.

Primero, instala el SDK de Strands con soporte para Ollama.

Nota: Strands Agents requiere Python 3.10 o superior. El Python que viene preinstalado en macOS (3.9.6) no es compatible. Si no tienes una versión reciente, instálala con brew install python@3.13.

Crea un entorno virtual para no contaminar tu sistema. Con pip:

python3 -m venv .venv
source .venv/bin/activate
pip install 'strands-agents[ollama]'
Enter fullscreen mode Exit fullscreen mode

O si usas uv (más rápido y gestiona Python por ti):

uv venv --python 3.13
source .venv/bin/activate
uv pip install 'strands-agents[ollama]'
Enter fullscreen mode Exit fullscreen mode

Ahora, el código:

from strands import Agent
from strands.models.ollama import OllamaModel

modelo = OllamaModel(
    model_id="llama3.1",
    host="http://localhost:11434",
)

dj = Agent(
    model=modelo,
    system_prompt="""Eres un DJ y curador musical experto.
    Respondes en español, con onda y buen gusto.
    Recomiendas música basándote en el mood, la ocasión, y los gustos del usuario."""
)

dj("Recomiéndame algo para escuchar mientras programo")
Enter fullscreen mode Exit fullscreen mode

Cinco líneas relevantes. Eso es un agente.

Bueno, técnicamente es un modelo con un prompt — todavía no tiene herramientas, así que solo responde con su conocimiento general. Si le pides una playlist con canciones de tu biblioteca, va a alucinar títulos que suenan razonables pero no existen.

Un agente es un modelo + un prompt + un loop. Eso es todo.

Y fíjate: el modelo corre local. No hay API key, no hay vendor, no hay datos saliendo de tu laptop.

Capa 2: Dale herramientas — la biblioteca de canciones

El agente básico no conoce tu música. Sabe de artistas y géneros en general porque fue entrenado con texto de internet. Pero no sabe que en tu biblioteca tienes ese álbum de la Ley que siempre pones cuando cocinas.

Para eso necesita una herramienta.

Pero primero, necesitamos datos. Crea un archivo data/canciones.json con tu biblioteca de canciones. Cada canción necesita: titulo, artista, genero, mood, energia (0-100), y duracion_min. Aquí tienes un ejemplo para arrancar:

Ver ejemplo de data/canciones.json (30 canciones)

[
  {"titulo": "So What", "artista": "Miles Davis", "genero": "jazz", "mood": "chill", "energia": 30, "duracion_min": 9.3},
  {"titulo": "Take Five", "artista": "Dave Brubeck", "genero": "jazz", "mood": "chill", "energia": 35, "duracion_min": 5.4},
  {"titulo": "Blue in Green", "artista": "Miles Davis", "genero": "jazz", "mood": "melancólico", "energia": 20, "duracion_min": 5.4},
  {"titulo": "Fly Me to the Moon", "artista": "Frank Sinatra", "genero": "jazz", "mood": "chill", "energia": 40, "duracion_min": 3.5},
  {"titulo": "Feeling Good", "artista": "Nina Simone", "genero": "jazz", "mood": "energético", "energia": 55, "duracion_min": 2.9},
  {"titulo": "Bohemian Rhapsody", "artista": "Queen", "genero": "rock", "mood": "energético", "energia": 80, "duracion_min": 5.9},
  {"titulo": "Hotel California", "artista": "Eagles", "genero": "rock", "mood": "chill", "energia": 45, "duracion_min": 6.5},
  {"titulo": "Smells Like Teen Spirit", "artista": "Nirvana", "genero": "rock", "mood": "energético", "energia": 90, "duracion_min": 5.0},
  {"titulo": "Under Pressure", "artista": "Queen & David Bowie", "genero": "rock", "mood": "energético", "energia": 75, "duracion_min": 4.0},
  {"titulo": "Creep", "artista": "Radiohead", "genero": "rock", "mood": "melancólico", "energia": 50, "duracion_min": 3.9},
  {"titulo": "Gasolina", "artista": "Daddy Yankee", "genero": "reggaetón", "mood": "fiesta", "energia": 95, "duracion_min": 3.1},
  {"titulo": "Dákiti", "artista": "Bad Bunny & Jhay Cortez", "genero": "reggaetón", "mood": "fiesta", "energia": 80, "duracion_min": 3.3},
  {"titulo": "Tití Me Preguntó", "artista": "Bad Bunny", "genero": "reggaetón", "mood": "fiesta", "energia": 85, "duracion_min": 4.0},
  {"titulo": "Pepas", "artista": "Farruko", "genero": "reggaetón", "mood": "fiesta", "energia": 92, "duracion_min": 4.5},
  {"titulo": "La Bicicleta", "artista": "Shakira & Carlos Vives", "genero": "reggaetón", "mood": "fiesta", "energia": 78, "duracion_min": 3.8},
  {"titulo": "Midnight City", "artista": "M83", "genero": "electrónica", "mood": "energético", "energia": 75, "duracion_min": 4.0},
  {"titulo": "Strobe", "artista": "Deadmau5", "genero": "electrónica", "mood": "chill", "energia": 55, "duracion_min": 10.3},
  {"titulo": "Around the World", "artista": "Daft Punk", "genero": "electrónica", "mood": "fiesta", "energia": 82, "duracion_min": 7.1},
  {"titulo": "Intro", "artista": "The xx", "genero": "electrónica", "mood": "chill", "energia": 25, "duracion_min": 2.1},
  {"titulo": "Teardrop", "artista": "Massive Attack", "genero": "electrónica", "mood": "melancólico", "energia": 35, "duracion_min": 5.3},
  {"titulo": "Disorder", "artista": "Joy Division", "genero": "indie", "mood": "melancólico", "energia": 60, "duracion_min": 3.6},
  {"titulo": "Do I Wanna Know?", "artista": "Arctic Monkeys", "genero": "indie", "mood": "chill", "energia": 55, "duracion_min": 4.6},
  {"titulo": "Somebody Else", "artista": "The 1975", "genero": "indie", "mood": "melancólico", "energia": 45, "duracion_min": 5.7},
  {"titulo": "Electric Feel", "artista": "MGMT", "genero": "indie", "mood": "energético", "energia": 72, "duracion_min": 3.8},
  {"titulo": "Tongue", "artista": "MNEK", "genero": "indie", "mood": "energético", "energia": 68, "duracion_min": 3.5},
  {"titulo": "Only Shallow", "artista": "My Bloody Valentine", "genero": "shoegaze", "mood": "energético", "energia": 70, "duracion_min": 4.2},
  {"titulo": "When You Sleep", "artista": "My Bloody Valentine", "genero": "shoegaze", "mood": "chill", "energia": 45, "duracion_min": 4.1},
  {"titulo": "Alison", "artista": "Slowdive", "genero": "shoegaze", "mood": "melancólico", "energia": 40, "duracion_min": 5.1},
  {"titulo": "Cherry-coloured Funk", "artista": "Cocteau Twins", "genero": "shoegaze", "mood": "chill", "energia": 38, "duracion_min": 4.5},
  {"titulo": "Vapour Trail", "artista": "Ride", "genero": "shoegaze", "mood": "energético", "energia": 65, "duracion_min": 4.2}
]
Enter fullscreen mode Exit fullscreen mode

Puedes modificar este archivo con tu propia música. Lo importante es que mantenga la misma estructura.

Ahora sí, en Strands, crear una herramienta es decorar una función de Python:

from strands import Agent, tool
from strands.models.ollama import OllamaModel
import json

# Cargar biblioteca local de canciones
with open("data/canciones.json") as f:
    BIBLIOTECA = json.load(f)

@tool
def buscar_canciones(genero: str = "", mood: str = "", artista: str = "") -> str:
    """Busca canciones en la biblioteca musical del usuario.

    Args:
        genero: Género musical (ej: rock, jazz, reggaetón, electrónica)
        mood: Estado de ánimo o energía (ej: chill, fiesta, melancólico, energético)
        artista: Nombre del artista o banda
    """
    resultados = BIBLIOTECA
    if genero:
        resultados = [c for c in resultados if genero.lower() in c["genero"].lower()]
    if mood:
        resultados = [c for c in resultados if mood.lower() in c["mood"].lower()]
    if artista:
        resultados = [c for c in resultados if artista.lower() in c["artista"].lower()]

    if not resultados:
        return "No encontré canciones con esos criterios en tu biblioteca."

    return json.dumps(resultados[:10], ensure_ascii=False, indent=2)
Enter fullscreen mode Exit fullscreen mode

Fíjate en el decorador @tool. Convierte cualquier función de Python en una herramienta que el agente puede invocar. El docstring no es decorativo — el SDK lo parsea para generar un tool spec (JSON schema) que es lo que el modelo recibe para decidir cuándo y cómo usar la herramienta. Si el docstring es vago, el modelo no va a saber cuándo llamarla.

Ahora conectamos la herramienta al agente:

modelo = OllamaModel(model_id="llama3.1", host="http://localhost:11434")

dj = Agent(
    model=modelo,
    system_prompt="""Eres un DJ y curador musical experto.
    Usa la herramienta buscar_canciones para encontrar música en la biblioteca del usuario.
    Siempre basa tus recomendaciones en canciones que el usuario realmente tiene.""",
    tools=[buscar_canciones]
)

dj("Quiero escuchar jazz mientras trabajo")
Enter fullscreen mode Exit fullscreen mode

Ahora cuando le pides jazz para trabajar, el agente no inventa. Llama a buscar_canciones(genero="jazz", mood="chill"), obtiene canciones reales de tu biblioteca, y arma una recomendación con lo que realmente tienes.

Pasó de "saber cosas" a "hacer cosas".

Y el sistema de tools es extensible. Existe un paquete de community tools donde cualquiera puede publicar los suyos. ¿Tienes una API que quieres conectar? Escribe un @tool y compártelo.

Capa 3: Más herramientas, más inteligencia

Un tool está bien. Pero la cosa se pone interesante cuando agregas varios.

@tool
def analizar_energia(canciones: list) -> str:
    """Analiza el nivel de energía promedio de una lista de canciones y sugiere el orden ideal.

    Args:
        canciones: Lista de nombres de canciones a analizar
    """
    energia_map = {}
    for cancion in BIBLIOTECA:
        energia_map[cancion["titulo"].lower()] = cancion.get("energia", 50)

    analisis = []
    for titulo in canciones:
        energia = energia_map.get(titulo.lower(), 50)
        analisis.append({"titulo": titulo, "energia": energia})

    # Ordenar por energía para un flow natural
    analisis.sort(key=lambda x: x["energia"])
    promedio = sum(c["energia"] for c in analisis) / len(analisis) if analisis else 0

    return json.dumps({
        "energia_promedio": round(promedio),
        "flow": "ascendente" if analisis[0]["energia"] < analisis[-1]["energia"] else "descendente",
        "orden_sugerido": [c["titulo"] for c in analisis],
        "nota": "Energía baja → alta para ir subiendo el mood" if promedio < 60 else "Playlist con buena energía"
    }, ensure_ascii=False)

@tool
def duracion_playlist(canciones: list) -> str:
    """Calcula la duración total de una playlist y sugiere si necesita más canciones.

    Args:
        canciones: Lista de nombres de canciones
    """
    duracion_map = {}
    for cancion in BIBLIOTECA:
        duracion_map[cancion["titulo"].lower()] = cancion.get("duracion_min", 3.5)

    total = sum(duracion_map.get(t.lower(), 3.5) for t in canciones)

    return json.dumps({
        "canciones": len(canciones),
        "duracion_total_min": round(total, 1),
        "duracion_formato": f"{int(total // 60)}h {int(total % 60)}min" if total >= 60 else f"{int(total)}min",
        "sugerencia": "Playlist corta, podrías agregar más canciones" if total < 30 else "Buena duración"
    }, ensure_ascii=False)
Enter fullscreen mode Exit fullscreen mode

Ahora el agente tiene tres herramientas. Y aquí es donde se ve el agent loop en acción:

dj = Agent(
    model=modelo,
    system_prompt="""Eres un DJ y curador musical experto.
    Usa tus herramientas para armar playlists basadas en la biblioteca real del usuario.
    Considera el mood, la energía, y la duración para crear una experiencia coherente.""",
    tools=[buscar_canciones, analizar_energia, duracion_playlist]
)

dj("Armame una playlist de una hora para una fiesta en casa")
Enter fullscreen mode Exit fullscreen mode

El agente no ejecuta las herramientas en un orden predefinido. No hay un pipeline que diga "primero busca, luego analiza, luego calcula duración". El modelo recibe los tool specs (nombre, descripción, parámetros) y en cada iteración del loop decide: ¿necesito más información? ¿Qué tool me la da? ¿Con qué parámetros?

En este caso: llama a buscar_canciones(mood="fiesta"), analiza la energía con analizar_energia(canciones=[...]) para ordenarlas con un flow ascendente, y verifica la duración con duracion_playlist(canciones=[...]) para cubrir la hora completa. Si la duración no alcanza, puede volver a llamar buscar_canciones con otros criterios.

Tú no orquestaste nada. El modelo resolvió la secuencia en runtime.

Nota sobre modelos locales: con llama3.1:8b, el agente invoca los tools correctamente pero a veces ignora los resultados y alucina canciones que no están en tu biblioteca. Esto es una limitación del tamaño del modelo (8B parámetros), no del SDK. Con una sola herramienta (Capa 2) funciona bien; con múltiples tools el modelo se confunde más fácilmente. Si necesitas tool-calling más confiable, usa un modelo más grande (llama3.1:70b) o un modelo cloud como Claude o GPT-4 (ver la sección "De tu laptop a la nube").

Y si quieres entender cómo funciona por dentro, el código está ahí. El agent loop de Strands no es una caja negra. Es código abierto que puedes leer en GitHub.

Capa 4: Memoria — el DJ te conoce

Hay un problema con todo lo que hemos construido hasta ahora.

Cada conversación empieza de cero.

Le dices "me encanta el indie rock", cierras la conversación, vuelves al día siguiente, y el agente no tiene idea de quién eres. Es como ir a un bar donde el DJ cambia cada vez que parpadeas.

Strands resuelve esto con session management. FileSessionManager persiste el historial de conversación a disco como archivos JSON, organizados por sesión y agente. Cuando el agente se inicializa con un session_id existente, carga los mensajes anteriores y el modelo los tiene como contexto.

from strands import Agent
from strands.models.ollama import OllamaModel
from strands.session.file_session_manager import FileSessionManager

modelo = OllamaModel(model_id="llama3.1", host="http://localhost:11434")

# El session manager guarda las conversaciones a disco
# session_id identifica al usuario, storage_dir es donde se guardan los archivos
session_manager = FileSessionManager(session_id="usuario-1", storage_dir="./sesiones")

dj = Agent(
    model=modelo,
    system_prompt="""Eres un DJ y curador musical experto.
    Recuerdas los gustos del usuario entre conversaciones.
    Si el usuario ya te dijo qué le gusta, úsalo para personalizar tus playlists.""",
    tools=[buscar_canciones, analizar_energia, duracion_playlist],
    session_manager=session_manager,
)

# Primera conversación
dj("Me encanta el indie rock y el shoegaze. No soporto el reggaetón.")

# ... tiempo después, otra conversación ...

# El agente recuerda
dj("Armame algo para el viernes")
# → Arma una playlist de indie rock y shoegaze, sin reggaetón
Enter fullscreen mode Exit fullscreen mode

Pocas líneas y tu agente tiene continuidad.

Y aquí viene lo interesante desde el punto de vista open source: FileSessionManager es una implementación de la interfaz SessionManager. Guarda sesiones a disco como archivos JSON organizados por sesión y agente. Pero si quieres guardar sesiones en Redis, en Postgres, o en S3, implementas la interfaz y listo. Ya existen session managers de la comunidad, como el de AgentCore Memory. La arquitectura está diseñada para que la comunidad la extienda.

De tu laptop a la nube (en una línea)

Todo lo que hemos construido corre localmente. Ollama en tu máquina, modelo en tu máquina, datos en tu máquina.

Pero si quieres compartir tu agente con el mundo, necesitas algo más.

Y aquí está lo elegante de la arquitectura: el SDK abstrae el model provider detrás de una interfaz común. Cambiar de proveedor es cambiar la instanciación del modelo — el resto del código no se entera.

# Local con Ollama
from strands.models.ollama import OllamaModel
modelo = OllamaModel(model_id="llama3.1", host="http://localhost:11434")

# En la nube con Amazon Bedrock
from strands.models import BedrockModel
modelo = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0", region_name="us-east-1")
Enter fullscreen mode Exit fullscreen mode

El resto del código del agente no cambia. Ni una línea. Los tools, el system prompt, el session manager: todo igual. Esto es posible porque Strands define una interfaz Model que todos los providers implementan — OllamaModel, BedrockModel, OpenAIModel, AnthropicModel, etc.

Esa es la arquitectura en capas:

Arquitectura en capas — tu código, el SDK, y el provider

La capa open source es la constante. El proveedor de abajo es la variable. Ollama, Bedrock, OpenAI, Anthropic, Google Gemini, LiteLLM: todos son plugins que implementan la misma interfaz. Sin esa capa de abstracción, estás atado al SDK del vendor con el que empezaste.

Lo que aprendiste

Un agente de IA no es magia. Es un modelo + herramientas + un loop de razonamiento.

Construimos un DJ de playlists capa por capa: primero un agente que solo habla, luego le dimos herramientas para buscar en una biblioteca real de canciones, después agregamos más tools y vimos cómo el modelo decide solo qué llamar y en qué orden, le dimos memoria para que recuerde tus gustos, y al final mostramos que el mismo código puede correr local o en la nube cambiando una línea.

Todo con Python, Strands Agents y Ollama. Todo open source. Todo en tu laptop.

Qué sigue

Si quieres ir más allá:

El DJ sabe de música. ¿Qué vas a construir tú?


¿Te resultó útil este artículo? Compártelo con tu equipo o déjame saber en los comentarios qué agente te gustaría construir. Y si ya estás experimentando con Strands o con agentes de IA en general, me encantaría escuchar tu experiencia.

Top comments (0)