Desarrollamos tu web presencial SPA DESDE 300€. Si, es una locura. Web SPA DESDE 300€ — Si, es una locura. Hablemos →

Por qué un agente de IA es mucho más que llamar a un modelo

Por qué un agente de IA es mucho más que llamar a un modelo

Casi todo el que integra por primera vez un modelo de lenguaje en una aplicación llega a la misma conclusión equivocada: que ya está hecho. La llamada funciona, el modelo contesta y parece que lo único que queda es pulir el prompt. Lo que separa esa demo de un agente que puedes poner delante de un cliente no es el modelo, es todo lo que hay que construir alrededor para que no se rompa, no se dispare de coste y no se quede mudo a mitad de una conversación. Los ejemplos van en Laravel, uno de los stacks en los que nos especializamos en AndorraDev, pero la lista de piezas que hay que construir es la misma en Python, Node o lo que uses.

La llamada al modelo son cuatro líneas

Esto es la petición HTTP entera a la API del modelo, tal cual está en el código que usamos en producción:

$response = Http::withHeaders([
    'Authorization' => 'Bearer ' . $this->apiKey,
    'Content-Type' => 'application/json',
])->timeout($this->timeout)->post($this->baseUrl . '/chat/completions', $body);

Cuatro líneas. El paquete que las rodea tiene casi siete mil. Esa proporción es el artículo entero: hacer que un modelo conteste es trivial, y no es el trabajo.

El modelo no responde, pide herramientas

En cuanto le das capacidades, el modelo deja de contestar en una sola vuelta. Contesta pidiendo que ejecutes algo, tú lo ejecutas, le devuelves el resultado, y vuelve a hablar. A veces pide otra cosa. A veces cinco veces seguidas.

Eso es un bucle, y ahí aparece el primer problema que nadie prevé en la demo: hay que devolverle su propio turno aunque venga vacío. Cuando el modelo solo pide herramientas, su mensaje no lleva texto. Si lo omites del historial por parecer inútil, los resultados que envías después apuntan a llamadas que ya no existen y el proveedor rechaza la petición entera.

Y las herramientas fallan. La respuesta correcta a una herramienta que revienta no es propagar la excepción, es devolverle el error al modelo como si fuera un resultado más. Desde una traza de pila no puede recuperarse; desde un {"error": "..."} sí puede elegir otra vía.

Ese bucle necesita un tope, y no por elegancia

Un modelo que insiste en una búsqueda que no funciona la reformulará indefinidamente. Sin límite, el bucle corre hasta que algo agota su tiempo, habiendo pagado cada vuelta.

El tope tampoco es un error: al agotarse, la última respuesta vuelve marcada, se registra un aviso y el usuario recibe algo. Un agente que se queda mudo porque llegó a su límite es peor que uno que dice lo que tiene.

El detalle que separa un agente usable de uno que te arruina

Un modelo no tiene memoria de sus propios intentos dentro de una misma vuelta. Si una búsqueda no devuelve nada, no concluye que ese dato no existe: concluye que la formuló mal, y lo intenta otra vez con otras palabras. Esa insistencia es razonable en una persona y ruinosa cuando cada intento se factura.

Cuando el modelo no encuentra nada, su reacción natural es reformular y volver a buscar. Es razonable una vez. A la cuarta está quemando dinero para llegar a la misma conclusión, que es que eso no está.

El problema para detectarlo es que el sistema no sabe qué devuelven tus herramientas. Cada proyecto tiene las suyas. La solución fue mirar la forma en vez del contenido: si el resultado trae alguna de las claves típicas de una búsqueda, items, results, matches, es una búsqueda; y si todas vienen vacías, no encontró nada.

Al segundo vacío seguido, el resultado que recibe el modelo lleva añadido esto:

Van dos búsquedas seguidas sin resultados. Si te has quedado sin reformulaciones razonables, dile al usuario que no has encontrado nada en lugar de volver a buscar.

Funciona porque no le prohíbes nada, le das el dato que le falta. El modelo no sabe cuántas veces lleva buscando; nadie se lo había dicho.

Un detalle fino: un resultado que no parece una búsqueda no rompe la racha. Si entre dos búsquedas muertas hay una consulta de otra cosa, la cuenta sigue viva.

La conversación crece hasta que no cabe

Toda conversación larga acaba superando la ventana del modelo. Hay que resumir, y ahí casi todo el mundo comete el mismo error: resumirlo todo.

Dos reglas que nos costó aprender:

Los últimos mensajes nunca se comprimen. Es donde vive el hilo de lo que se está diciendo, y "un resumen de lo que acabamos de hablar" es exactamente donde estos sistemas se vuelven vagos y contestan al lado.

Los resultados de herramientas nunca entran en el resumen. Pertenecen al turno que los produjo. Arrastrarlos entre turnos es como un bucle acaba releyendo sus propias tuberías en lugar de la conversación.

El resumen además es incremental: se guarda hasta qué mensaje se ha resumido, y la siguiente compresión parte del resumen anterior en vez de rehacerlo. Y si la compresión falla, se conserva el resumen viejo. Perderlo tiraría todo lo que la conversación había establecido; mantener uno algo desactualizado cuesta un poco de contexto y nada más.

Andie recomienda

Para decidir si toca comprimir no hace falta un tokenizador de verdad: dividir los caracteres entre cuatro basta. Equivocarse un diez por ciento solo significa comprimir un turno antes o después, y arrastrar una dependencia entera para afinar eso no compensa. Guarda la precisión para lo que factura, que es la respuesta del proveedor.

Preguntar si hay saldo antes de gastar, no después

Si vendes esto, cada vuelta del bucle cuesta dinero. Y como el bucle llama al modelo una vez por iteración, la comprobación de saldo se hace varias veces en un mismo turno, así que tiene que ser barata o memorizada.

