通过 Azure OpenAI 开始使用多模式视觉聊天应用

本文介绍如何使用 Azure OpenAI 多模式模型在聊天应用中生成对用户消息和上传图像的响应。 此聊天应用示例还包括预配 Azure OpenAI 资源以及使用 Azure 开发人员 CLI 将应用部署到 Azure 容器应用所需的所有基础结构和配置。

按照本文中的说明操作,你将:

  • 部署使用托管标识进行身份验证的 Azure 容器聊天应用。
  • 上传要用作聊天流的一部分的图像。
  • 使用 OpenAI 库与 Azure OpenAI 多模式大型语言模型(LLM)聊天。

完成此文章后,可以使用自定义代码开始修改新项目。

注意

本文使用一个或多个 AI 应用模板作为本文中的示例和指南的基础。 AI 应用模板为你提供了维护良好、易于部署的参考实现,可帮助确保 AI 应用有一个高质量的起点。

体系结构概述

下图显示了聊天应用的简单体系结构: 显示从客户端到后端应用的体系结构的示意图。

聊天应用作为 Azure 容器应用运行。 应用通过 Microsoft Entra ID 使用托管标识在生产环境中通过 Azure OpenAI 进行身份验证,而不是 API 密钥。 在开发过程中,应用支持多种身份验证方法,包括 Azure 开发人员 CLI 凭据、API 密钥和 GitHub 模型,以便在不使用 Azure 资源的情况下进行测试。

应用程序体系结构依赖于以下服务和组件:

  • Azure OpenAI 表示我们向其发送用户查询的 AI 提供程序。
  • Azure 容器应用是托管应用程序的容器环境。
  • 托管标识 可帮助我们确保一流的安全性,并消除了开发人员安全管理机密的要求。
  • 用于预配 Azure 资源的 Bicep 文件,包括 Azure OpenAI、Azure 容器应用、Azure 容器注册表、Azure Log Analytics 和基于角色的访问控制(RBAC)角色。
  • Microsoft AI 聊天协议提供了跨 AI 解决方案和语言的标准化 API 协定。 聊天应用符合 Microsoft AI 聊天协议。
  • 一个 Python Quart ,它 openai 使用包生成对包含上传图像文件的用户消息的响应。
  • 基本 HTML/JavaScript 前端,它使用 JSON 行通过 ReadableStream 从后端流式传输响应。

成本

为了尽量降低此示例中的定价,大多数资源都使用基本定价层或消耗定价层。 根据需要根据预期使用情况更改层级别。 若要停止产生费用,在完成本文后删除资源。

在示例存储库中了解有关成本的详细信息

先决条件

开发容器 环境提供了完成本文所需的所有依赖项。 可以在 GitHub Codespaces(在浏览器中)或在本地使用 Visual Studio Code 运行开发容器。

若要使用本文,需要满足以下先决条件:

打开开发环境

按照以下说明部署预配置开发环境,其中包含完成本文所需的所有依赖项。

GitHub Codespaces 运行由 GitHub 托管的开发容器,将 Visual Studio Code 网页版作为用户界面。 对于最简单的开发环境,请使用 GitHub Codespaces,以便预先安装完成本文所需的合适的开发人员工具和依赖项。

重要

所有 GitHub 帐户每月最多可使用 Codespaces 60 小时,其中包含两个核心实例。 有关详细信息,请参阅 GitHub Codespaces 每月包含的存储和核心小时数

使用以下步骤在 GitHub 存储库的main分支上Azure-Samples/openai-chat-vision-quickstart创建新的 GitHub Codespace。

  1. 右键单击以下按钮,然后在新窗口中选择“打开”链接。 此操作允许你拥有可供查看的开发环境和文档。

    在 GitHub Codespaces 中打开

  2. “创建代码空间”页上,查看并选择“创建新代码空间”

  3. 等待 Codespace 启动。 此启动过程会花费几分钟时间。

  4. 使用屏幕底部终端中的 Azure 开发人员 CLI 登录到 Azure。

    azd auth login
    
  5. 从终端复制代码,然后将其粘贴到浏览器中。 按照说明使用 Azure 帐户进行身份验证。

本文中的剩余任务需要在此开发容器的上下文中完成。

部署和运行

示例存储库包含聊天应用 Azure 部署的所有代码和配置文件。 以下步骤将引导你完成示例聊天应用 Azure 部署过程。

将聊天应用部署到 Azure

重要

