El bucle de un agente: devolver los resultados al modelo y saber cuándo parar

El bucle de un agente: devolver los resultados al modelo y saber cuándo parar

El bucle es tuyo, no del modelo

Una llamada a un modelo de lenguaje produce una respuesta y termina. El modelo no puede ejecutar una tool, esperar el resultado y seguir pensando por su cuenta: cuando pide una tool, su turno se acaba ahí. Si una persona pregunta «¿cuánto he progresado en press banca este mes?», el modelo puede pedir primero el historial de entrenamientos, pero no verá ese historial hasta que alguien se lo envíe en una llamada nueva.

Ese «alguien» es el harness: el programa que rodea al modelo. Recibe la petición, ejecuta la tool, añade el resultado a la conversación y vuelve a llamar al modelo. Repite el proceso hasta que el modelo contesta sin pedir nada más. Lo que solemos llamar «agente» es, en su núcleo, ese bucle escrito en el código de la aplicación. El modelo decide qué pedir; el bucle decide si se ejecuta, cuántas veces se repite y cuándo se para.

El ciclo completo

El bucle de tools de un agente El harness llama al modelo. Si la respuesta no pide tools, sale con la respuesta final. Si las pide y quedan rondas, las ejecuta, añade al historial el turno del modelo y los resultados, y vuelve a llamar. Si se agotan las rondas, corta el bucle. 1 Llamar al modelo historial → nuevo turno 2 ¿Pide tools? no → salida 1 Salida 1 respuesta final sí 3 ¿Quedan rondas? no → salida 2 Salida 2 fin sin tools sí 4 Ejecutar las tools código local, una a una 5 Reinyectar turno del modelo + resultados ronda + 1
Hay dos salidas: la normal, cuando el modelo deja de pedir tools, y la de seguridad, cuando se agota el tope de rondas: una última llamada sin tools para contestar con lo que ya sabe.

Cada vuelta del bucle es una ronda: una llamada al modelo, la ejecución de las tools que haya pedido y el reenvío de sus resultados. El bucle tiene dos salidas. La normal se produce cuando el modelo responde sin pedir tools: esa es la respuesta final para la persona. La de seguridad se produce cuando se agota el número máximo de rondas, aunque el modelo siga pidiendo tools.

El bucle, paso a paso

Este es el bucle en pseudocódigo. No importa el lenguaje ni la API: lo que hay que hacer es lo mismo en todos los casos. Fíjate en tres cosas: cuándo sale el bucle, qué se añade al historial en cada vuelta y cuándo se vuelve a llamar al modelo.

turn = call_model(history)

repeat at most MAX_ROUNDS times:
    if the turn asks for no tools:
        exit                           # normal exit: this is the final answer

    results = []
    for each tool request in the turn:
        result = run_tool(request.name, request.arguments)
        add (request.id, result) to results

    append the turn to the history     # what the model asked for
    append the results to the history  # what the app answered
    turn = call_model(history)

return turn

Ese es el núcleo. El repeat at most impone el tope de rondas, y el exit es la salida normal. En cada vuelta, las tools se ejecutan de una en una, en el orden en que el modelo las pidió. Es más lento que ejecutarlas en paralelo, pero evita que dos escrituras sobre los mismos datos compitan entre sí. Cambiar esa decisión es cambiar el bucle, no el modelo. Lo único que falta es qué hacer cuando el bucle termina porque se acabaron las rondas, y eso tiene su propia sección más abajo.

Qué se reinyecta y en qué orden

El modelo no recuerda nada entre llamadas. Por eso, en cada llamada recibe la conversación entera: los mensajes anteriores de la persona y del asistente y, desde que empezó el bucle, cada petición de tools con su resultado. Cada ronda añade dos cosas al final de ese historial, en este orden: primero el turno del modelo con sus peticiones (tool_use) y después un mensaje con un resultado (tool_result) por cada petición. El identificador conecta cada resultado con su petición.

El orden no es un detalle de estilo. Anthropic rechaza un tool_result que no sigue al turno con su tool_use, y un resultado sin la petición que lo motivó deja al modelo sin contexto para interpretarlo. Los tres proveedores resuelven lo mismo de formas distintas:

