Middleware para agentes IA: elimina código duplicado
Tienes un agente con cinco herramientas. Necesitas logging en cada una. Y caché. Y validación de permisos. Y rate limiting por usuario.
Si lo implementas directamente en cada herramienta, escribes el mismo bloque de código cinco veces. La sexta herramienta tarda más en añadir que en implementar. Y cuando cambias el formato de tus logs a Datadog, editas cinco archivos.
Este es el problema que resuelve el patrón middleware aplicado a agentes IA.
El problema: lógica transversal en cada herramienta
En un agente de producción, cada herramienta necesita exactamente las mismas capas de infraestructura:
- Logging: qué llamó el modelo, con qué args, cuánto tardó, qué devolvió
- Caché: si el resultado es el mismo para los mismos inputs, no llames a la API externa
- Auth: este usuario tiene permiso para ejecutar esta herramienta
- Rate limiting: este tenant no puede llamar a
queryDB()más de 50 veces por minuto
Con cinco herramientas y cuatro concerns, tienes 20 bloques de lógica potencialmente idéntica. Cuando el equipo crece y llega la herramienta número doce, el nuevo desarrollador implementa el logging con un formato diferente porque nadie le dijo dónde estaba el estándar. Y no lo encontrará porque está repartido por doce archivos.
La solución no es documentación. Es código que hace imposible saltarse la lógica.
El patrón middleware aplicado a herramientas de agente
La idea es sencilla: en vez de ejecutar las herramientas directamente, envuelves cada ejecución en una cadena de funciones que se ejecutan antes y después.
La firma del middleware
type ToolHandler = (input: unknown) => Promise<unknown>
type Middleware = (
toolName: string,
handler: ToolHandler,
next: ToolHandler
) => ToolHandler
Cada middleware recibe el nombre de la herramienta, el handler original y una función next que representa el resto de la cadena. Puede ejecutar lógica antes de llamar a next, después, o cortocircuitar (para el caché, por ejemplo, que devuelve el resultado sin llamar al exterior).
Composición de middlewares
function composeMiddlewares(
middlewares: Middleware[],
handler: ToolHandler,
toolName: string
): ToolHandler {
return middlewares.reduceRight(
(next, middleware) => middleware(toolName, handler, next),
handler
)
}
Los cuatro middlewares esenciales en producción
Logger: una sola fuente de verdad
const loggerMiddleware: Middleware = (toolName, _handler, next) => {
return async (input) => {
const start = Date.now()
try {
const result = await next(input)
console.log({
tool: toolName,
input,
durationMs: Date.now() - start,
status: 'ok'
})
return result
} catch (err) {
console.error({
tool: toolName,
input,
durationMs: Date.now() - start,
status: 'error',
err
})
throw err
}
}
}
En vez de logs dispersos por cada herramienta, tienes una sola fuente de verdad. Cambias el destino a Datadog o Sentry en un único sitio.
Caché con TTL: menos llamadas a APIs externas
const cache = new Map<string, { value: unknown; expiresAt: number }>()
const cacheMiddleware = (ttlMs = 60_000): Middleware =>
(toolName, _handler, next) => {
return async (input) => {
const key = `${toolName}:${JSON.stringify(input)}`
const cached = cache.get(key)
if (cached && cached.expiresAt > Date.now()) return cached.value
const result = await next(input)
cache.set(key, { value: result, expiresAt: Date.now() + ttlMs })
return result
}
}
Una herramienta como searchCRM({ query: "Acme" }) puede devolver el mismo resultado durante 60 segundos. Con este middleware no llamas a Salesforce más de lo necesario. En un agente de alto volumen, eso reduce latencia y coste de forma directa.
Rate limiting por herramienta y usuario
const rateLimiter = new Map<string, number[]>()
const rateLimitMiddleware = (maxCalls: number, windowMs: number): Middleware =>
(toolName, _handler, next) => {
return async (input) => {
const userId = (input as any)?.__userId ?? 'anonymous'
const key = `${userId}:${toolName}`
const now = Date.now()
const calls = (rateLimiter.get(key) ?? []).filter(t => t > now - windowMs)
if (calls.length >= maxCalls) {
throw new Error(`Rate limit exceeded for ${toolName}`)
}
rateLimiter.set(key, [...calls, now])
return next(input)
}
}
Integración con el SDK de Anthropic
Tus herramientas están definidas como objetos con name, description e inputSchema. El middleware se aplica al handler de ejecución, no a la definición que ve el modelo:
const middlewares = [
loggerMiddleware,
cacheMiddleware(30_000),
rateLimitMiddleware(100, 60_000),
]
function wrapTools(tools: Record<string, ToolHandler>) {
return Object.fromEntries(
Object.entries(tools).map(([name, handler]) => [
name,
composeMiddlewares(middlewares, handler, name)
])
)
}
const wrappedTools = wrapTools({
searchCRM: async (input) => { /* lógica CRM */ },
queryDB: async (input) => { /* lógica BD */ },
sendEmail: async (input) => { /* lógica email */ },
})
La definición de herramientas que le pasas al modelo queda intacta. Solo cambia cómo se ejecutan.
Si ya implementas selección dinámica de herramientas por relevancia, el middleware se aplica exactamente en el mismo punto: en el executor, no en el registry.
Qué cambia en producción
Con este patrón:
- Añadir una herramienta nueva = implementar el handler. El logging, caché, auth y rate limiting se aplican solos.
- Cambiar el formato de logs = editar un archivo.
- Desactivar la caché para debugging = quitar un middleware de la lista.
- Auditar qué herramientas se llaman más = leer los logs, que ya existen por defecto.
- Añadir un nuevo concern = escribir un middleware, añadirlo al array.
La deuda técnica de los cross-cutting concerns no desaparece. Se centraliza donde se puede gestionar.
Cuando construyes un agente en producción real, esta clase de arquitectura es la diferencia entre un sistema que evoluciona limpio y uno que se convierte en un laberinto de código duplicado. En DAILYMP lo aplicamos desde la primera herramienta, no cuando el agente ya tiene doce. Si necesitas ayuda para estructurar correctamente tu implementación de integración de IA, podemos revisarlo juntos.
¿Tienes un agente creciendo y necesitas organizarlo antes de que se te vaya de las manos? Hablamos por WhatsApp y vemos cómo estructurar tu implementación.