本文介绍如何使用 Azure OpenAI 多模式模型在聊天应用中生成对用户消息和上传图像的响应。 此聊天应用示例还包括预配 Azure OpenAI 资源以及使用 Azure 开发人员 CLI 将应用部署到Azure 容器应用所需的所有基础结构和配置。
按照本文的说明,你可以:
- 部署使用托管标识进行身份验证的Azure容器聊天应用。
- 上传要用作聊天流的一部分的图像。
- 使用 OpenAI 库的响应 API 与 Azure OpenAI 多模式大型语言模型(LLM)聊天。
完成此文章后,可以使用自定义代码开始修改新项目。
注意
本文使用一个或多个 AI 应用模板作为本文中的示例和指南的基础。 AI 应用模板为你提供了维护良好、易于部署的参考实现,可帮助确保 AI 应用有一个高质量的起点。
体系结构概述
聊天应用作为Azure容器应用运行。 应用程序在生产环境中通过 Microsoft Entra ID 使用托管身份与 Azure OpenAI 进行身份验证,而不是通过 API 密钥。 在开发过程中,应用支持多种身份验证方法,包括Azure开发人员 CLI 凭据和 API 密钥。
应用程序体系结构依赖于以下服务和组件:
- Azure OpenAI表示我们向其发送用户的查询的 AI 提供程序。
- Azure 容器应用是托管应用程序的容器环境。
- 托管标识 可帮助我们确保一流的安全性,并消除了开发人员需要安全管理机密的需求。
- Bicep文件用于部署Azure资源,包括Azure OpenAI、Azure 容器应用、Azure 容器注册表、Azure Log Analytics以及基于角色的访问控制(RBAC)角色。
- Python Quart 应用,该应用使用
openai包生成对上传图像文件的用户消息的响应。 - 基本的 HTML/JavaScript 前端,通过 ReadableStream 使用 JSON Lines 从后端流式传输响应。
成本
为了尽量降低此示例中的定价,大多数资源都使用基本定价层或消耗定价层。 根据需要根据预期使用情况更改层级别。 若要停止产生费用,在完成本文后删除资源。
了解更多关于成本的信息在示例存储库中。
先决条件
开发容器 环境提供了完成本文所需的所有依赖项。 可以在 GitHub Codespaces(在浏览器中)或使用Visual Studio Code在本地运行开发容器。
若要使用本文,需要满足以下先决条件:
Azure订阅 - 免费创建一个订阅
Azure帐户权限 - Azure帐户必须具有
Microsoft.Authorization/roleAssignments/write权限,例如 User Access Administrator 或 Owner。GitHub帐户
开放开发环境
按照以下说明部署预配置开发环境,其中包含完成本文所需的所有依赖项。
GitHub Codespaces 运行一个由 GitHub 托管的开发容器,使用 Visual Studio Code for the Web 作为用户界面。 对于最直接的开发环境,请使用 GitHub Codespaces,以便预先安装正确的开发人员工具和依赖项来完成本文。
重要
所有 GitHub 帐户每月最多可以免费使用 Codespaces 60 小时,并且包含两个核心实例。 有关详细信息,请参阅 GitHub Codespaces 每月提供的存储和核心小时数。
使用以下步骤在 main GitHub 存储库的 Azure-Samples/openai-chat-vision-quickstart 分支上创建新的 GitHub Codespace。
右键单击以下按钮,然后选择在新窗口中打开链接。 此操作允许你拥有可供查看的开发环境和文档。
在“创建代码空间”页上,查看并选择“创建新代码空间”
等待 Codespace 启动。 此启动过程会花费几分钟时间。
在屏幕底部的终端中使用Azure开发人员 CLI 登录到Azure。
azd auth login从终端复制代码,然后将其粘贴到浏览器中。 按照说明使用Azure帐户进行身份验证。
本文中的剩余任务需要在此开发容器的上下文中完成。
部署和运行
示例存储库包含聊天应用Azure部署的所有代码和配置文件。 以下步骤将引导完成示例聊天应用Azure部署过程。
将聊天应用部署到Azure
重要
为了降低成本,此示例对大多数资源使用基本或消耗定价层。 根据需要调整分层,并在完成后删除资源,以避免产生费用。
运行以下用于Azure资源预配和源代码部署的Azure开发人员 CLI 命令:
azd up使用下表回答提示:
提示 答案 环境名称 保持简短和小写。 添加名称或别名。 例如, chat-vision。 它用作资源组名称的一部分。订阅 请在指定的订阅中创建资源。 位置(用于托管) 从列表中选择附近的位置。 Azure OpenAI 模型的位置 从列表中选择附近的位置。 如果可以使用与第一个位置相同的位置,请选择该位置。 等待应用部署完成。 部署通常需要 5 到 10 分钟才能完成。
使用聊天应用向大型语言模型提问
终端在成功部署应用程序后显示 URL。
选择标记为
Deploying service web的 URL 在浏览器中打开聊天应用程序。在浏览器中,通过单击 “选择文件 ”并选择图像来上传图像。
询问有关上传的图像的问题,例如“图像是什么?”
答案来自 Azure OpenAI,并显示结果。
浏览示例代码
此示例使用 Azure OpenAI 多模式模型生成对用户消息和上传图像的响应。
Base64 对前端中上传的图像进行编码
上传的图像需要经过 Base64 编码,以便可以直接用作数据 URI 作为消息的一部分。
在示例中,文件标记script中的src/quartapp/templates/index.html以下前端代码片段处理该功能。
toBase64箭头函数使用readAsDataURL方法来异步读取上传的图像文件,并将其作为 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 侦听器处理完整的聊天交互流。 当用户提交消息时,会按照以下流程进行:
- 获取上传的图像文件(如果存在),并将其编码为 Base64
- 在聊天中创建并显示用户的消息,包括上传的图像
- 准备一个带有“正在输入...”指示器的助理消息容器
- 以响应 API 格式将用户的消息添加到消息历史记录数组
- 使用消息历史记录和上下文(包括 Base64 编码的图像和文件名)向
/chat/stream终结点发送fetchPOST 请求 - 处理流式传输的JSON 行响应,以逐步显示每个文本的增量变化。
- 处理流媒体过程中出现的任何错误
- 在收到完整的响应后添加语音输出按钮,以便用户可以听到响应
- 清除输入字段并返回下一条消息的焦点
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;
}
});
使用后端处理映像
在 src\quartapp\chat.py 文件中,用于映像处理的后端代码在配置无密钥身份验证后开始。
注意
有关如何使用无密钥连接技术进行 Azure OpenAI 的身份验证和授权的详细信息,请查看 Microsoft Learn 一文 入门 Azure OpenAI 安全构建块。
身份验证配置
该 configure_openai() 函数在应用开始处理请求之前设置 OpenAI 客户端。 它使用 Quart 的 @bp.before_app_serving 修饰器基于环境变量配置身份验证。 此灵活的系统允许开发人员在不同的上下文中工作,而无需更改代码。
身份验证模式说明
-
本地开发 (
OPENAI_HOST=local):在没有身份验证的情况下连接到与 OpenAI 兼容的本地 API 服务(如 Ollama 或 LocalAI)。 使用此模式进行测试,无需 Internet 或 API 成本。 -
Azure OpenAI 与 API 密钥(
AZURE_OPENAI_KEY_FOR_CHATVISION环境变量):使用 API 密钥进行身份验证。 避免在生产环境中使用此模式,因为 API 密钥需要手动轮换,并在暴露时带来安全风险。 使用它在 Docker 容器内进行本地测试,而无需Azure CLI凭据。 -
生产与托管标识(
RUNNING_IN_PRODUCTION=true):使用ManagedIdentityCredential通过容器应用的托管标识使用 Azure OpenAI 进行身份验证。 建议将此方法用于生产,因为它无需管理机密。 Azure 容器应用通过Bicep自动提供托管标识并在部署期间授予权限。 -
Development with Azure CLI(默认模式):使用
AzureDeveloperCliCredential通过本地登录Azure CLI凭据通过 Azure OpenAI 进行身份验证。 此模式简化了本地开发,而无需管理 API 密钥。
关键实现详细信息
-
get_bearer_token_provider()函数刷新Azure凭据,并将其用作持有者令牌。 - Azure OpenAI 终结点路径以
/openai/v1/结尾,这是适用于 Microsoft Foundry 模型的公开可用的 OpenAI 兼容终结点。 - 该函数是异步的,因为 Quart 是异步 Web 应用框架。 Quart 允许请求处理程序是异步的,因此,当应用等待缓慢的 LLM API 响应时,服务器可以继续处理其他请求。
下面是来自 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)
聊天处理程序函数
该 chat_handler() 函数处理发送到终结点的 /chat/stream 聊天请求。 它接收具有 JSON 有效负载的 POST 请求。
JSON 有效负载包括:
-
消息:对话历史记录列表。 每条消息都有一个
(“用户”或“助手”)和 (一个内容部件数组,使用 Responses API 输入格式 )。 -
context:用于处理的额外数据,包括:
-
文件:Base64 编码的图像数据(例如
data:image/png;base64,...)。 - file_name:上传的图像的原始文件名(可用于记录或标识图像类型)。
-
文件:Base64 编码的图像数据(例如
处理程序提取消息历史记录和图像数据。 如果未上传图像,则图像值为 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"]
为视觉请求生成输入数组
response_stream() 函数准备发送到 Azure OpenAI 响应 API 的输入数组。
@stream_with_context修饰器在流式处理响应时保留请求上下文。
输入准备逻辑
-
从对话历史记录开始:函数以
all_input开头,其中包括除最近一条消息() 以外的所有以前的消息。request_messages[0:-1]消息已采用来自前端的响应 API 格式。 -
基于图像状态处理当前用户消息:
-
使用图像:将一个内容部件追加到用户的现有内容数组中
input_image。 - 没有图像:原样附加用户的消息。
-
使用图像:将一个内容部件追加到用户的现有内容数组中
@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])
接下来,bp.openai_client.responses.create调用 Azure OpenAI 响应 API 并流式传输响应。 该 store=False 参数指示 API 不将响应存储在服务器上,使调用无状态。
openai_stream = await bp.openai_client.responses.create(
model=bp.model_name,
input=all_input,
stream=True,
temperature=0.3,
store=False,
)
最后,响应被流式传输回客户端。 响应 API 会发出 许多事件类型,但生成的文本流式处理只需要 response.output_text.delta 事件。 错误事件记录并转发到前端。
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())
前端库和功能
前端使用新式浏览器 API 和库来创建交互式聊天体验。 开发人员可以通过了解以下组件来自定义界面或添加功能:
语音输入/输出:自定义 Web 组件使用浏览器的语音 API:
<speech-input-button>:使用 Web 语音 API 将SpeechRecognition语音转换为文本。 它提供一个语音按钮,可以侦听语音输入,并发出带有转录文本的speech-input-result事件。<speech-output-button>:使用SpeechSynthesisAPI 大声朗读文本。 它出现在每个助理响应之后,带有一个扬声器图标,让用户能够听到响应。
为什么使用浏览器 API 而不是 Azure 语音服务?
- 无成本 - 完全在浏览器中运行
- 即时响应 - 无网络延迟
- 隐私 - 语音数据保留在用户的设备上
- 无需额外的Azure资源
这些组件位于
src/quartapp/static/speech-input.js和speech-output.js中。图像预览:在分析提交进行确认之前,在聊天中显示上传的图像。 选择文件时,预览版会自动更新。
fileInput.addEventListener("change", async function() { const file = fileInput.files[0]; if (file) { const fileData = await toBase64(file); imagePreview.src = fileData; imagePreview.style.display = "block"; } });Bootstrap 5 和 Bootstrap 图标:提供响应式 UI 组件和图标。 应用使用 Bootswatch 中的 Cosmo 主题实现新式外观。
基于模板的消息呈现:对可重用邮件布局使用 HTML
<template>元素,确保样式和结构一致。
要浏览的其他示例资源
除了聊天应用示例,存储库中还有其他资源可供进一步学习。 查看 notebooks 目录中的以下笔记本:
| Notebook | 说明 |
|---|---|
| chat_pdf_images.ipynb | 此笔记本演示如何将 PDF 页面转换为图像,并将其发送到视觉模型进行推理。 |
| chat_vision.ipynb | 此笔记本用于手动试验应用中使用的视觉模型。 |
本地化内容:笔记本的西班牙语版本位于 notebooks/Spanish/ 目录中,为讲西班牙语的开发人员提供相同的实践学习。 英语和西班牙语笔记本都显示:
- 如何直接调用视觉模型进行试验
- 如何将 PDF 页面转换为图像进行分析
- 如何调整参数和测试提示
清理资源
清理 Azure 资源
本文中创建的 Azure 资源将向你的 Azure 订阅计费。 如果你预计将来不需要这些资源,请将其删除,以避免产生更多费用。
若要删除Azure资源并删除源代码,请运行以下Azure开发人员 CLI 命令:
azd down --purge
清理 GitHub Codespaces
删除 GitHub Codespaces 环境可确保可以最大程度地提高帐户获得的每个核心小时免费权利量。
重要
有关 GitHub 帐户权限的更多信息,请参阅 GitHub Codespaces 每月包含的存储和核心小时数。
找到由
Azure-Samples//openai-chat-vision-quickstartGitHub 存储库提供的正在运行的 Codespaces 实例。打开 codespace 的上下文菜单,然后选择“删除”。
获取帮助
将问题记录到存储库的 Issues。
