Introducción a las aplicaciones de chat de visión multimodal con Azure OpenAI

En este artículo se muestra cómo usar los modelos multimodales de Azure OpenAI para generar respuestas a mensajes de usuario e imágenes cargadas en una aplicación de chat. Este ejemplo de aplicación de chat también incluye toda la infraestructura y la configuración necesarias para aprovisionar Azure recursos de OpenAI e implementar la aplicación en Azure Container Apps mediante la CLI para desarrolladores de Azure.

Siguiendo las instrucciones de este artículo, podrá:

  • Implemente una aplicación de chat de contenedor de Azure que use la identidad administrada para la autenticación.
  • Cargue imágenes que se usarán como parte de la secuencia de chat.
  • Chatear con un modelo de lenguaje grande multimodal de Azure OpenAI mediante la API de Respuestas de la biblioteca OpenAI.

Una vez finalizado este artículo, puede comenzar a modificar el nuevo proyecto con su código personalizado.

Nota:

En este artículo se usan una o varias plantillas de aplicaciones de IA como base para los ejemplos e instrucciones del artículo. Las plantillas de aplicaciones de IA le proporcionan implementaciones de referencia bien mantenidas y fáciles de implementar que le ayudan a garantizar un punto inicial de alta calidad para sus aplicaciones de IA.

Introducción a la arquitectura

En el diagrama siguiente se muestra una arquitectura sencilla de la aplicación de chat: Diagrama que muestra la arquitectura del cliente a la aplicación back-end.

La aplicación de chat se ejecuta como una aplicación contenedora de Azure. La aplicación usa la identidad administrada a través de Microsoft Entra ID para autenticarse con Azure OpenAI en producción, en lugar de una clave de API. Durante el desarrollo, la aplicación admite varios métodos de autenticación, como Azure credenciales de la CLI para desarrolladores y claves de API.

La arquitectura de la aplicación se basa en los siguientes servicios y componentes:

  • Azure OpenAI representa el proveedor de IA al que se envían las consultas del usuario.
  • Azure Container Apps es el entorno de contenedor donde se hospeda la aplicación.
  • La identidad administrada nos ayuda a garantizar la mejor seguridad de la clase y elimina el requisito de que sea un desarrollador para administrar de forma segura un secreto.
  • Archivos Bicep para el aprovisionamiento de recursos de Azure, incluidos Azure OpenAI, Azure Container Apps, Azure Container Registry, Azure Log Analytics y el control de acceso en función de roles (RBAC).
  • Una aplicación Python Quart que usa el paquete openai para generar respuestas a mensajes de usuario con archivos de imagen cargados.
  • Un front-end HTML/JavaScript básico que transmite respuestas desde el back-end mediante JSON Lines a través de un ReadableStream.

Costos

En un intento de mantener los precios tan bajos como sea posible en este ejemplo, la mayoría de los recursos usan un plan de tarifa básico o de consumo. Modifique el nivel de nivel según sea necesario en función del uso previsto. Para dejar de incurrir en cargos, elimine los recursos cuando haya terminado con el artículo.

Obtenga más información sobre cost en el repositorio de ejemplo.

Requisitos previos

Hay disponible un entorno contenedor de desarrollo con todas las dependencias necesarias para completar este artículo. Puede ejecutar el contenedor de desarrollo en GitHub Codespaces (en un explorador) o localmente mediante Visual Studio Code.

Para usar este artículo, debe cumplir los siguientes requisitos previos:

Entorno de desarrollo abierto

Use las instrucciones siguientes para implementar un entorno de desarrollo preconfigurado que contenga todas las dependencias necesarias para completar este artículo.

GitHub Codespaces ejecuta un contenedor de desarrollo administrado por GitHub con Visual Studio Code para web como interfaz de usuario. Para el entorno de desarrollo más sencillo, use GitHub Codespaces para que tenga las herramientas de desarrollo y las dependencias correctas preinstaladas para completar este artículo.

Importante

Todas las cuentas de GitHub pueden usar Codespaces durante un máximo de 60 horas gratis cada mes con dos instancias principales. Para obtener más información, consulte almacenamiento incluido mensualmente y horas de núcleo de GitHub Codespaces.