为了降低成本,此示例对大多数资源使用基本或消耗定价层。 根据需要调整分层,并在完成后删除资源,以避免产生费用。

  1. 针对 Azure 资源预配和源代码部署运行以下 Azure 开发人员 CLI 命令:

    azd up
    
  2. 使用下表回答提示:

    提示 Answer
    环境名称 保持简短和小写。 添加名称或别名。 例如,chat-vision。 它用作资源组名称的一部分。
    订阅 选择要在其中创建资源的订阅。
    位置(用于托管) 从列表中选择附近的位置。
    Azure OpenAI 模型的位置 从列表中选择附近的位置。 如果可以使用与第一个位置相同的位置,请选择该位置。
  3. 等待应用部署完成。 部署通常需要 5 到 10 分钟才能完成。

使用聊天应用向大型语言模型提问

  1. 终端在成功部署应用程序后显示 URL。

  2. 选择标记为 Deploying service web 的 URL 在浏览器中打开聊天应用程序。

    显示上传的图像、有关图像的问题、AI 的响应和文本框的屏幕截图。

  3. 在浏览器中,通过单击 “选择文件 ”并选择图像来上传图像。

  4. 询问有关上传的图像的问题,例如“图像是什么?”

  5. 答案来自 Azure OpenAI 并显示结果。

浏览示例代码

虽然 OpenAI 和 Azure OpenAI 服务依赖于 常见的 Python 客户端库,但使用 Azure OpenAI 终结点时需要进行少量代码更改。 此示例使用 Azure OpenAI 多模式模型生成对用户消息和上传图像的响应。

Base64 对前端中上传的图像进行编码

上传的图像需要经过 Base64 编码,以便可以直接用作数据 URI 作为消息的一部分。

在示例中,文件标记script中的src/quartapp/templates/index.html以下前端代码片段处理该功能。 toBase64箭头函数使用readAsDataURL上传的图像文件中异步读取的方法FileReader作为 base64 编码字符串。

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

函数 toBase64 由窗体 submit 事件的侦听器调用。

事件 submit 侦听器处理完整的聊天交互流。 当用户提交消息时,会按照以下流程进行:

  1. 隐藏“no-messages-heading”元素以便显示对话已启动
  2. 获取上传的图像文件并将其进行 Base64 编码(如果存在)
  3. 在聊天中创建并显示用户的消息,包括上传的图像
  4. 准备一个带有“正在输入...”指示器的助理消息容器
  5. 将用户的消息添加到消息历史记录数组
  6. 调用 AI 聊天协议客户端的getStreamedCompletion()方法,使用消息历史记录和上下文(包括 Base64 编码的图像和文件名)
  7. 使用 Showdown.js 处理流式响应块并将 Markdown 转换为 HTML
  8. 处理流媒体过程中出现的任何错误
  9. 在收到完整的响应后添加语音输出按钮,以便用户可以听到响应
  10. 清除输入字段并返回下一条消息的焦点
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": message
    });

    try {
        messageDiv.scrollIntoView();
        const result = await client.getStreamedCompletion(messages, {
            context: {
                file: fileData,
                file_name: file ? file.name : null
            }
        });

        let answer = "";
        for await (const response of result) {
            if (!response.delta) {
                continue;
            }
            if (response.delta.content) {
                // Clear out the DIV if its the first answer chunk we've received
                if (answer == "") {
                    messageDiv.innerHTML = "";
                }
                answer += response.delta.content;
                messageDiv.innerHTML = converter.makeHtml(answer);
                messageDiv.scrollIntoView();
            }
            if (response.error) {
                messageDiv.innerHTML = "Error: " + response.error;
            }
        }
        messages.push({
            "role": "assistant",
            "content": 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;
    }
});

使用后端处理映像

src\quartapp\chat.py 文件中,用于映像处理的后端代码在配置无密钥身份验证后开始。

注意

有关如何使用无密钥连接进行身份验证和授权到 Azure OpenAI 的详细信息,请参阅 Azure OpenAI 安全构建基块 入门Microsoft Learn 一文。

身份验证配置

configure_openai() 函数在应用开始处理请求之前设置 OpenAI 客户端。 它使用 Quart 的 @bp.before_app_serving 修饰器基于环境变量配置身份验证。 此灵活的系统允许开发人员在不同的上下文中工作,而无需更改代码。

