Cómo armar tu propio agente con Ollama, Claude API y memoria persistente (sin pagar SaaS)
Cierre práctico de la serie sobre agentes de IA. Stack mínimo, decisiones honestas y los gotchas que descubrimos rompiendo cosas: Ollama Cloud no expone embeddings (te ahorramos las horas), prompt caching de Claude reduce costos al 90%, y un patrón de orquestación Opus + Sonnet en paralelo que paraleliza trabajo independiente sin que se pisen.
Cierre de la serie de cuatro posts sobre agentes de IA. Si llegas directo, los anteriores son: qué es un agente, memoria y comparativa OpenClaw vs Hermes vs Claude Code.
Hoy: el tutorial. Cómo armar tu propio agente self-hosted que combine modelos locales (Ollama), un cerebro grande cuando hace falta (Claude API) y memoria persistente. Con los gotchas que aprendimos por las malas.
Aclaración importante: claude.ai vs Claude API vs Claude Code
Tres cosas distintas que se confunden seguido:
| Producto | Qué es | Para qué sirve |
|---|---|---|
| claude.ai | Chat web con Claude | Explorar, hacer preguntas puntuales, ideación |
| Claude API | API programática (Python, TypeScript, etc.) | Construir tu propio agente, conectar Claude a tu software |
| Claude Code | CLI agentico oficial | Trabajar sobre repositorios con un agente listo |
En este post vamos a usar Claude API. claude.ai es donde experimentas; Claude API es donde construyes. Las dos son del mismo modelo por debajo, pero la API permite memoria, prompt caching, tool use, batch — todo lo que un agente serio necesita.
El stack mínimo que recomendamos
Después de varios meses iterando, esta es la pila que llegamos a recomendar como punto de partida sano:
┌─────────────────────────────────────┐
│ Tu agente (Python o TypeScript) │
│ Loop ReAct + tool use │
└──────────┬─────────────┬────────────┘
│ │
┌───────▼──────┐ ┌──▼──────────────┐
│ Claude API │ │ Ollama (local) │
│ Razonamiento│ │ Embeddings + │
│ heavy │ │ tareas baratas │
└───────┬──────┘ └──┬──────────────┘
│ │
┌───────▼─────────────▼──────────┐
│ Servidor MCP de memoria │
│ (Qdrant + un poco de FastMCP) │
└────────────────────────────────┘
Por qué esta combinación:
- Claude para razonamiento heavy porque sigue siendo lo más sólido para cadena larga, decisiones sobre tool use y outputs estructurados. Para tareas que valgan la pena el costo.
- Ollama local para tareas baratas y embeddings porque (a) mandar 10.000 docs a embedear vía API cloud cuesta una fortuna, (b) tareas simples (clasificar, resumir un chunk corto) corren bien en Llama 3, Qwen o DeepSeek local sin tocar tu API budget.
- Qdrant porque la memoria semántica vive ahí. Local, rápido, gratis hasta volúmenes de millones de vectores.
- MCP como protocolo de unión. Tu agente no se acopla a Qdrant — se acopla a un servidor MCP que podría ser Qdrant. Si mañana cambias a Pinecone o a un vault de markdown, no tocas el agente.
Setup paso a paso
1. Instalá Ollama y bajá los modelos
# macOS / Linux
curl -fsSL https://ollama.com/install.sh | sh
# Modelos que necesitas
ollama pull llama3.2 # Chat ligero
ollama pull bge-m3 # Embeddings (1024 dims)
ollama pull deepseek-coder # Si vas a hacer code stuff local
Verificá que Ollama te da embeddings. Acá viene el primer gotcha grande:
curl -X POST http://localhost:11434/api/embeddings \
-H 'Content-Type: application/json' \
-d '{"model":"bge-m3","prompt":"hola"}' | head -c 200
Si te devuelve un vector de números, vamos bien.
Gotcha #1 — Ollama Cloud NO expone embeddings. Lo descubrimos a las malas. La versión gestionada en
ollama.comsolo te da modelos de chat (kimi-k2, deepseek, gpt-oss). El endpoint/api/embedcon una API key válida para chat devuelve 401. Para embeddings necesitas Ollama local o autohosteado. Verifícalo siempre con curl antes de empezar a armar el resto del stack — no asumas.
2. Levantá Qdrant
# docker-compose.yml
services:
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- ./qdrant_data:/qdrant/storage
docker compose up -d qdrant
Creá una colección con la dimensión correcta para tu modelo de embeddings:
curl -X PUT http://localhost:6333/collections/memory \
-H 'Content-Type: application/json' \
-d '{"vectors":{"size":1024,"distance":"Cosine"}}'
Gotcha #2 — La dimensión del modelo de embeddings tiene que coincidir EXACTO con la colección. BGE-M3 son 1024 dimensiones, Nomic-embed-text son 768. Si tu colección está creada con 768 y le metes vectores de 1024, Qdrant rechaza el insert con un error oscuro. Si te lo encuentras, la solución es borrar y recrear la colección con el tamaño correcto.
3. Conseguí tu API key de Claude
console.anthropic.com → API Keys. Cargá saldo, generá la key, la guardas como variable de entorno:
export ANTHROPIC_API_KEY="sk-ant-..."
4. El esqueleto del agente
Esto es lo más simple posible que ya hace algo útil:
# agent.py
import os
import anthropic
from qdrant_client import QdrantClient
import requests
client = anthropic.Anthropic()
qdrant = QdrantClient(host="localhost", port=6333)
def embed(text: str) -> list[float]:
"""Embeddings con Ollama local."""
r = requests.post(
"http://localhost:11434/api/embeddings",
json={"model": "bge-m3", "prompt": text},
timeout=30
)
return r.json()["embedding"]
def remember(text: str, metadata: dict):
"""Guarda un hecho en la memoria."""
vec = embed(text)
qdrant.upsert(
collection_name="memory",
points=[{
"id": metadata["id"],
"vector": vec,
"payload": {"text": text, **metadata}
}]
)
def recall(query: str, k: int = 5) -> list[str]:
"""Recupera los k recuerdos más similares."""
vec = embed(query)
results = qdrant.search(
collection_name="memory",
query_vector=vec,
limit=k
)
return [hit.payload["text"] for hit in results]
def agent_step(user_message: str, system: str):
# Recuperar memoria relevante
memories = recall(user_message)
memory_block = "\n".join(f"- {m}" for m in memories)
# Llamar a Claude con prompt caching
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": system,
"cache_control": {"type": "ephemeral"}
},
{
"type": "text",
"text": f"Contexto recuperado de memoria:\n{memory_block}",
}
],
messages=[
{"role": "user", "content": user_message}
]
)
return response.content[0].text
Eso ya es un agente funcional con memoria. Le pides algo, recupera contexto relevante de Qdrant, se lo manda a Claude con caching, y te devuelve la respuesta.
5. Agregá tool use
Para que el agente "actúe" y no solo "responda", definí herramientas:
tools = [
{
"name": "remember",
"description": "Guarda un hecho importante para sesiones futuras",
"input_schema": {
"type": "object",
"properties": {
"fact": {"type": "string", "description": "El hecho a recordar"}
},
"required": ["fact"]
}
},
{
"name": "search_web",
"description": "Busca en internet",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
}
]
Cuando Claude responda con stop_reason: "tool_use", ejecuta la tool y devuelve el resultado al modelo. Ese es el loop ReAct del primer post. La SDK oficial maneja la coreografía; tú solo escribes las funciones.
Prompt caching: el truco que reduce 90% del costo
Si tu agente repite contexto largo en cada llamada (system prompt grande, memoria recuperada, instrucciones), el prompt caching es el mejor amigo de tu factura.
Funciona así: marcas con cache_control los bloques estables. La primera vez Claude los procesa normal (cuesta lo de siempre); las siguientes 5 minutos, ese mismo prefijo se reusa al 10% del costo.
system=[
{
"type": "text",
"text": "Eres un asistente experto en operaciones...",
"cache_control": {"type": "ephemeral"} # ← cacheado
}
]
Reglas para que sirva: (a) los bloques cacheados deben ser idénticos entre llamadas — un solo carácter diferente y el caché se invalida; (b) el bloque cacheado debe tener mínimo 1024 tokens (Sonnet 4.6) para que valga la pena; (c) la TTL son 5 minutos por defecto, así que solo sirve si las llamadas se hacen seguidas.
Para un agente que mantiene una conversación larga con contexto grande, la diferencia se nota literal en la factura del primer mes.
El patrón que usamos en producción: Opus orquesta, Sonnet en paralelo
Cuando el problema es paralelizable (cuatro módulos independientes, cuatro reportes simultáneos), no tiene sentido usar un solo agente lento. El patrón que validamos:
- Un agente Opus 4.7 orquesta. Define los contratos compartidos (tipos, mocks, interfaces) ANTES de spawnear cualquier subagente.
- Varios Sonnet 4.6 ejecutan en paralelo. Cada uno recibe un brief detallado: "esto YA existe, no lo recrees", "esto debes construir", "esto NO toques", "estos son los criterios de listo".
- Cada Sonnet escribe en su propia carpeta. Cero conflictos de merge porque los archivos son disjuntos.
- El orquestador integra al final. Verifica que los contratos se respetaron, corre tests cruzados.
Resultado real: cuatro módulos de frontend en ~5 minutos vs ~30 minutos secuenciales. Sonnet es más barato que Opus en ejecución, así que el costo total baja también.
Lo no obvio es que el orquestador debe definir los contratos antes. Si dejas que el primer Sonnet decida los tipos sobre la marcha, el segundo importa lo que cree que existe, falla. Tipos centralizados antes de spawnear, no al vuelo.
Memoria persistente con servidor MCP
Hasta acá tu agente es un script. Para que la memoria sea reutilizable por otros agentes (Claude Code, OpenClaw, Hermes Agent, lo que sea), conviene exponer la memoria como servidor MCP.
Eso es lo que hicimos con mcp-memory: un servidor FastMCP que expone tools remember, recall, forget, indexa contenido en Qdrant con embeddings de Ollama, y se conecta a Claude Code como cualquier otra herramienta MCP. Es open source, vive en github.com/GermaniU/mcp-memory. La idea: cualquier agente compatible con MCP puede compartir la misma memoria.
Gotcha #3 — los curl manuales a un endpoint MCP necesitan
Accept: application/json, text/event-stream. Sin ese header, FastMCP devuelve 406 / connection reset y parece que el server está muerto. El cliente MCP lo añade solo, pero al diagnosticar a mano hay que ponerlo. Anotalo si vas a debuggear servidores MCP.
Costos reales en mayo 2026
Para un agente que corre 8 horas al día en una operación PyME (revisando correos, agendando, redactando, consultando memoria):
| Componente | Costo mensual aproximado |
|---|---|
| VPS para Ollama + Qdrant (4 vCPU, 16 GB RAM) | 20-40 USD |
| API de Claude (con caching agresivo) | 30-80 USD |
| Storage para memoria | <5 USD |
| Total | ~50-130 USD/mes |
Para comparar: un SaaS equivalente arranca en 50-100 USD/mes por usuario y suele subir con uso. Para una persona puede ser igual; para un equipo de 5+ el self-hosted suele ser radicalmente más barato. Para un equipo de 50+ siempre lo es.
Cuándo conviene self-hosted y cuándo no
Self-hosted gana cuando:
- Tienes más de 3-5 usuarios y el costo del SaaS escala mal.
- Privacidad o compliance importan (LATAM, banca, salud, legal).
- Quieres portabilidad y evitar lock-in a un proveedor.
- Tu equipo tiene capacidad técnica de operar un par de containers.
Self-hosted pierde cuando:
- Eres uno solo y quieres algo que funcione en 10 minutos.
- No tienes tiempo ni equipo para mantener una pila pequeña.
- El SaaS específico tiene features que tú no vas a poder replicar.
Si caes del lado de "self-hosted gana" pero no tienes ganas de armar todo desde cero, te lo configuramos. Llevamos meses operando exactamente este stack.
Cierre de la serie
Cuatro posts conectados:
- Qué es un agente de IA en 2026 — el marco.
- La memoria es lo que separa un agente útil de un chatbot olvidadizo — el problema central.
- OpenClaw vs Hermes Agent vs Claude Code — qué herramienta cuándo.
- Este post — cómo armarte el tuyo.
Si la serie te sirvió, lo más útil que puedes hacer es probar algo. Aunque sea un agente de 50 líneas que conteste con tu memoria. La diferencia entre quien gana con esta tecnología y quien la mira de afuera es esa.
Y si quieres esto montado para tu negocio sin armarlo desde cero — con seguridad, memoria, supervisión humana y respaldo — eso es exactamente lo que hacemos. Hablemos.
Relacionado: Configurar y usar Claude Code en un proyecto · Tu vault de Obsidian convertido en memoria viva · RAG en producción: tres bugs reales · SSH read-only + Tailscale: que la IA vigile el servidor.
Preguntas frecuentes
¿Necesito GPU para correr Ollama local?
Para los modelos de embeddings (BGE-M3, Nomic) corren bien en CPU; un servidor moderno con 8-16 GB de RAM los maneja sin GPU. Para modelos de chat locales (Llama 3, DeepSeek), una GPU acelera mucho — sin GPU vas a tener latencias de varios segundos por respuesta. Si tu uso es solo embeddings + Claude API para razonamiento, no necesitas GPU. Si quieres correr todo el chat local sin Claude, conviene tener GPU.
¿Conviene usar Claude Sonnet, Opus o Haiku para el agente?
Depende de la tarea. Sonnet 4.6 es el sweet spot para la mayoría de agentes en producción: razona bien, es mucho más barato que Opus, y maneja contextos largos sin degradar. Opus 4.7 vale cuando la tarea requiere razonamiento profundo continuo (orquestar a otros agentes, code review denso, decisiones complejas). Haiku 4.5 es ideal para subagentes con tareas acotadas — clasificación, extracción, resúmenes cortos. El patrón ganador es Opus orquestando + Sonnet o Haiku ejecutando en paralelo, no Opus para todo.
¿Puedo usar este stack 100% offline, sin internet?
Casi: Ollama + Qdrant + tu agente corren totalmente offline. El único componente cloud es Claude API. Si la queres 100% offline tienes dos opciones: (a) usar solo Ollama con un modelo grande local (Llama 3 70B, Qwen 2.5, DeepSeek-R1) y aceptar que la calidad de razonamiento será menor que Claude; (b) un proveedor self-hosted como vLLM con un modelo abierto. Para PyME real recomendamos hibrido: modelos locales para 80% de las tareas, Claude API solo para el 20% que requiere razonamiento heavy.
¿El prompt caching de Claude funciona con memoria recuperada de Qdrant?
Funciona si la memoria recuperada es estable entre llamadas — y normalmente no lo es porque cada query genera un set distinto de chunks. La estrategia que mejor nos funcionó: cachear lo estable (system prompt, instrucciones generales, esquema de tools) y NO cachear la memoria recuperada (que cambia cada turno). El ahorro viene del system prompt, no de la memoria. Si tu system prompt es chico, prompt caching no te va a ayudar mucho — vale la pena cuando el system es 2000+ tokens.
¿Qué pasa si el agente alucina y guarda algo incorrecto en memoria?
Pasa más seguido de lo que se admite. Mitigaciones que usamos: (1) limitar qué tipo de cosas puede guardar (con un schema en la tool de remember); (2) marcar cada hecho con metadata de fuente y timestamp; (3) revisión humana periódica de la memoria del agente, especialmente al principio; (4) un comando para que tú puedas borrar hechos directamente. La memoria de un agente es como un junior — al principio supervisas cada cosa que escribe, después confías más, pero la supervisión nunca llega a cero. El patrón vault-as-memory ayuda porque cada hecho es un .md que tú puedes leer y editar.