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:
- Herramientas (tools): funciones que el modelo puede invocar cualquier función de Python que le expongas
- 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:
- Razona: "Necesito buscar canciones con energía baja"
-
Invoca:
buscar_canciones(mood="chill", genero="jazz") - Recibe el resultado: una lista de canciones reales de tu biblioteca
- Razona de nuevo: "Debería verificar que la energía sea consistente"
-
Invoca:
analizar_energia(canciones=[...]) - 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.
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.
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
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
Son unos 4.9 GB. Solo necesitas hacerlo una vez.
Ahora levanta el servidor:
ollama serve
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]'
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]'
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")
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}
]
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)
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")
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)
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")
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
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")
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:
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á:
- Documentación de Strands Agents — guías, ejemplos, y API reference
- Repo de Strands en GitHub — el código fuente completo, licencia Apache 2.0
- Community tools — herramientas creadas por la comunidad que puedes usar y extender
- Ollama — para correr modelos localmente
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)