Hace tiempo vengo consumiendo cursos de orquestación de agentes de distintos frameworks y me propuse navegar por los diferentes patrones de Strands Agents de complejidad media, ejecutados localmente (con la idea de encarar más adelante el deploy en AWS). Este artículo está dedicado a Agents-as-Tools, corriendo con Ollama en una laptop con solo 16GB de RAM 💻.
Introducción
Para desarrollar y comprender agentes con el framework Strands Agents te propongo hacer un curso de fundamentos o leer su documentación (que es extensa). Los fundamentos son importantes! Es primordial conocer cómo dar complejidad a nuestros agentes, no esperar delegar toda la responsabilidad del diseño a la IA (recursos al pie de estas notas).
Para que la prueba de concepto se pueda ejecutar localmente, utilicé Ollama + el pequeño modelo gemma4:e2b-it-qat (que pesa poco mas de 4gb) ejecutando en Docker.
El caso de uso que tomé para desarrollar el patrón es la automatización del soporte básico de TI:
🛠 Se recibe un ticket de soporte y se determina si es un ticket de facturación, técnico o seguridad. Luego se envía al agente especializado para que lo resuelva.
Conceptos claves
➡ Agents As Tools es un patrón de orquestación multiagente que como se describe en la documentación es jerárquico (hub and spooke), donde un orquestador delega tareas a agentes especializados que tienen diferentes tools para resolver. El orquestador y cada agente poseen contexto aislado, no se contaminan entre sí.
Para clarificar el funcionamiento: al delegar, hay dos agents loops anidados, no uno compartido: el orquestador no "pausa" su razonamiento en espera del especialista, simplemente visualiza una tool call que devuelve texto, por detrás, esa tool call disparó un loop completo e independiente que ya terminó antes de volver.
Orchestrator loop
└─ llama billing_agent(query) → dispara un loop nuevo, aislado
└─ Billing loop: check_invoice_status → process_refund → respuesta final
← recibe solo el string final (no ve las tool calls internas)
└─ continúa su propio loop: delega a otro especialista o responde al usuario
➡ @tool no distingue entre operación simple o integración vía APis y agente completo , el patrón Agents-as-Tools explota que un agente cumpla la misma interfaz que una tool (recibe input, devuelve output), para que se pueda componer recursivamente. Es lo que permite anidar agentes dentro de agentes sin que el nivel superior necesite saber la complejidad interna del nivel inferior.
⭐ La descripción (docstring) de cada función @tool no es un comentario decorativo: es el input que el LLM orquestador lee en tiempo de ejecución para decidir a qué especialista delegar. Distinto del parámetro description= del propio Agent, que es metadata informativa y no interviene en el routing.
➡ Memoria del orquestador (ConversationManager). Utilicé el concepto de compresión al hacer que el agente recuerde los últimos 10 mensajes o tickets de la sesión para mantener contexto entre interacciones consecutivas con SlidingWindowConversationManager(window_size=10)
Mantiene los mensajes en una lista dentro del objeto Python en RAM durante la ejecución del proceso.
➡ Hook de dominio (SteeringHandler). Implementa el "HookProvider" de Strands, en este caso de uso fuerza el orden de pasos del protocolo de respuesta ante incidentes de Seguridad. Si el LLM intenta llamar por ejemplo a revoke_access_token sin haber auditado primero la IP, el hook lo bloquea con event.cancel_tool antes de que la función se ejecute. El framework impone la lógica de negocio determinista, sin depender del criterio del modelo. Describe con precisión los tres eventos BeforeInvocationEvent, AfterToolCallEvent, BeforeToolCallEvent
Los hooks sirven en general para:
- Prevenir loops infinitos o excesivos de tool calls (control de costos/tokens)
- Guardrails de seguridad/negocio (ej. no permitir más de n transacciones, n consultas a una API con rate limit, etc.)
- Es el mecanismo estándar de Strands para inyectar lógica de control sin modificar el core del agent loop.
➡ Evaluaciones (Evals): evaluación de comportamiento del agente (con Contains, no asserts tradicionales).
Algunas consideraciones que tomé para poder ejecutar los evals sin que se cuelguen en un entorno local con el modelo en ollama: Desactivé OTLP de Opentelemetry, este si internamente no alcanza un endpoint (localhost:4318) se cuelga indefinidamente.
En el caso de uso del repositorio se hacen evaluaciones deterministas ya que se utiliza solo un LLM, no se abarcan todas las evaluaciones pero sí dos importantes descritas debajo.
Output Evaluation. Evalúa el resultado final (el texto que recibe el usuario), sin importar el camino interno que tomó el agente para llegar ahí. Es un chequeo determinista de keyword matching: la respuesta debe contener ciertas palabras clave esperadas (INV-1002, USR-884, etc.).
Trajectory Evaluation. No evalúa qué respondió el agente, sino cómo llegó ahí, qué tools/sub-agentes invocó durante su razonamiento. En el código, extract_called_tools() inspecciona agent.messages buscando bloques toolUse, y evaluate_trajectory() verifica que el sub-agente esperado (ej. BillingAgent) haya sido efectivamente el delegado. Para la ejecución en entorno local con Ollama, la función extract_called_tools extrae directamente los nombres de las herramientas en agent.messages y comprueba en milisegundos si se invocó a case.expected_agent, sin necesidad de instanciar un modelo juez adicional ni hacer peticiones extra a Bedrock/Ollama (sin cuelgues asíncronos).
⭐ PASS / FAIL a nivel dimensión, en el output: cada EvalCase se marca como aprobado (case_passed) solo si ambas dimensiones pasan (traj_eval.test_pass and out_eval.test_pass). Un agente puede delegar al especialista correcto (trayectoria OK) pero responder sin las keywords esperadas (output FAIL), y el caso se marca como fallido igual. Una sola dimensión de evaluación no alcanza: el agente puede "hacer lo correcto" pero "comunicarlo mal", o viceversa.
Descripción de la solución
📦 Repositorio GitHub: github.com/reinalau/strands-agents-as-tools
Para alojar el LLM utilicé Docker Desktop, luego descargué la imagen de Ollama e instalé gemma4:e2b-it-qat (que pesa poco mas de 4GB). Todos los modelos gemma los podés encontrar aquí pero este en particular funciona OK en una laptop con 16GB de RAM.
Como condición necesaria asegurate de dar memoria suficiente a Docker.
# Levantar el servidor de Ollama con un volumen persistente
docker run -d --name ollama -p 11434:11434 -v ollama_data:/root/.ollama ollama/ollama
# Descargar el modelo
docker exec -it ollama ollama pull gemma4:e2b-it-qat
# Probar que el modelo responde
docker exec -it ollama ollama run gemma4:e2b-it-qat
# Verificar que el modelo está corriendo
docker exec -it ollama ollama ps
Importante! Testear el modelo gemma 4 en la terminal haciendo alguna pregunta para corroborar que responda.
El código del multiagente está en Python y la estructura del proyecto se diseñó de manera que sea más explicativo su lógica:
strands-agents-as-tools
├── README.md
├── requirements.txt
├── .env.example
├── .gitignore
│
├── src/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ │
│ ├── agents/
│ │ ├── __init__.py
│ │ ├── orchestrator.py
│ │ ├── billing_agent.py
│ │ ├── technical_agent.py
│ │ └── security_agent.py
│ │
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── billing_tools.py
│ │ ├── technical_tools.py
│ │ └── security_tools.py
│ │
│ └── hooks/
│ ├── __init__.py
│ └── security_verification_hook.py # SteeringHandler de dominio
│
├── examples/
│ └── sample_tickets.py
│
├── evals/
│ ├── __init__.py
│ ├── eval_cases.py # Casos con inputs, agent esperado y keywords
│ └── run_evals.py # Evaluador dual: Trayectoria (Tool Call) + Output (Keywords)
│
└── tests/
└── test_agents.py # Tests unitarios de tools (sin LLM)
Ejecución local
Una vez que tenemos el repositorio del código fuente clonado y el docker de ollama con el modelo gemma 4 ejecutando, pasamos armar el ambiente.
Los requerimientos que vamos a encontrar en requirements.txt:
strands-agents
strands-agents-evals
ollama
python-dotenv
pytest
pip install -r requirements.txt
En el .env vamos a necesitar:
OLLAMA_HOST=http://localhost:11434
MODEL_NAME=gemma4:e2b-it-qat
LOG_LEVEL=INFO
cp .env.example .env
Los test son pruebas unitarias deterministas (tests/). Permite validar rápidamente y de forma aislada la lógica de código puro de las herramientas (tools) de negocio (facturación, soporte técnico y seguridad), sin requerir inferencia de LLM. Garantizan que las funciones auxiliares retornen los estados y formatos esperados antes de ser expuestas como capacidades @tool al sistema multiagente.
python -m pytest tests/
Procedemos a ejecutar el flujo principal demostrativo donde se ve al orquestador delegar tickets a los especialistas procesados por Ollama. Puede ser algo lento debido al modelo de ollama elegido pero finalmente termina el flujo con los ejemplos de tickets. Se utilizan ejemplos de soportes dentro de main con from examples.sample_tickets import SAMPLE_TICKETS ...te invito a cambiarlos!
python -m src.main
Por último, hacemos una evaluación de pruebas de calidad y precisión ejecutadas sobre el agente real con el concepto de "Evals". Esto es importante para todos los agentes futuros que construyas: hay que tomar la documentación de Strands y descubrir cómo y para qué tenemos que evaluar nuestros agentes.
También recomiendo establecer OTEL_SDK_DISABLED=true para desactivar el recolector de telemetría OpenTelemetry (explicado en Conceptos Claves)
export OTEL_SDK_DISABLED="true"
python -m evals.run_evals
Este último script ejecuta dos metodologías de evaluación según las especificaciones de Strands Agents: Output / Keyword Evaluation y Tool-Call Trajectory Evaluation (explicado en Conceptos Claves)
📝 Nota: Todo la explicación del código del repositorio y paso a paso de la ejecución se encuentra en el README.md
Conclusiones
Implementar Agents-as-Tools con un modelo local vía Ollama fue una buena forma de entender el patrón de orquestación sin gastar, pero también descubrí que modelos chicos como gemma4:e2b-it-qat pueden ser inconsistentes en su razonamiento (llamadas duplicadas a una misma tool, dudas antes de delegar) a diferencia de un modelo más grande (Bedrock/Claude/GPT-4) que probablemente sea más preciso. Esto no invalida el patrón, todo lo contrario: reafirma por qué los guardrails deterministas (el hook de seguridad) y las evaluaciones automáticas es la forma de detectar el comportamiento errático antes de que llegue a producción.
Limitaciones del ejemplo
Este repositorio es educativo, y tiene varias simplificaciones:
-
Tools hardcodeadas con datos mock:
check_invoice_status,check_system_status,audit_ip_address, etc. devuelven datos fijos en el código, no consultan ningún sistema real. - Un solo LLM local: no se probó el comportamiento con otros modelos para comparar consistencia de routing/tool-calling entre modelos de distinto tamaño.
- Evaluación: solo se cubrieron Output y Trajectory de forma determinista; no se probó chaos testing, LLM-as-judge, ni simulación multi-turno (mirar la documentación oficial).
Pensando en este mismo Multiagente productivo 🤯
Conectar tools a APIs/sistemas reales: reemplazar los mocks por llamadas a una API de facturación real, un endpoint de status de infraestructura (ej. AWS CloudWatch, Datadog), y un servicio real de gestión de identidad (ej. IAM, Okta) para revocación de tokens. Varios de estos ya existen como servidores MCP mantenidos por la comunidad/AWS (conectarlos vía
MCPClientsería más rápido que escribir cada tool a mano).Persistencia real de sesión: en este ejemplo cada ticket es stateless entre ejecuciones de
main.py; sumar unSessionManager(FileSessionManageroDynamoDBSessionManager) para simular conversaciones multi-turno reales de un mismo usuario.Comparar modelos: correr el mismo test suite contra un modelo de Bedrock o la API de Anthropic, y comparar tasa de errores de routing/tool-calling contra el modelo local, cuantificar el trade-off costo/latencia vs. confiabilidad.
Chaos testing: simular caídas de las APIs reales (una vez conectadas) con
ChaosPlugin, para validar que el agente degrada con gracia en vez de alucinar una respuesta.
⭐ Migrar a Bedrock AgentCore: envolver el orquestador con BedrockAgentCoreApppara exponerlo vía el runtime administrado de AWS, adicionando AgentCoreMemorySessionManagerpara memoria semántica de usuario (facts/preferences) en vez de solo SlidingWindowConversationManager
Recursos
Cursos Fundamentos
Building AI Agent Harnesses with Strands Agents
Building AI Agent Harnesses – Video CourseDocumentación Strands Agents
Agents as Tools
Trajectory EvaluatorArtículos de Builders y AWS Developer Advocate
AWS Strands Agents: simplifica la creación de agentes de IA
Detener alucinaciones de agentes de IA

Top comments (0)