Llevo varios meses construyendo un sistema multi-agente que ya está en su versión 3.0, con CLI interactiva, dashboard web por WebSocket, 15 expertos especializados y soporte para 60+ proveedores LLM vía LiteLLM. Este post no es teoría, es lo que he ido aprendiendo construyéndolo.
Cero hype. Solo lo que funciona en producción.
Patrón central: CEO + 15 expertos + Worker Pool
[ Usuario / Tarea ]
│
[ Orquestador (CEO) ] stateless
descompone en HandoffTickets
│
▼
[ Worker Pool (≤3 paralelos) ]
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
[ Developer ] [ Security ] [ Tester ] (15 expertos)
│ │ │
▼ ▼ ▼
[ Tools: archivos, código, tests, git, shell, BD ]
│
┌─────────────┴─────────────┐
▼ ▼
[ Redis: estado ] [ Qdrant: memoria semántica ]El orquestador no piensa, enruta. Los expertos piensan en su dominio. Las herramientas hacen el trabajo real.
Los siete principios que sostienen el diseño
P1. Stateless Orchestrator
El CEO (core/orchestrator.py) no acumula contexto entre llamadas. Recibe la tarea, la descompone en HandoffTicket (Pydantic) y los reparte. Punto.
¿Por qué? Porque un orquestador con memoria se contradice a sí mismo después de tres pasos. Un orquestador stateless con contexto explícito (tope de 500 tokens por ticket) es predecible.
P2. Handoff Protocol con JSON estricto
Entre agentes nunca pasa texto libre. Pasa un objeto Pydantic. Si el experto recibe basura, falla rápido en vez de inventar.
class HandoffTicket(BaseModel):
ticket_id: str
expert_type: str # "developer", "tester", "security", ...
task: str
context: dict # ≤500 tokens
priority: int
class ExpertResult(BaseModel):
ticket_id: str
expert_type: str
output: str # comprimido por SummaryAgent
tools_used: list[str]
tokens_used: int
cost_usd: floatP3. BaseExpert: la lógica vive en la clase base
Los 15 expertos heredan de super_experts/base_expert.py. La clase base contiene el motor (build_prompt, run_tool_use_loop, compress_output, handle_error). Cada experto concreto declara solo lo suyo: keywords, tools permitidas y archivo de prompt.
class BaseExpert:
def build_prompt(self, ticket): ...
def run_tool_use_loop(self, response): ...
def compress_output(self, output): ...
def handle_error(self, error): ...
class DeveloperExpert(BaseExpert):
KEYWORDS = ["code", "implement", "function", "class", "bug"]
TOOLS = ["file_reader", "file_writer", "code_executor", "test_runner"]
PROMPT_FILE = "config/prompts/developer.md"Añadir un experto nuevo es escribir 20 líneas y un Markdown con el prompt. No es heroísmo, es ingeniería.
P4. Token Budget por nivel de agente
Cada agente tiene un techo de tokens. Si lo supera, el ticket falla rápido. Sin esto, el contexto crece y el coste se dispara. Cuando un experto produce output largo, un Summary Agent lo comprime antes de devolverlo al orquestador.
P5. Tool Use Loop con max_iterations
Los expertos llaman herramientas en bucle: prompt → tool_call → tool_result → prompt → tool_call... hasta llegar a la solución o tocar max_iterations. El bucle infinito es el bug más caro de los multi-agentes. Siempre tope.
P6. LiteLLM como capa universal
Detrás de cada llamada a un LLM no hay SDK de Anthropic, OpenAI ni Google. Hay LiteLLM. Cambiar de proveedor es una env var:
# Groq, rápido y gratis para pruebas
LLM_MODEL=groq/llama-3.3-70b-versatile
# Claude para razonamiento complejo
LLM_MODEL=anthropic/claude-sonnet-4-6
# GPT-4o
LLM_MODEL=openai/gpt-4o
# Local con Ollama, sin coste, sin internet
LLM_MODEL=ollama/llama3.2Cero código tocado. Esta decisión salva semanas cuando un proveedor sube precios o saca un modelo nuevo.
P7. Observabilidad first
Cada llamada a LLM es un span de OpenTelemetry. Tokens entrantes y salientes, coste estimado en USD, latencia, modelo, agente, ticket_id. Sin esto vuelas a ciegas. El debug multi-agente sin trazas es arqueología, no ingeniería.
Worker Pool con tope de 3 paralelos
Una decisión menor pero importante: los expertos corren en paralelo (core/worker_pool.py), pero nunca más de 3 a la vez (MAX_PARALLEL_AGENTS=3 por defecto).
¿Por qué? Tres razones:
- Rate limit del proveedor LLM. Groq, Anthropic y OpenAI tienen límites RPM/TPM. Lanzar 10 agentes a la vez te tumba la sesión.
- Coste predecible. Cada experto factura tokens. Tres en paralelo es controlable; diez no.
- Debug humano. Un timeline con 3 agentes ejecutando es legible. Con 10 es ruido.
El pool corre con un semáforo de Redis. Cuando un slot se libera, el siguiente ticket entra. Simple y robusto.
Memoria a dos niveles
| Capa | Tecnología | Para qué |
|---|---|---|
| Estado runtime | Redis 5.0+ | Sesiones, tickets en curso, rate limiting (RPM/TPM), PubSub para WebSocket |
| Memoria semántica | Qdrant 1.9+ | Resultados anteriores indexados por similaridad |
La separación es clave. Mezclar las dos en un mismo backend (todo en Postgres con tablas TTL caseras, por ejemplo) parece más simple pero acaba lento y propenso a bugs de consistencia.
Embeddings vía Ollama local (nomic-embed-text, 768 dimensiones) o OpenAI (1536). Configurable. Y QDRANT_MODE=off para entornos donde la memoria semántica no aporta.
Lo que descarté
Frameworks "todo en uno"
Probé LangChain agents, AutoGen, CrewAI. Todos funcionan en un demo. Todos esconden la lógica detrás de abstracciones que dificultan debug y trazabilidad. El coste de aprenderlos es comparable a escribir tu propio orquestador en Python plano, y lo segundo da más control.
LangGraph sí lo consideré: aporta una máquina de estados explícita sin tanta magia. Pero al final, con LiteLLM + Pydantic + un orquestador propio de 300 líneas, no necesitaba más.
Peer-to-peer entre agentes
Probé sistemas donde los expertos se llamaban directamente entre ellos (Developer pide ayuda al Security Expert sin pasar por CEO). Resultado: emergent behavior interesante en demos, pesadilla en debug. El orquestador centralizado pierde algo de "magia" pero gana cordura operacional.
Memoria persistente entre tareas en el mismo agente
Tener un experto que "recuerda" todas las conversaciones previas suena potente. En la práctica genera dos problemas: el contexto crece y el coste se dispara, y decisiones viejas contaminan tareas nuevas. Mejor scratchpad efímero por sesión + memoria semántica explícita en vectorial cuando hace falta.
El resultado
Un sistema con CLI interactiva (python cli.py), API REST + WebSocket (POST /sessions), dashboard web en tiempo real, 15 expertos especializados, soporte de 60+ proveedores LLM vía LiteLLM, memoria semántica con Qdrant, control de coste por sesión y observabilidad por OpenTelemetry. Todo en Python 3.11+ tipado con Pydantic.
Cualquiera que entre al repo entiende qué pasa en una hora. Eso es lo que diferencia ingeniería de magia: que un compañero pueda leer el código y predecir el comportamiento sin ejecutarlo.
LLMs envueltos en lógica explícita son ingeniería. LLMs envueltos en magia son apuesta. Cuanto más visible es el grafo de decisión, más confío en el sistema.