Por qué tu agente de IA hace 40 llamadas para responder una pregunta

MCP

Alguien le hizo una pregunta razonable a un asistente conectado al servidor MCP de BugDump:

Dame un informe de mis tareas de esta semana, separadas en «En progreso» y «Hecho».

Respondió bien. También tardó unos cuarenta segundos y una treintena de llamadas a herramientas, porque listó las tareas y luego abrió el historial de cada una, una por una, para averiguar cuándo se había movido y quién la movió.

Es el problema N+1. Cualquier ingeniero de backend se lo ha encontrado en un ORM: cargas una lista y luego lanzas una consulta por fila porque lo que de verdad necesitabas no estaba en la lista. Aquí tiene la misma forma, con un motor de consultas más lento y más caro (un modelo de lenguaje) en el lugar donde antes estaba el bucle.

El agente no era tonto. Mi API no le daba otra forma de responder.

El agente solo puede preguntar lo que expusiste

Un modelo con herramientas hace algo parecido a planificar consultas. Lee las descripciones de tus herramientas, deduce qué llamadas podrían dar la respuesta y las ejecuta. Si el eje que necesita no es un filtro, lo reconstruye a mano: trayendo filas y revisándolas.

El list_tasks de BugDump podía filtrar por persona asignada, por estado, por columna, por etiqueta, por cualquier propiedad personalizada y por un rango de fechas de creación. Lo que no podía era responder cuándo se terminó esto. La hora de finalización existía, en una columna llamada resolvedAt, indexada y lista. Simplemente no era un filtro, y no venía en la respuesta.

Así que «qué terminé esta semana» se descomponía, desde el punto de vista del modelo, en: listar las tareas cerradas y luego abrir el historial de cada una y buscar el movimiento. Una llamada por tarea. La información estaba a una cláusula WHERE de distancia.

Tres preguntas que vale la pena hacerse sobre cada herramienta

Desde entonces he auditado las herramientas de BugDump con tres preguntas. Puedes robarlas.

1. ¿Puede quien llama filtrar por cada eje que tienen los datos?

No «cada campo», sino cada eje por el que alguien va a preguntar. El tiempo casi siempre es uno, y casi siempre más de uno: creado, actualizado, terminado, tocado por última vez. Cada uno es una pregunta distinta. Una herramienta que solo expone createdAt obliga a quien pregunte por los otros tres a paginar filas.

La solución fueron dos parámetros, resolvedFrom y resolvedTo. Cuatro líneas de código en el servidor. Eliminaron toda una clase de N+1 de cada informe que escribe un agente.

2. ¿Sabe la herramienta quién la llama?

«Mis tareas» es lo que más se repite en estas peticiones, y durante mucho tiempo el agente no podía responderlo sin adivinar. get_current_project devolvía los miembros del proyecto (nombres, roles, ids), pero nunca decía en nombre de cuál de ellos actúa este token.

Fíjate en qué hace un agente con eso. Revisa la lista de miembros, ve «Alex Rivera», recuerda que el usuario dijo «mis» y elige a quien tenga el nombre más parecido a algo que vio antes. Funciona hasta que un equipo tiene dos personas llamadas Alex.

Ahora BugDump devuelve un objeto viewer: el id de usuario, el nombre y el rol del miembro al que pertenece el token. La identidad no debería ser una deducción.

3. ¿Devuelve la lista lo mismo que el detalle?

Si get_task incluye un campo y list_tasks lo omite, has construido un bucle de peticiones. El agente recibe veinte filas, necesita ese campo para las veinte y llama al endpoint de detalle veinte veces.

En BugDump, a los elementos de la lista les faltaban resolvedAt y updatedAt, mientras que el detalle traía los dos. Añadirlos es un cambio de dos líneas que elimina veinte viajes de ida y vuelta.

La trampa: un timestamp que miente

Mientras añadía los filtros, estuve a punto de publicar un cuarto, updatedFrom / updatedTo sobre updatedAt, porque es la forma obvia de preguntar «qué cambió esta semana».

Habría hecho daño, y el motivo se aplica en general.

En la tabla de tareas de BugDump, updatedAt lo mantiene el ORM: cualquier UPDATE de la fila lo actualiza. Pero las tareas guardan el estado, la persona asignada, la prioridad, las etiquetas y la fecha límite en una tabla de valores aparte. Así que:

  • asignar una tarea no mueve updatedAt;

  • cambiar su prioridad no lo mueve;

  • moverla entre dos columnas abiertas no lo mueve;

  • que un desconocido la vote en tu portal público sí lo mueve.

Un filtro sobre esa columna habría respondido a «en qué trabajé esta semana» con una lista que excluía el trabajo e incluía el clic de un desconocido. Y lo peor: habría parecido creíble. Una mala herramienta no falla con un error, falla con una respuesta equivocada dicha con seguridad.

Lo dejé fuera, y lo dije en la descripción de la herramienta: updatedAt marca cuándo se tocó la fila, no cuándo hubo actividad. La descripción de una herramienta es documentación que el modelo de verdad lee. Úsala para descartar cosas, no solo para explicarlas.

Lo que cuesta equivocarse en esto

Un endpoint lento, visto desde un agente, no es una página lenta. Es:

  • Latencia que nota el usuario. Treinta viajes de ida y vuelta en secuencia, cada uno esperando a que un modelo decida el siguiente.

  • Ventana de contexto quemada. Cada respuesta son tokens. Treinta historiales de tareas pueden desplazar lo que el usuario de verdad pidió.

  • Dinero. Son tokens facturados, cada vez que alguien pregunta.

  • Truncado silencioso. Un agente que se queda sin espacio se detiene antes de tiempo y resume lo que tiene, y su respuesta no dice «solo leí once de las cuarenta tareas».

Eso último es lo que convierte esto en un problema de corrección, no de rendimiento.

El mismo informe, ahora

Con los tres arreglos, el informe semanal es:

  1. get_current_project: quién soy y cuáles son las columnas;

  2. list_tasks con assigneeId + la columna «En progreso»;

  3. list_tasks con assigneeId + status: "closed" + resolvedFrom.

Tres llamadas, siempre tres, sin importar cuántas tareas devuelvan. Cada fila ya trae resolvedAt, así que agrupar por día no necesita nada más.

Una salvedad que documento en vez de esconder: resolvedAt registra la última vez que una tarea entró en una columna cerrada. Si reclasificas una columna de abierta a cerrada, todas sus tareas quedan marcadas con ese momento, así que el informe de una semana puede incluir trabajo que en realidad se terminó meses antes. Los agentes no pueden deducir salvedades así. Escríbelas donde el modelo las vaya a leer.

Si estás construyendo un servidor MCP

Hazle a un agente real una pregunta real y mira el registro de llamadas. No el camino feliz que diseñaste, sino la pregunta que un usuario escribiría de verdad, con sus palabras, un viernes por la tarde.

Cada bucle que veas es un eje que tu API no expuso. La solución suele ser una cláusula WHERE, un campo en la respuesta de una lista o una frase en una descripción. Sale mucho más barato que el rodeo del modelo.

El servidor MCP de BugDump es gratis en todos los planes, incluido el Gratis, y Usar BugDump con Claude Code (MCP) explica cómo conectar Claude Code a tu proyecto y pedirle el resumen de tu semana.

Te puede interesar