Siga estos pasos para crear un nuevo GitHub Codespace en la rama main del repositorio de Azure-Samples/openai-chat-vision-quickstart GitHub.

  1. Haga clic con el botón derecho en el botón siguiente y seleccione Abrir vínculo en la nueva ventana. Esta acción le permite tener el entorno de desarrollo y la documentación disponible para su revisión.

    Abrir en GitHub Codespaces

  2. En la página Crear espacio de códigos , revise y seleccione Crear nuevo espacio de código.

  3. Espere a que se inicie Codespaces. Este proceso de startup puede tardar unos minutos.

  4. Inicie sesión en Azure con la CLI para desarrolladores de Azure en el terminal situado en la parte inferior de la pantalla.

    azd auth login
    
  5. Copie el código del terminal y péguelo en un navegador. Siga las instrucciones para autenticarse con su cuenta de Azure.

Las tareas restantes de este artículo tienen lugar en el contexto de este contenedor de desarrollo.

Despliegue y ejecución

El repositorio de ejemplo contiene todos los archivos de código y de configuración para la implementación de la aplicación de chat en Azure. Los siguientes pasos le guiarán a través del proceso de implementación de la aplicación de chat de ejemplo en Azure.

Implementación de la aplicación de chat en Azure

Importante

Para mantener bajos los costos, en este ejemplo se usan planes de tarifa básicos o de consumo para la mayoría de los recursos. Ajuste el nivel según sea necesario y elimine los recursos cuando haya terminado para evitar cargos.

  1. Ejecute el siguiente comando Azure Developer CLI para el aprovisionamiento de recursos de Azure y la implementación del código fuente:

    azd up
    
  2. Use la siguiente tabla para responder a las solicitudes:

    Prompt Respuesta
    Nombre del entorno Hágala corta y en minúsculas. Agregue su nombre o alias. Por ejemplo, chat-vision. Se usa como parte del nombre del grupo de recursos.
    Suscripción Seleccione la suscripción en la que crear los recursos.
    Ubicación (para el hospedaje) Seleccione una ubicación cercana en la lista.
    Ubicación del modelo de OpenAI de Azure Seleccione una ubicación cercana en la lista. Si está disponible la misma ubicación que la primera, selecciónela.
  3. Espere hasta que se implemente la aplicación. La implementación suele tardar entre 5 y 10 minutos en completarse.

Uso de la aplicación de chat para formular preguntas al modelo de lenguaje grande

  1. El terminal muestra una dirección URL después de una implementación correcta de la aplicación.

  2. Seleccione esa URL etiquetada Deploying service web para abrir la aplicación de chat en un navegador.

    Captura de pantalla que muestra una imagen cargada, una pregunta sobre la imagen, la respuesta de la inteligencia artificial y el cuadro de texto.

  3. En el explorador, cargue una imagen haciendo clic en Elegir archivo y seleccionando una imagen.

  4. Haga una pregunta sobre la imagen cargada, como "¿Cuál es la imagen?".

  5. La respuesta procede de Azure OpenAI y se muestra el resultado.

Exploración del código de ejemplo

En este ejemplo se utiliza un modelo multimodal de Azure OpenAI para generar respuestas a mensajes de usuario e imágenes cargadas.

Codificación base64 de la imagen cargada en el front-end

La imagen cargada debe estar codificada en Base64 para que se pueda usar directamente como un URI de datos como parte del mensaje.

En el ejemplo, el siguiente fragmento de código de front-end en la scriptetiqueta del src/quartapp/templates/index.html archivo controla esa funcionalidad. La toBase64 función de flecha usa el readAsDataURL método del FileReader para leer de forma asincrónica el archivo de imagen cargado como una cadena codificada en base64.

    const toBase64 = file => new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
    });

La función toBase64 es llamada por un oyente en el evento submit del formulario.

El submit escuchador de eventos controla el flujo completo de interacción del chat. Cuando el usuario envía un mensaje, se produce el siguiente flujo:

  1. Obtiene el archivo de imagen cargado (si está presente) y codifica como Base64.
  2. Crea y muestra el mensaje del usuario en el chat, incluida la imagen cargada.
  3. Prepara un contenedor de mensaje de asistente con un indicador de "Escribiendo..."
  4. Agrega el mensaje del usuario al array del historial de mensajes en formato de la API de respuestas
  5. Envía una fetch solicitud POST al /chat/stream punto de conexión con el historial de mensajes y el contexto (incluida la imagen codificada en Base64 y el nombre de archivo)
  6. Procesa la respuesta de líneas JSON transmitidas para mostrar cada texto de forma incremental
  7. Controla los errores durante el streaming.
  8. Agrega un botón de salida de voz después de recibir la respuesta completa para que los usuarios puedan escuchar la respuesta.
  9. Borra el campo de entrada y devuelve el foco para el siguiente mensaje.