身份验证模式说明
  • 本地开发OPENAI_HOST=local):在没有身份验证的情况下连接到与 OpenAI 兼容的本地 API 服务(如 Ollama 或 LocalAI)。 使用此模式进行测试,无需 Internet 或 API 成本。
  • GitHub 模型 (OPENAI_HOST=github):使用 GitHub 的 AI 模型市场,并通过 GITHUB_TOKEN 进行身份验证。 使用 GitHub 模型时,将模型名称加上 openai/ 前缀(例如, openai/gpt-4o)。 此模式允许开发人员在预配 Azure 资源之前试用模型。
  • Azure OpenAI 与 API 密钥AZURE_OPENAI_KEY_FOR_CHATVISION 环境变量):用于身份验证的 API 密钥。 避免在生产环境中使用此模式,因为 API 密钥需要手动轮换,并在暴露时带来安全风险。 在不使用 Azure CLI 凭据的情况下,将其用于 Docker 容器内的本地测试。
  • 使用托管标识进行生产RUNNING_IN_PRODUCTION=true):用于 ManagedIdentityCredential 通过容器应用的托管标识通过 Azure OpenAI 进行身份验证。 建议将此方法用于生产,因为它无需管理机密。 Azure 容器应用通过 Bicep 在部署期间自动提供托管标识和授予权限。
  • 使用 Azure CLI 进行开发 (默认模式):使用 AzureDeveloperCliCredential 本地登录的 Azure CLI 凭据通过 Azure OpenAI 进行身份验证。 此模式简化了本地开发,而无需管理 API 密钥。
关键实现详细信息
  • get_bearer_token_provider() 函数会刷新 Azure 凭据,并将其用作持有者令牌。
  • Azure OpenAI 终结点路径中包含 /openai/v1/ 以满足 OpenAI 客户端库的要求。
  • 日志记录显示哪个身份验证模式处于活动状态。
  • 该函数是为了支持 Azure 凭据操作而设计的异步函数。

下面是来自 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", "github")

    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 openai_host == "github":
        bp.model_name = f"openai/{bp.model_name}"
        bp.openai_client = AsyncOpenAI(
            api_key=os.environ["GITHUB_TOKEN"],
            base_url="https://models.github.ai/inference",
        )
        current_app.logger.info("Using GitHub models with GITHUB_TOKEN as key")
    elif os.getenv("AZURE_OPENAI_KEY_FOR_CHATVISION"):
        bp.openai_client = AsyncOpenAI(
            base_url=os.environ["AZURE_OPENAI_ENDPOINT"],
            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"] + "/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"] + "/openai/v1/",
            api_key=token_provider,
        )
        current_app.logger.info("Using Azure OpenAI with az CLI credential for tenant ID: %s", tenant_id)

聊天处理程序函数

chat_handler() 函数处理发送到终结点的 /chat/stream 聊天请求。 它接收一个具有遵循 MICROSOFT AI 聊天协议的 JSON 有效负载的 POST 请求。

JSON 有效负载包括:

  • 消息:对话历史记录列表。 每条消息都有一个 role (“user”或“assistant”)和 content (消息文本)。
  • context:用于处理的额外数据,包括:
    • 文件:Base64 编码的图像数据(例如 data:image/png;base64,...)。
    • file_name:上传的图像的原始文件名(可用于记录或标识图像类型)。
  • 温度 (可选):控制响应随机性的浮点数(默认值为 0.5)。

处理程序提取消息历史记录和图像数据。 如果未上传图像,则图像值为 null,代码将处理这种情况。

@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"]
    # The context also includes the filename for reference
    # file_name = request_json["context"]["file_name"]

为视觉请求生成消息数组

response_stream() 函数准备发送到 Azure OpenAI API 的消息数组。 @stream_with_context修饰器在流式处理响应时保留请求上下文。

消息准备逻辑

  1. 从对话历史记录开始:函数以all_messages开头,其中包括系统消息和除最近一条消息() 以外的所有以前的消息。request_messages[0:-1]
  2. 基于图像状态处理当前用户消息
    • 使用图像:使用文本和image_url对象将用户的消息格式化为多部分内容数组。 该 image_url 对象包含 Base64 编码的图像数据和参数 detail
    • 没有图像:将用户的消息追加为纯文本。
  3. 参数detail:设置为“auto”,以便模型根据图像大小在“低”和“高”详细信息之间进行选择。 低细节更快、更便宜,而高细节为复杂图像提供更准确的分析。
    @stream_with_context
    async def response_stream():
        # This sends all messages, so API request may exceed token limits
        all_messages = [
            {"role": "system", "content": "You are a helpful assistant."},
        ] + request_messages[0:-1]
        all_messages = request_messages[0:-1]
        if image:
            user_content = []
            user_content.append({"text": request_messages[-1]["content"], "type": "text"})
            user_content.append({"image_url": {"url": image, "detail": "auto"}, "type": "image_url"})
            all_messages.append({"role": "user", "content": user_content})
        else:
            all_messages.append(request_messages[-1])