Hay una distinción que conviene tener clara desde el principio, porque determina si reintentas:

Un 429 del proveedor es problema suyo, ocurre después de que la petición salga, es temporal y se reintenta.

Un saldo agotado es problema tuyo, ocurre antes de que la petición salga, no es temporal y no se reintenta nunca. Ahí no hay espera que valga: o cambia el plan o entra el periodo siguiente.

Tratarlos igual significa reintentar contra tu propia falta de crédito, que es una forma elegante de no hacer nada durante un rato.

Dos proveedores, un solo dialecto

En cuanto quieres poder cambiar de proveedor, aparece el trabajo aburrido de traducir. Y no es un mapeo de campos, son incompatibilidades reales.

Anthropic rechaza resultados de herramienta en turnos separados: hay que fusionarlos en uno solo. Y rechaza un turno de asistente sin contenido, así que hay que rellenarlo con algo.

Peor todavía: las capacidades no van por proveedor, van por modelo, y solo se descubren probando. Medido contra la API real:

claude-opus-4-8   + temperature  ->  400 "`temperature` is deprecated for this model"
claude-sonnet-4-6 + temperature  ->  200 OK

Mismo proveedor, misma familia, comportamiento distinto. La única forma de saberlo es que alguien se coma el 400.

Y aquí una decisión de diseño que recomiendo copiar: ante un modelo desconocido, asumir que lo permite todo. Si te equivocas por ahí, sale un 400 visible el primer día. Si te equivocas al revés, estás enviando peticiones capadas sin enterarte, y eso puede correr meses.

Tres decisiones de diseño que conviene conocer

Las tres son deliberadas y condicionan para qué encaja la pieza.

El bucle es síncrono de principio a fin. La respuesta se entrega completa, no palabra a palabra. Si quieres el efecto de escritura progresiva, se resuelve en la capa de entrega: en nuestro caso el turno se calcula en una cola y la interfaz pregunta, que es el patrón correcto para un asistente de trabajo.

Ante un 429 se cambia de proveedor, no se espera. Y viene apagado por defecto. La decisión es deliberada: una clave mal puesta o una petición malformada no van a ir mejor en otro sitio, y reintentarlas en todos convierte un fallo en tantos como proveedores tengas, cada uno facturado. La espera creciente, cuando la quieras, es una política tuya y va fuera.

Y una que es muy fácil de hacer mal. Es tentador poner la decisión de anonimizar datos personales dentro del bucle de herramientas, porque es donde se llama al modelo. El problema es que el bucle no es el único que llama: el compresor de historial también manda transcripciones enteras fuera. Si el anonimizador es privado del bucle, esa segunda vía no tiene de dónde sacarlo y sencillamente no pregunta. La decisión de anonimizar tiene que vivir fuera y compartirla todo el que hable con el proveedor.

Qué hay en el mercado y en qué se diferencia

En PHP hay tres capas distintas y conviene no confundirlas, porque no resuelven lo mismo.

openai-php/laravel y los clientes equivalentes de cada proveedor son la capa de abajo: envuelven la API en un objeto cómodo. Hacen muy bien lo que hacen y no pretenden ser otra cosa. El bucle, el tope, la compresión del historial y el control de saldo siguen siendo tuyos.

Prism sube un escalón: normaliza varios proveedores detrás de una misma interfaz y trae herramientas y salida estructurada. Si lo que necesitas es poder cambiar de modelo sin reescribir medio proyecto, es una elección muy sólida.

Neuron AI ya se plantea directamente como framework de agentes en PHP, con memoria, RAG y observabilidad. Fuera de PHP, las referencias son LangChain y LlamaIndex, y traen bastante más superficie de la que suele hacer falta.

Laragents, que es el que mantenemos, nació de necesitar el bucle completo con tope duro, control de saldo por usuario y compresión de historial dentro de una aplicación Laravel que ya existía. Si lo tuyo es una llamada puntual a un modelo, cualquiera de los de arriba te sobra. Si lo que vas a poner en producción gasta dinero por conversación, la lista de piezas es la de este artículo, la montes con lo que la montes.

Y si no trabajas con Laravel

La lista de lo que hay que construir es la misma en cualquier stack, y es lo que de verdad deberías presupuestar:

El bucle de herramientas con un tope duro. La reinyección del turno del asistente aunque venga vacío. Los errores de herramienta como resultado y no como excepción. Una compresión de historial que respete los últimos turnos. La comprobación de saldo antes de cada llamada, distinguiendo tu límite del suyo. La normalización entre proveedores. Y algún mecanismo que corte la búsqueda infinita.

La llamada al modelo, insisto, son cuatro líneas. Si tu presupuesto de "integrar IA" contempla solo eso, va a faltar todo lo demás.

Dónde está el código

El paquete está publicado en Packagist, el código en GitHub y la documentación del paquete recoge todas las opciones. Está en producción en Abodara, donde el asistente busca inmuebles y cruza demandas con propiedades.

Si estás pensando en meter un asistente en tu producto y quieres saber qué hay debajo del titular, es de lo que más trabajamos ahora mismo en inteligencia artificial y aplicaciones web a medida. Cuéntanos el caso y te decimos qué parte es real y qué parte es demo.

Escrito por
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desarrollador full-stack con más de 15 años de experiencia en Laravel, React, Node.js y arquitecturas cloud. Ayudo a empresas en Andorra a construir su presencia digital.

Partner de diseño · ionospace.
Necesitas ayuda? ×
Andie by AndorraDev
Asistente IA + equipo humano
Asistente IA de AndorraDev
Andie
Hola! Soy Andie, el asistente IA de AndorraDev. ¿En qué puedo ayudarte? Si necesitas hablar con Edu, solo pídelo.
17:30