ProveedorQuién guarda el historial dentro del bucleQué envía el bucle en cada ronda
OpenAI (Responses)El proveedorSolo los resultados nuevos (function_call_output con su call_id) y el previous_response_id, que le dice a OpenAI a qué respuesta anterior continúa.
Anthropic (Messages)La aplicaciónEl historial entero: la conversación, el turno del modelo sin tocar y, a continuación, un mensaje con un tool_result por cada tool_use_id.
Google (Interactions)La aplicación, porque Gymnasia no guarda la interacción en GoogleEl historial entero, con cada function_call seguido de su function_result con el mismo call_id.

OpenAI es la excepción solo dentro del bucle de un mismo mensaje: como su API puede recordar una respuesta por su identificador, basta con enviarle lo nuevo. Cuando la persona escribe un mensaje nuevo, Gymnasia empieza desde cero y le envía la conversación completa, igual que a los demás. Y en todos los casos se cumple lo mismo: la petición va antes que su resultado, y cada resultado lleva el identificador de la petición a la que responde.

Por qué hace falta un tope

Nada obliga al modelo a dejar de pedir tools. Puede repetir la misma llamada porque el resultado no le convence, encadenar búsquedas sin llegar a una conclusión o reaccionar a un error pidiendo la tool otra vez. Cada ronda es una llamada de red, con su latencia y su coste en tokens, y el historial crece en cada vuelta. Sin un límite, un bucle así puede dejar a la persona esperando indefinidamente.

Gymnasia fija el tope en una constante compartida por los tres proveedores: MAX_TOOL_ROUNDS = 10. El número es una decisión de producto, no una verdad técnica. Tiene que ser lo bastante alto para las consultas reales que encadenan varias tools, como leer el historial, calcular una marca y guardar un objetivo, y lo bastante bajo para que un bucle descontrolado se corte antes de que la espera o la factura se noten.

Qué pasa cuando se agotan las rondas

Cortar el bucle es la parte fácil. Lo difícil es decidir qué se le dice a la persona. En la primera versión de Gymnasia esto estaba mal resuelto, y merece contarse porque es fácil repetirlo en cualquier harness:

  • Google lanzaba un error técnico: la respuesta seguía pendiente de tools al alcanzar el límite.
  • OpenAI y Anthropic salían del bucle sin avisar y devolvían el último turno, que todavía pedía tools. Como el texto que el modelo escribe entre rondas («voy a buscar tu historial…») se va acumulando, la persona veía esa frase a medias como si fuera la respuesta final, o un error genérico del tipo «el proveedor no devolvió contenido».

En ningún caso recibía una respuesta útil ni sabía que se había agotado el presupuesto de rondas. Cortar sin más es tirar el trabajo hecho: el agente quizá ya había leído el historial y solo le faltaba un paso.

La solución: una última llamada en la que el modelo solo puede escribir

Cuando se agotan las rondas, el bucle no corta en seco. Hace una última llamada al modelo en la que no puede pedir ninguna tool, ni las que se quedaron pendientes ni otras: solo puede contestar con texto, usando lo que ya ha averiguado.

Antes hay que resolver un detalle. El último turno del modelo todavía contiene peticiones de tools, y los proveedores exigen que cada petición tenga su resultado. Así que el bucle las responde sin ejecutarlas, con un resultado que solo dice lo que pasó. La instrucción de qué hacer ahora va aparte, en las instrucciones de sistema de esa última llamada:

# The rounds ran out and the last turn still asks for tools.
if the turn asks for tools:
    for each pending request:
        result = "Not executed: the step limit for this answer was reached."
    append the turn and those results to the history

    instructions = system_prompt + "You have reached the step limit for this answer.
        Answer with what you already know, explain the limit,
        say what was left undone and ask whether to continue."
    turn = call_model(history, instructions, tools = none)

return turn