form.addEventListener("submit", async function(e) {
    e.preventDefault();

    // Hide the no-messages-heading when a message is added
    document.getElementById("no-messages-heading").style.display = "none";

    const file = document.getElementById("file").files[0];
    const fileData = file ? await toBase64(file) : null;

    const message = messageInput.value;

    const userTemplateClone = userTemplate.content.cloneNode(true);
    userTemplateClone.querySelector(".message-content").innerText = message;
    if (file) {
        const img = document.createElement("img");
        img.src = fileData;
        userTemplateClone.querySelector(".message-file").appendChild(img);
    }
    targetContainer.appendChild(userTemplateClone);

    const assistantTemplateClone = assistantTemplate.content.cloneNode(true);
    let messageDiv = assistantTemplateClone.querySelector(".message-content");
    targetContainer.appendChild(assistantTemplateClone);

    messages.push({
        "role": "user",
        "content": [{"type": "input_text", "text": message}]
    });

    try {
        messageDiv.scrollIntoView();
        const response = await fetch("/chat/stream", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({
                messages: messages,
                context: {
                    file: fileData,
                    file_name: file ? file.name : null
                }
            })
        });

        if (!response.ok || !response.body) {
            throw new Error(`Request failed (${response.status})`);
        }

        let answer = "";
        for await (const chunk of readNDJSONStream(response.body)) {
            if (chunk.type === "error" || chunk.type === "response.failed") {
                messageDiv.innerHTML = "Error: " + (chunk.error || "Unknown error");
                break;
            }
            if (chunk.type === "response.output_text.delta") {
                // Clear out the DIV if its the first answer chunk we've received
                if (answer == "") {
                    messageDiv.innerHTML = "";
                }
                answer += chunk.delta;
                messageDiv.innerHTML = converter.makeHtml(answer);
                messageDiv.scrollIntoView();
            }
        }

        messages.push({
            "role": "assistant",
            "content": [{"type": "output_text", "text": answer}]
        });

        messageInput.value = "";

        const speechOutput = document.createElement("speech-output-button");
        speechOutput.setAttribute("text", answer);
        messageDiv.appendChild(speechOutput);
        messageInput.focus();
    } catch (error) {
        messageDiv.innerHTML = "Error: " + error;
    }
});

Manejo de la imagen en el backend

En el src\quartapp\chat.py archivo, el código de back-end para el control de imágenes comienza después de configurar la autenticación sin claves.

Nota:

Para obtener más información sobre cómo usar conexiones sin claves para la autenticación y autorización para Azure OpenAI, consulte el artículo Get started with the Azure OpenAI security building block Microsoft Learn .

Configuración de autenticación

La configure_openai() función configura el cliente de OpenAI antes de que la aplicación empiece a atender solicitudes. Usa el decorador de @bp.before_app_serving Quart para configurar la autenticación basada en variables de entorno. Este sistema flexible permite a los desarrolladores trabajar en diferentes contextos sin cambiar el código.

Modos de autenticación explicados
  • Desarrollo local (OPENAI_HOST=local): se conecta a un servicio de API compatible con OpenAI local (como Ollama o LocalAI) sin autenticación. Use este modo para realizar pruebas sin costos de Internet o API.
  • Azure OpenAI con clave de API (AZURE_OPENAI_KEY_FOR_CHATVISION variable de entorno): usa una clave de API para la autenticación. Evite este modo en producción porque las claves de API requieren rotación manual y suponen riesgos de seguridad si se exponen. Úselo para realizar pruebas locales dentro de un contenedor de Docker sin CLI de Azure credenciales.
  • Production with Managed Identity (RUNNING_IN_PRODUCTION=true): usa ManagedIdentityCredential para autenticarse con Azure OpenAI a través de la identidad administrada de la aplicación contenedora. Este método se recomienda para producción porque elimina la necesidad de administrar secretos. Azure Container Apps proporciona automáticamente la identidad administrada y concede permisos durante la implementación a través de Bicep.
  • Desarrollo con CLI de Azure (modo predeterminado): Usa AzureDeveloperCliCredential para autenticarse con Azure OpenAI mediante credenciales de CLI de Azure de inicio de sesión local. Este modo simplifica el desarrollo local sin administrar claves de API.
Detalles clave de implementación
  • La función get_bearer_token_provider() actualiza las credenciales de Azure y las usa como tokens portadores.
  • La ruta de acceso del punto de conexión de OpenAI de Azure termina con /openai/v1/, el punto de conexión compatible con OpenAI que está disponible en general para los modelos de Foundry de Microsoft.
  • La función es asincrónica, ya que Quart es un marco de aplicación web asincrónica. Quart permite que los controladores de solicitudes sean asincrónicos, por lo que mientras la aplicación espera respuestas lentas de la API LLM, el servidor puede seguir controlando otras solicitudes.