注意

有关 detail 图像参数和相关设置的更多信息,请查阅 Microsoft Learn 文章 “使用启用视觉的聊天模型” 中的 详细参数设置 部分。

接下来, bp.openai_client.chat.completions 通过 Azure OpenAI API 调用获取聊天完成并流式传输响应。

        chat_coroutine = bp.openai_client.chat.completions.create(
            # Azure OpenAI takes the deployment name as the model name
            model=bp.model_name,
            messages=all_messages,
            stream=True,
            temperature=request_json.get("temperature", 0.5),
        )

最后,响应将流式传输到客户端,并针对任何异常进行错误处理。

        try:
            async for event in await chat_coroutine:
                event_dict = event.model_dump()
                if event_dict["choices"]:
                    yield json.dumps(event_dict["choices"][0], ensure_ascii=False) + "\n"
        except Exception as e:
            current_app.logger.error(e)
            yield json.dumps({"error": str(e)}, ensure_ascii=False) + "\n"

    return Response(response_stream())

前端库和功能

前端使用新式浏览器 API 和库来创建交互式聊天体验。 开发人员可以通过了解以下组件来自定义界面或添加功能:

  1. 语音输入/输出:自定义 Web 组件使用浏览器的语音 API:

    • <speech-input-button>:使用 Web 语音 API 将 SpeechRecognition语音转换为文本。 它提供一个语音按钮,可以侦听语音输入,并发出带有转录文本的 speech-input-result 事件。

    • <speech-output-button>:使用 SpeechSynthesis API 大声朗读文本。 它出现在每个助理响应之后,带有一个扬声器图标,让用户能够听到响应。

    为什么使用浏览器 API 而不是 Azure 语音服务?

    • 无成本 - 完全在浏览器中运行
    • 即时响应 - 无网络延迟
    • 隐私 - 语音数据保留在用户的设备上
    • 无需额外的 Azure 资源

    这些组件位于 src/quartapp/static/speech-input.jsspeech-output.js中。

  2. 图像预览:在分析提交进行确认之前,在聊天中显示上传的图像。 选择文件时,预览版会自动更新。

    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 和 Bootstrap 图标:提供响应式 UI 组件和图标。 应用使用 Bootswatch 中的 Cosmo 主题实现新式外观。

  4. 基于模板的消息呈现:对可重用邮件布局使用 HTML <template> 元素,确保样式和结构一致。

要浏览的其他示例资源

除了聊天应用示例,存储库中还有其他资源可供进一步学习。 查看目录中的以下笔记本 notebooks

Notebook 说明
chat_pdf_images.ipynb 此笔记本演示如何将 PDF 页面转换为图像,并将其发送到视觉模型进行推理。
chat_vision.ipynb 此笔记本用于手动试验应用中使用的视觉模型。

本地化内容:笔记本的西班牙语版本位于 notebooks/Spanish/ 目录中,为讲西班牙语的开发人员提供相同的实践学习。 英语和西班牙语笔记本都显示:

  • 如何直接调用视觉模型进行试验
  • 如何将 PDF 页面转换为图像进行分析
  • 如何调整参数和测试提示

清理资源

清理 Azure 资源

本文中创建的 Azure 资源的费用将计入你的 Azure 订阅。 如果你预计将来不需要这些资源,请将其删除,以避免产生更多费用。

要删除 Azure 资源并移除源代码,请运行以下 Azure Developer CLI 命令:

azd down --purge

清理 GitHub Codespaces

删除 GitHub Codespaces 环境可确保可以最大程度地提高帐户获得的每核心免费小时数权利。

重要

有关 GitHub 帐户权利的详细信息,请参阅 GitHub Codespaces 每月包含的存储和核心小时数

  1. 登录到 GitHub Codespaces 仪表板

  2. 找到当前正在运行的、源自 Azure-Samples//openai-chat-vision-quickstart GitHub 存储库的 Codespaces。

  3. 打开 codespace 的上下文菜单,然后选择“删除”。

获取帮助

将问题记录到存储库 的问题

后续步骤