Así la persona recibe algo como «He encontrado tu historial, pero he llegado al límite de pasos de esta respuesta y no he calculado la marca. ¿Quieres que continúe?». Si contesta que sí, el agente sigue: el tope se cuenta por mensaje, así que el mensaje nuevo arranca con otras diez rondas. Y si la tool pendiente era una escritura, como guardar un objetivo, el modelo sabe que no se hizo y no dirá lo contrario.

Por qué la instrucción no va en el resultado de la tool

La primera versión lo metía todo en el resultado: «No ejecutada… responde con lo que sabes y pregunta si quiere que continúes». Con Google funcionaba, pero con OpenAI no. El modelo trataba ese texto como un dato devuelto por una tool, no como una orden: no preguntaba, y a veces hasta decía que no tenía acceso a las herramientas.

Tiene sentido que sea así. El resultado de una tool puede contener texto que no controlas, como el de una página web o un documento, y un modelo que obedeciera las órdenes escritas ahí sería fácil de manipular. Al pasar la instrucción a las instrucciones de sistema de la llamada de cierre, las tres pruebas con OpenAI explicaron el límite y terminaron preguntando si continuar.

Regla de diseño:

el resultado de una tool cuenta qué ha pasado; lo que el modelo debe hacer a continuación va en las instrucciones de sistema.

Cómo se le dice al modelo que no use tools

Todas estas APIs tienen un parámetro, tool_choice, que decide qué puede hacer el modelo con las tools en una llamada concreta. Con auto, el valor habitual, el modelo decide si pide alguna. Con none, las tools siguen declaradas, pero el modelo no puede pedir ninguna y tiene que responder con texto. La llamada de cierre usa none. Cada proveedor lo escribe de una forma, y hay dos trampas:

ProveedorCómo se pide que no use tools en la llamada de cierre
OpenAI (Responses) y compatiblestool_choice: "none".
Anthropic (Messages)tool_choice: { type: "none" }, sin quitar la lista de tools: si el historial contiene peticiones de tools, la API exige que las tools sigan declaradas.
Google (Interactions)tool_choice: "none" dentro de generation_config. Si se pone en la raíz de la petición, la API responde con un error 400 Unknown parameter.

Si ni así hay respuesta

La llamada de cierre también puede fallar: se cae la red, o el modelo ignora la instrucción y vuelve a pedir una tool. Solo en ese caso la persona ve un error. Pero no el genérico «el proveedor no devolvió contenido», que no explica nada e invita a repetir la misma pregunta. En su lugar ve uno que dice qué ha pasado y qué puede hacer: «Esta consulta necesitaba más pasos de los que el asistente puede dar en una sola respuesta. Prueba a dividirla en preguntas más pequeñas».

Regla de diseño:

que el agente se quede sin rondas no es un fallo más. Normalmente ya ha averiguado algo útil, así que dale una última oportunidad de contarlo sin dejarle pedir más tools, que diga con claridad qué no llegó a hacer y que pregunte si debe continuar. Reserva el mensaje de error para cuando ni eso funcione, y que explique lo que ha pasado.

Tests del bucle

El bucle se puede probar entero sin red y sin modelo. Basta con un proveedor falso: una función que, en lugar de llamar a la API, devuelve turnos preparados de antemano, uno por ronda. Así cada test fija exactamente qué pide el modelo en cada vuelta y comprueba lo que hace el bucle. La misma batería se repite para OpenAI, Anthropic y Google.

Dos rondas y respuesta final

El proveedor falso pide una tool, luego otra y después contesta con texto. El bucle debe ejecutar las dos tools, llamar dos veces al modelo y devolver la respuesta final.

El orden del historial

En la segunda llamada, el historial debe contener cada turno del modelo seguido de sus resultados, ronda a ronda. Si alguien invierte las dos entradas, el test falla antes de que lo haga el proveedor real.

Sin tools, sin vueltas

Si la primera respuesta ya es texto, el bucle sale en la primera vuelta: no ejecuta ninguna tool y no vuelve a llamar al modelo.

Un modelo que no para

El proveedor falso pide tools en cada turno, sin fin. El bucle debe ejecutar exactamente el tope de rondas, responder la petición pendiente sin ejecutarla y hacer la llamada de cierre sin tools.