Este es el código de configuración de autenticación completo de chat.py:

@bp.before_app_serving
async def configure_openai():
    bp.model_name = os.getenv("OPENAI_MODEL", "gpt-4o")
    openai_host = os.getenv("OPENAI_HOST", "azure")

    if openai_host == "local":
        bp.openai_client = AsyncOpenAI(api_key="no-key-required", base_url=os.getenv("LOCAL_OPENAI_ENDPOINT"))
        current_app.logger.info("Using local OpenAI-compatible API service with no key")
    elif os.getenv("AZURE_OPENAI_KEY_FOR_CHATVISION"):
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=os.getenv("AZURE_OPENAI_KEY_FOR_CHATVISION"),
        )
        current_app.logger.info("Using Azure OpenAI with key")
    elif os.getenv("RUNNING_IN_PRODUCTION"):
        client_id = os.environ["AZURE_CLIENT_ID"]
        azure_credential = ManagedIdentityCredential(client_id=client_id)
        token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default")
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=token_provider,
        )
        current_app.logger.info("Using Azure OpenAI with managed identity credential for client ID %s", client_id)
    else:
        tenant_id = os.environ["AZURE_TENANT_ID"]
        azure_credential = AzureDeveloperCliCredential(tenant_id=tenant_id)
        token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default")
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"].rstrip("/") + "/openai/v1",
            api_key=token_provider,
        )
        current_app.logger.info("Using Azure OpenAI with az CLI credential for tenant ID: %s", tenant_id)

Función de controlador de chat

La chat_handler() función procesa las solicitudes de chat enviadas al punto de conexión /chat/stream. Recibe una solicitud POST con una carga JSON.

La carga JSON incluye:

  • messages: una lista del historial de conversaciones. Cada mensaje tiene un role ("usuario" o "asistente") y content (una matriz de elementos de contenido mediante el formato de entrada de la API de respuestas).
  • context: datos adicionales para el procesamiento, entre los que se incluyen:
    • file: datos de imagen codificados en Base64 (por ejemplo, data:image/png;base64,...).
    • file_name: nombre de archivo original de la imagen cargada (útil para registrar o identificar el tipo de imagen).

El controlador extrae el historial de mensajes y los datos de imagen. Si no se carga ninguna imagen, el valor de la imagen es nully el código controla este caso.

@bp.post("/chat/stream")
async def chat_handler():
    request_json = await request.get_json()
    request_messages = request_json["messages"]
    # Get the base64 encoded image from the request context
    # This will be None if no image was uploaded
    image = request_json["context"]["file"]

Creación de la matriz de entrada para solicitudes de visión

La función response_stream() prepara la matriz de entrada que se envía a la API de respuestas de OpenAI de Azure. El @stream_with_context decorador mantiene el contexto de solicitud mientras transmite la respuesta.

Lógica de preparación de entrada

  1. Comience con el historial de conversaciones: la función comienza con all_input, que incluye todos los mensajes anteriores, excepto el más reciente (request_messages[0:-1]). Los mensajes ya están en formato de API de respuestas desde el front-end.
  2. Controle el mensaje de usuario actual en función de la presencia de la imagen:
    • Con imagen: añada una parte del input_image contenido a la matriz de contenido existente del usuario.
    • Sin imagen: anexe el mensaje del usuario as-is.
    @stream_with_context
    async def response_stream():
        # This sends all messages, so API request may exceed token limits
        all_input = list(request_messages[0:-1])

        # Add the current user message, appending image if provided
        if image:
            user_content = request_messages[-1]["content"] + [{"type": "input_image", "image_url": image}]
            all_input.append({"role": "user", "content": user_content})
        else:
            all_input.append(request_messages[-1])

A continuación, bp.openai_client.responses.create llama a la API de respuestas de OpenAI Azure y transmite la respuesta. El store=False parámetro indica a la API que no almacene respuestas en el servidor, lo que hace que la llamada no tenga estado.

        openai_stream = await bp.openai_client.responses.create(
            model=bp.model_name,
            input=all_input,
            stream=True,
            temperature=0.3,
            store=False,
        )

Por último, la respuesta se devuelve al cliente. La API de respuestas emite muchos tipos de eventos, pero solo se necesita el evento para transmitir response.output_text.delta texto generado. Los eventos de error se registran y se reenvían a la interfaz de usuario.

        try:
            async for event in openai_stream:
                if event.type == "response.output_text.delta":
                    yield json.dumps({"type": event.type, "delta": event.delta}, ensure_ascii=False) + "\n"
                elif event.type in ("response.failed", "error"):
                    current_app.logger.error("Responses API error: %s", event)
                    yield json.dumps({"type": event.type}, ensure_ascii=False) + "\n"
        except Exception as e:
            current_app.logger.exception("Error in response stream")
            yield json.dumps({"error": str(e)}, ensure_ascii=False) + "\n"

    return Response(response_stream())

Bibliotecas y características de front-end

El front-end usa las API y bibliotecas modernas del explorador para crear una experiencia de chat interactiva. Los desarrolladores pueden personalizar la interfaz o agregar características mediante la comprensión de estos componentes:

  1. Entrada y salida de voz: los componentes web personalizados usan las API de voz del explorador:

    • <speech-input-button>: convierte la voz en texto mediante la API de SpeechRecognitionVoz Web. Proporciona un botón de micrófono que escucha la entrada de voz y emite un speech-input-result evento con el texto transcrito.

    • <speech-output-button>: lee el texto en voz alta mediante la SpeechSynthesis API. Aparece después de cada respuesta del asistente con un icono de altavoz, lo que permite a los usuarios escuchar la respuesta.

    ¿Por qué usar las API del navegador en lugar de Azure Speech Services?

    • Sin costo: se ejecuta completamente en el explorador
    • Respuesta instantánea: sin latencia de red
    • Privacidad: los datos de voz permanecen en el dispositivo del usuario
    • No es necesario usar recursos adicionales de Azure

    Estos componentes están en src/quartapp/static/speech-input.js y speech-output.js.

  2. Vista previa de la imagen: muestra la imagen cargada en el chat antes de enviar el análisis para su confirmación. La vista previa se actualiza automáticamente cuando se selecciona un archivo.

    fileInput.addEventListener("change", async function() {
        const file = fileInput.files[0];
        if (file) {
            const fileData = await toBase64(file);
            imagePreview.src = fileData;
            imagePreview.style.display = "block";
        }
    });
    
  3. Bootstrap 5 e Iconos de Bootstrap: proporciona componentes de interfaz de usuario adaptables e iconos. La aplicación usa el tema Cosmo de Bootswatch para un aspecto moderno.

  4. Representación de mensajes basada en plantillas: usa elementos HTML <template> para diseños de mensajes reutilizables, lo que garantiza un estilo y una estructura coherentes.

Otros recursos de ejemplo que se van a explorar

Además del ejemplo de aplicación de chat, hay otros recursos en el repositorio para explorar para obtener más información. Consulte los cuadernos siguientes en el notebooks directorio :

Notebook Descripción
chat_pdf_images.ipynb En este cuaderno se muestra cómo convertir páginas PDF en imágenes y enviarlos a un modelo de visión para la inferencia.
chat_vision.ipynb Este cuaderno se proporciona para la experimentación manual con el modelo de visión que se usa en la aplicación.

Contenido localizado: las versiones en español de los cuadernos están en el notebooks/Spanish/ directorio y ofrecen el mismo aprendizaje práctico para desarrolladores de habla española. Los cuadernos inglés y español muestran:

  • Cómo llamar a modelos de visión directamente para la experimentación
  • Cómo convertir páginas PDF en imágenes para el análisis
  • Cómo ajustar parámetros e indicaciones de prueba

Limpieza de recursos

Limpieza de recursos de Azure

Los recursos de Azure creados en este artículo se facturan a su suscripción de Azure. Si no espera necesitar estos recursos en el futuro, elimínelos para evitar incurrir en más gastos.

Para eliminar los recursos de Azure y quitar el código fuente, ejecute el siguiente comando de la CLI para desarrolladores de Azure:

azd down --purge

Limpieza de GitHub Codespaces

Al eliminar el entorno de GitHub Codespaces, se asegura que puedas maximizar la cantidad de horas gratuitas por núcleo disponibles para tu cuenta.

Importante

Para obtener más información sobre los derechos de su cuenta de GitHub, consulte almacenamiento y horas centrales mensuales incluidas en GitHub Codespaces.

  1. Inicie sesión en el panel GitHub Codespaces.

  2. Localice los Codespaces que se ejecutan actualmente en el repositorio de Azure-Samples//openai-chat-vision-quickstart GitHub.

  3. Abra el menú contextual delcodespace y seleccione Eliminar.

Obtener ayuda

Registre su problema en los Issues del repositorio.

Pasos siguientes