Para la parte del tope hay tres comprobaciones más. La primera: al llegar al límite, las peticiones pendientes reciben el resultado «No ejecutada» y no se ejecutan. La segunda: la llamada de cierre lleva tool_choice con el valor none, en el sitio que exige cada proveedor, y la instrucción de cierre en las instrucciones de sistema, solo en esa llamada. La tercera: si el cierre falla, la persona ve el mensaje que explica qué pasó, no el genérico.

¿Cómo sabemos que estos tests sirven para algo? Rompiendo el código a propósito y comprobando que fallan. Invertimos el orden del historial, subimos el tope a 11 y quitamos el tool_choice de la llamada de cierre, y en cada caso varios tests se pusieron en rojo. Un test que sigue en verde con el código roto no protege nada.

Aprender a construir un agente completo

Este artículo forma parte de una serie sobre cómo construir un agente o harness usando una app de gimnasio como caso práctico. El paso anterior fue convertir una tool call en código ejecutable; aquí hemos cerrado el ciclo, devolviendo el resultado al modelo y poniendo un límite al número de vueltas.

Ver el índice completo de la serie sobre el agente de Gymnasia.

Seguir leyendo

Últimos posts -->

¿Has visto estos proyectos?

Gymnasia

Gymnasia Gymnasia
Expo
React Native
TypeScript
OpenAI
Anthropic

App de fitness con dos agentes que se ejecutan íntegramente en el dispositivo, sin backend, de forma que los datos del usuario nunca salen del móvil. Un coach conversacional BYOK con tools locales y system prompt remoto con fallback offline, y un estimador de comidas que saca los macronutrientes de una foto del plato, con lectura de códigos de barras contra OpenFoodFacts.

LangGraph Deep Researcher

LangGraph Deep Researcher LangGraph Deep Researcher
Python
LangGraph
FastAPI
React
TypeScript
Docker

Sistema multiagente de investigación construido con LangGraph. Un supervisor descompone tu pregunta en temas y lanza subagentes de búsqueda en paralelo; cada uno comprime sus hallazgos antes de pasarlos a un agente redactor que escribe el informe final en markdown con sus fuentes. Streaming en vivo por WebSockets, modelo configurable por rol y claves de API propias que nunca se guardan en el servidor.

Tau

Tau Tau
Python
LangChain

Sistema multiagente de tutoría para estudiantes de secundaria, con un agente por asignatura y material de curso elaborado y validado por un equipo de profesores. Llegó a usarse con alumnos reales en un colegio privado en España y en un instituto en Colombia.

Ver todos los proyectos -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres ver alguna charla?

Últimas charlas -->

¿Quieres mejorar con estos tips?

Últimos tips -->

Usa esto en local

Los espacios de Hugging Face nos permite ejecutar modelos con demos muy sencillas, pero ¿qué pasa si la demo se rompe? O si el usuario la elimina? Por ello he creado contenedores docker con algunos espacios interesantes, para poder usarlos de manera local, pase lo que pase. De hecho, es posible que si pinchas en alún botón de ver proyecto te lleve a un espacio que no funciona.

Flow edit

Flow edit Flow edit

Edita imágenes con este modelo de Flow. Basándose en SD3 o FLUX puedes editar cualquier imagen y generar nuevas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos los contenedores -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres entrenar tu modelo con estos datasets?

short-jokes-dataset

HuggingFace

Dataset de chistes en inglés

Uso: Fine-tuning de modelos de generación de texto humorístico

231K filas 2 columnas 45 MB
Ver en HuggingFace →

opus100

HuggingFace

Dataset con traducciones de inglés a español

Uso: Entrenamiento de modelos de traducción inglés-español

1M filas 2 columnas 210 MB
Ver en HuggingFace →

netflix_titles

HuggingFace

Dataset con películas y series de Netflix

Uso: Análisis de catálogo de Netflix y sistemas de recomendación

8.8K filas 12 columnas 3.5 MB
Ver en HuggingFace →
Ver más datasets -->