你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:探索 Azure OpenAI 服务嵌入和文档搜索

本教程将引导你使用 Azure OpenAI 嵌入 API 执行文档搜索,你将通过此操作查询知识库以查找最相关的文档。

本教程介绍如何执行下列操作:

  • 安装 Azure OpenAI。
  • 下载示例数据集并准备进行分析。
  • 为资源终结点和 API 密钥创建环境变量。
  • 请使用以下模型之一:text-embedding-ada-002(版本 2)、text-embedding-3-large、text-embedding-3-small 模型。
  • 使用余弦相似性对搜索结果进行排名。

先决条件

设置

Python 库

如果尚未安装,则需要安装以下库:

pip install openai num2words matplotlib plotly scipy scikit-learn pandas tiktoken

下载 BillSum 数据集

BillSum 是美国国会法案和加州法案的数据集。 出于说明目的,我们只探讨美国法案。 语料库由国会第 103 - 115 届 (1993 - 2018) 会议的法案组成。 数据被拆分为 18,949 个训练法案和 3,269 个测试法案。 BillSum 语料库侧重于长度从 5,000 到 20,000 个字符的中等长度立法。 有关该项目的详细信息以及此数据集派生自的原始学术论文,请参阅 BillSum 项目的 GitHub 存储库

本教程使用 bill_sum_data.csv 文件,可从我们的 GitHub 示例数据下载此文件

还可以在本地计算机上运行以下命令来下载示例数据:

curl "https://raw.githubusercontent.com/Azure-Samples/Azure-OpenAI-Docs-Samples/main/Samples/Tutorials/Embeddings/data/bill_sum_data.csv" --output bill_sum_data.csv

检索密钥和终结点

若要成功对 Azure OpenAI 发出调用,需要一个终结点和一个密钥。

变量名称
ENDPOINT 从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到服务终结点。 或者,也可以通过 Azure AI Studio 中的“部署”页找到终结点。 示例终结点为:https://docs-test-001.openai.azure.com/
API-KEY 从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到此值。 可以使用 KEY1KEY2

在 Azure 门户中转到你的资源。 可在“资源管理”部分中找到“密钥和终结点”部分。 复制终结点和访问密钥,因为在对 API 调用进行身份验证时需要这两项。 可以使用 KEY1KEY2。 始终准备好两个密钥可以安全地轮换和重新生成密钥,而不会导致服务中断。

Azure 门户中 Azure OpenAI 资源概述 UI 的屏幕截图,其中终结点和访问密钥的位置用红圈标示。

环境变量

为密钥和终结点创建和分配持久环境变量。

重要

如果使用 API 密钥,请将其安全地存储在某个其他位置,例如 Azure Key Vault 中。 请不要直接在代码中包含 API 密钥,并且切勿公开发布该密钥。

有关 Azure AI 服务安全性的详细信息,请参阅对 Azure AI 服务的请求进行身份验证

setx AZURE_OPENAI_API_KEY "REPLACE_WITH_YOUR_KEY_VALUE_HERE" 
setx AZURE_OPENAI_ENDPOINT "REPLACE_WITH_YOUR_ENDPOINT_HERE" 

设置环境变量后,可能需要关闭并重新打开 Jupyter 笔记本或正在使用的任何 IDE,以便可以访问环境变量。 虽然我们强烈建议使用 Jupyter Notebook,但如果出于某种原因无法返回 pandas 数据帧,则需要使用 print(dataframe_name) 修改任何返回 pandas 数据帧的代码,而不是像在代码块末尾经常做的那样直接调用 dataframe_name

在首选 Python IDE 中运行以下代码:

导入库

import os
import re
import requests
import sys
from num2words import num2words
import os
import pandas as pd
import numpy as np
import tiktoken
from openai import AzureOpenAI

现在,需要读取 csv 文件并创建 pandas 数据帧。 创建初始数据帧后,可以通过运行 df 来查看表的内容。

df=pd.read_csv(os.path.join(os.getcwd(),'bill_sum_data.csv')) # This assumes that you have placed the bill_sum_data.csv in the same directory you are running Jupyter Notebooks
df

输出:

csv 文件中的初始 DataFrame 表结果的屏幕截图。

初始表的列数超过所需列数,我们将创建一个名为 df_bills 的较小的新数据帧,该数据帧仅包含 textsummarytitle 的列。

df_bills = df[['text', 'summary', 'title']]
df_bills

输出:

较小的数据帧表结果的屏幕截图,其中仅显示文本、摘要和标题列。

接下来,通过删除冗余的空格和清理标点来执行一些轻数据清理,以便为标记化准备数据。

pd.options.mode.chained_assignment = None #https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#evaluation-order-matters

# s is input text
def normalize_text(s, sep_token = " \n "):
    s = re.sub(r'\s+',  ' ', s).strip()
    s = re.sub(r". ,","",s)
    # remove all instances of multiple spaces
    s = s.replace("..",".")
    s = s.replace(". .",".")
    s = s.replace("\n", "")
    s = s.strip()
    
    return s

df_bills['text']= df_bills["text"].apply(lambda x : normalize_text(x))

现在,需要删除对于令牌限制(大约 8192 个令牌)来说太长的任何法案。

tokenizer = tiktoken.get_encoding("cl100k_base")
df_bills['n_tokens'] = df_bills["text"].apply(lambda x: len(tokenizer.encode(x)))
df_bills = df_bills[df_bills.n_tokens<8192]
len(df_bills)
20

注意

在这种情况下,所有帐单都低于嵌入模型输入令牌限制,但可以使用上述技术删除会导致嵌入失败的条目。 当遇到超出嵌入限制的内容时,还可以将内容分块成较小的部分,然后一次嵌入一个。

我们将再次探讨 df_bills。

df_bills

输出:

数据帧的屏幕截图,其中包含名为 n_tokens 的新列。

若要更深入地了解 n_tokens 列以及文本的最终标记化方式,运行以下代码可能会有所帮助:

sample_encode = tokenizer.encode(df_bills.text[0]) 
decode = tokenizer.decode_tokens_bytes(sample_encode)
decode

对于我们的文档,我们有意截断输出,但在你的环境中运行此命令将返回标记化为区块的索引零的全文。 你可以看到,在某些情况下,整个单词用单个令牌表示,而单词的其他部分则拆分为多个令牌。

[b'SECTION',
 b' ',
 b'1',
 b'.',
 b' SHORT',
 b' TITLE',
 b'.',
 b' This',
 b' Act',
 b' may',
 b' be',
 b' cited',
 b' as',
 b' the',
 b' ``',
 b'National',
 b' Science',
 b' Education',
 b' Tax',
 b' In',
 b'cent',
 b'ive',
 b' for',
 b' Businesses',
 b' Act',
 b' of',
 b' ',
 b'200',
 b'7',
 b"''.",
 b' SEC',
 b'.',
 b' ',
 b'2',
 b'.',
 b' C',
 b'RED',
 b'ITS',
 b' FOR',
 b' CERT',
 b'AIN',
 b' CONTRIBUT',
 b'IONS',
 b' BEN',
 b'EF',
 b'IT',
 b'ING',
 b' SC',

如果随后检查 decode 变量的长度,你会发现它与 n_tokens 列中的第一个数字匹配。

len(decode)
1466

现在,我们详细了解了标记化的工作原理,可以继续嵌入。 请务必注意,我们实际上尚未对文档进行标记化。 列 n_tokens 只是一种确保传递给模型进行标记化和嵌入的数据均未超过输入令牌限制 8192 个的方法。 当我们将文档传递到嵌入模型时,它会将文档分解为类似(但不必等同)于上述示例的令牌,然后将令牌转换为可通过向量搜索访问的一系列浮点数。 这些嵌入可以存储在本地,也可以存储在 Azure 数据库中以支持 Vector Search。 因此,每个帐单在数据帧右侧的新 ada_v2 列中都有相应的嵌入向量。

在以下示例中,我们为每个要嵌入的项调用一次嵌入模型。 在处理大型嵌入项目时,也可以向模型传递一个要嵌入的输入数组,而不是一次一个输入。 向模型传递输入数组时,每次调用嵌入终结点的输入项的最大数目为 2048。

client = AzureOpenAI(
  api_key = os.getenv("AZURE_OPENAI_API_KEY"),  
  api_version = "2024-02-01",
  azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
)

def generate_embeddings(text, model="text-embedding-ada-002"): # model = "deployment_name"
    return client.embeddings.create(input = [text], model=model).data[0].embedding

df_bills['ada_v2'] = df_bills["text"].apply(lambda x : generate_embeddings (x, model = 'text-embedding-ada-002')) # model should be set to the deployment name you chose when you deployed the text-embedding-ada-002 (Version 2) model
df_bills

输出:

df_bills 命令的格式化结果的屏幕截图。

运行下面的搜索代块时,我们将使用同一个 text-embedding-ada-002(版本 2)模型的嵌入搜索查询“我能否获取有关有线电视公司税收的信息?” 接下来,我们将找到嵌入到查询中新嵌入文本(按余弦相似性排名)的最接近帐单。

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def get_embedding(text, model="text-embedding-ada-002"): # model = "deployment_name"
    return client.embeddings.create(input = [text], model=model).data[0].embedding

def search_docs(df, user_query, top_n=4, to_print=True):
    embedding = get_embedding(
        user_query,
        model="text-embedding-ada-002" # model should be set to the deployment name you chose when you deployed the text-embedding-ada-002 (Version 2) model
    )
    df["similarities"] = df.ada_v2.apply(lambda x: cosine_similarity(x, embedding))

    res = (
        df.sort_values("similarities", ascending=False)
        .head(top_n)
    )
    if to_print:
        display(res)
    return res


res = search_docs(df_bills, "Can I get information on cable company tax revenue?", top_n=4)

输出

运行搜索查询后 res 的格式化结果的屏幕截图。

最后,我们将展示基于对整个知识库的用户查询的文档搜索的排名靠前结果。 这返回了排名靠前的结果“1993 年纳税人观看权法案”。 此文档的查询与文档之间的余弦相似性分数为 0.76:

res["summary"][9]
"Taxpayer's Right to View Act of 1993 - Amends the Communications Act of 1934 to prohibit a cable operator from assessing separate charges for any video programming of a sporting, theatrical, or other entertainment event if that event is performed at a facility constructed, renovated, or maintained with tax revenues or by an organization that receives public financial support. Authorizes the Federal Communications Commission and local franchising authorities to make determinations concerning the applicability of such prohibition. Sets forth conditions under which a facility is considered to have been constructed, maintained, or renovated with tax revenues. Considers events performed by nonprofit or public organizations that receive tax subsidies to be subject to this Act if the event is sponsored by, or includes the participation of a team that is part of, a tax exempt organization."

先决条件

注意

本教程中的许多示例在各步骤中重复使用变量。 在整个过程中始终保持打开同一个终端会话。 如果在上一步中设置的变量由于关闭终端而丢失,则必须从头开始。

检索密钥和终结点

若要成功对 Azure OpenAI 发出调用,需要一个终结点和一个密钥。

变量名称
ENDPOINT 从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到服务终结点。 或者,也可以通过 Azure AI Studio 中的“部署”页找到终结点。 示例终结点为:https://docs-test-001.openai.azure.com/
API-KEY 从 Azure 门户检查资源时,可在“密钥和终结点”部分中找到此值。 可以使用 KEY1KEY2

在 Azure 门户中转到你的资源。 可在“资源管理”部分中找到“密钥和终结点”部分。 复制终结点和访问密钥,因为在对 API 调用进行身份验证时需要这两项。 可以使用 KEY1KEY2。 始终准备好两个密钥可以安全地轮换和重新生成密钥,而不会导致服务中断。

Azure 门户中 Azure OpenAI 资源概述 UI 的屏幕截图,其中终结点和访问密钥的位置用红圈标示。

环境变量

为密钥和终结点创建和分配持久环境变量。

重要

如果使用 API 密钥,请将其安全地存储在某个其他位置,例如 Azure Key Vault 中。 请不要直接在代码中包含 API 密钥,并且切勿公开发布该密钥。

有关 Azure AI 服务安全性的详细信息,请参阅对 Azure AI 服务的请求进行身份验证

setx AZURE_OPENAI_API_KEY "REPLACE_WITH_YOUR_KEY_VALUE_HERE" 
setx AZURE_OPENAI_ENDPOINT "REPLACE_WITH_YOUR_ENDPOINT_HERE" 

在本教程中,我们使用 PowerShell 7.4 参考文档作为已知的安全示例数据集。 或者,可以选择浏览 Microsoft Research 工具示例数据集。

创建要在其中存储项目的文件夹。 将你的位置设置为项目文件夹。 使用 Invoke-WebRequest 命令将数据集下载到本地计算机,然后展开存档。 最后,将位置设置为包含 PowerShell 版本 7.4 的参考信息的子文件夹。

New-Item '<FILE-PATH-TO-YOUR-PROJECT>' -Type Directory
Set-Location '<FILE-PATH-TO-YOUR-PROJECT>'

$DocsUri = 'https://github.com/MicrosoftDocs/PowerShell-Docs/archive/refs/heads/main.zip'
Invoke-WebRequest $DocsUri -OutFile './PSDocs.zip'

Expand-Archive './PSDocs.zip'
Set-Location './PSDocs/PowerShell-Docs-main/reference/7.4/'

在本教程中,我们将处理大量数据,因此我们使用 .NET 数据表对象来实现高效性能。 该数据表包含“标题”、“内容”、“准备”、“uri”、“文件”和“向量”列。 “标题”列是主键

在下一步中,我们将每个 Markdown 文件的内容加载到数据表中。 我们还使用 PowerShell -match 运算符捕获已知的文本行 title:online version:,并将它们存储在不同的列中。 某些文件不包含元数据文本行,但由于它们是概述页面而不是详细的参考文档,因此我们将它们从数据表中排除。

# make sure your location is the project subfolder

$DataTable = New-Object System.Data.DataTable

'title', 'content', 'prep', 'uri', 'file', 'vectors' | ForEach-Object {
    $DataTable.Columns.Add($_)
} | Out-Null
$DataTable.PrimaryKey = $DataTable.Columns['title']

$md = Get-ChildItem -Path . -Include *.md -Recurse

$md | ForEach-Object {
    $file       = $_.FullName
    $content    = Get-Content $file
    $title      = $content | Where-Object { $_ -match 'title: ' }
    $uri        = $content | Where-Object { $_ -match 'online version: ' }
    if ($title -and $uri) {
        $row                = $DataTable.NewRow()
        $row.title          = $title.ToString().Replace('title: ', '')
        $row.content        = $content | Out-String
        $row.prep           = '' # use later in the tutorial
        $row.uri            = $uri.ToString().Replace('online version: ', '')
        $row.file           = $file
        $row.vectors        = '' # use later in the tutorial
        $Datatable.rows.add($row)
    }
}

使用 out-gridview 命令查看数据(在 Cloud Shell 中不可用)。

$Datatable | out-gridview

输出:

初始 DataTable 结果的屏幕截图。

接下来,通过移除多余的字符、空格和其他文档表示法来执行一些轻数据清理,以便为标记化准备数据。 示例函数 Invoke-DocPrep 演示了如何使用 PowerShell -replace 运算符循环访问要从内容中移除的字符列表。

# sample demonstrates how to use `-replace` to remove characters from text content
function Invoke-DocPrep {
param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    [string]$content
)
    # tab, line breaks, empty space
    $replace = @('\t','\r\n','\n','\r')
    # non-UTF8 characters
    $replace += @('[^\x00-\x7F]')
    # html
    $replace += @('<table>','</table>','<tr>','</tr>','<td>','</td>')
    $replace += @('<ul>','</ul>','<li>','</li>')
    $replace += @('<p>','</p>','<br>')
    # docs
    $replace += @('\*\*IMPORTANT:\*\*','\*\*NOTE:\*\*')
    $replace += @('<!','no-loc ','text=')
    $replace += @('<--','-->','---','--',':::')
    # markdown
    $replace += @('###','##','#','```')
    $replace | ForEach-Object {
        $content = $content -replace $_, ' ' -replace '  ',' '
    }
    return $content
}

创建 Invoke-DocPrep 函数后,使用 ForEach-Object 命令将准备的内容存储在“准备”列中,用于数据表中的所有行。 我们使用的是新列,因此,如果以后要检索它,可以使用原始格式。

$Datatable.rows | ForEach-Object { $_.prep = Invoke-DocPrep $_.content }

再次查看数据表以查看更改。

$Datatable | out-gridview

当我们将文档传递道嵌入模型时,它会将文档编码为标记,然后返回一系列浮点数以用于余弦相似性搜索。 这些嵌入可以存储在本地,已可以存储在 Azure AI 搜索中的矢量搜索等服务中。 每个文档在新的“向量”列中都有自己的相应的嵌入向量。

下一个示例循环访问数据表中的每一行,检索预处理内容的向量,并将它们存储到“向量”列。 OpenAI 服务会限制频繁的请求,因此该示例包含指数回退,如文档所建议。

脚本完成后,每行应该有一个包含每个文档的 1536 个向量的逗号分隔列表。 如果发生错误并且状态代码为 400,则会将文件路径、标题和错误代码添加到名为 $errorDocs 的变量中,以进行故障排除。 当令牌计数超过模型的提示限制时,会发生最常见的错误。

# Azure OpenAI metadata variables
$openai = @{
    api_key     = $Env:AZURE_OPENAI_API_KEY 
    api_base    = $Env:AZURE_OPENAI_ENDPOINT # should look like 'https://<YOUR_RESOURCE_NAME>.openai.azure.com/'
    api_version = '2024-02-01' # may change in the future
    name        = $Env:AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT # custom name you chose for your deployment
}

$headers = [ordered]@{
    'api-key' = $openai.api_key
}

$url = "$($openai.api_base)/openai/deployments/$($openai.name)/embeddings?api-version=$($openai.api_version)"

$Datatable | ForEach-Object {
    $doc = $_

    $body = [ordered]@{
        input = $doc.prep
    } | ConvertTo-Json

    $retryCount = 0
    $maxRetries = 10
    $delay      = 1
    $docErrors = @()

    do {
        try {
            $params = @{
                Uri         = $url
                Headers     = $headers
                Body        = $body
                Method      = 'Post'
                ContentType = 'application/json'
            }
            $response = Invoke-RestMethod @params
            $Datatable.rows.find($doc.title).vectors = $response.data.embedding -join ','
            break
        } catch {
            if ($_.Exception.Response.StatusCode -eq 429) {
                $retryCount++
                [int]$retryAfter = $_.Exception.Response.Headers |
                    Where-Object key -eq 'Retry-After' |
                    Select-Object -ExpandProperty Value

                # Use delay from error header
                if ($delay -lt $retryAfter) { $delay = $retryAfter++ }
                Start-Sleep -Seconds $delay
                # Exponential back-off
                $delay = [math]::min($delay * 1.5, 300)
            } elseif ($_.Exception.Response.StatusCode -eq 400) {
                if ($docErrors.file -notcontains $doc.file) {
                    $docErrors += [ordered]@{
                        error   = $_.exception.ErrorDetails.Message | ForEach-Object error | ForEach-Object message
                        file    = $doc.file
                        title   = $doc.title
                    }
                }
            } else {
                throw
            }
        }
    } while ($retryCount -lt $maxRetries)
}
if (0 -lt $docErrors.count) {
    Write-Host "$($docErrors.count) documents encountered known errors such as too many tokens.`nReview the `$docErrors variable for details."
}

你现在有一个 PowerShell 7.4 参考文档的本地内存数据库表。

根据搜索字符串,我们需要计算另一组向量,以便 PowerShell 可以按相似性对每个文档进行排名。

在下一个示例中,将检索搜索字符串 get a list of running processes 的向量。

$searchText = "get a list of running processes"

$body = [ordered]@{
    input = $searchText
} | ConvertTo-Json

$url = "$($openai.api_base)/openai/deployments/$($openai.name)/embeddings?api-version=$($openai.api_version)"

$params = @{
    Uri         = $url
    Headers     = $headers
    Body        = $body
    Method      = 'Post'
    ContentType = 'application/json'
}
$response = Invoke-RestMethod @params
$searchVectors = $response.data.embedding -join ','

最后,下一个示例函数借用 Lee Holmes 编写的示例脚本 Measure-VectorSimilarity 中的示例,执行余弦相似性计算,然后对数据表中的每一行进行排名。

# Sample function to calculate cosine similarity
function Get-CosineSimilarity ([float[]]$vector1, [float[]]$vector2) {
    $dot = 0
    $mag1 = 0
    $mag2 = 0

    $allkeys = 0..($vector1.Length-1)

    foreach ($key in $allkeys) {
        $dot  += $vector1[$key]  * $vector2[$key]
        $mag1 += ($vector1[$key] * $vector1[$key])
        $mag2 += ($vector2[$key] * $vector2[$key])
    }

    $mag1 = [Math]::Sqrt($mag1)
    $mag2 = [Math]::Sqrt($mag2)

    return [Math]::Round($dot / ($mag1 * $mag2), 3)
}

下一个示例中的命令循环访问 $Datatable 中的所有行,并计算与搜索字符串的余弦相似性。 结果经过了排序,并且前三个结果存储在名为 $topThree 的变量中。 该示例不返回输出。

# Calculate cosine similarity for each row and select the top 3
$topThree = $Datatable | ForEach-Object {
    [PSCustomObject]@{
        title = $_.title
        similarity = Get-CosineSimilarity $_.vectors.split(',') $searchVectors.split(',')
    }
} | Sort-Object -property similarity -descending | Select-Object -First 3 | ForEach-Object {
    $title = $_.title
    $Datatable | Where-Object { $_.title -eq $title }
}

在网格视图中查看 $topThree 变量的输出,其中只有“标题”和“url”属性。

$topThree | Select "title", "uri" | Out-GridView

输出:

搜索查询完成后格式化结果的屏幕截图。

$topThree 变量包含数据表的行中的所有信息。 例如,“内容”属性包含原始文档格式。 使用 [0] 索引到数组中的第一项。

$topThree[0].content

查看完整文档(在此页的输出片段中截断)。

---
external help file: Microsoft.PowerShell.Commands.Management.dll-Help.xml
Locale: en-US
Module Name: Microsoft.PowerShell.Management
ms.date: 07/03/2023
online version: https://learn.microsoft.com/powershell/module/microsoft.powershell.management/get-process?view=powershell-7.4&WT.mc_id=ps-gethelp
schema: 2.0.0
title: Get-Process
---

# Get-Process

## SYNOPSIS
Gets the processes that are running on the local computer.

## SYNTAX

### Name (Default)

Get-Process [[-Name] <String[]>] [-Module] [-FileVersionInfo] [<CommonParameters>]
# truncated example

最后,可以将数据存储到磁盘并在将来重新调用,而无需在每次需要查询数据集时都重新生成嵌入。 下一个示例中 DataTable 对象类型的 WriteXML()ReadXML() 方法简化了该过程。 XML 文件的架构要求数据表具有 TableName

<YOUR-FULL-FILE-PATH> 替换为要在其中写入和读取 XML 文件的完整路径。 路径应以 .xml结尾。

# Set DataTable name
$Datatable.TableName = "MyDataTable"

# Writing DataTable to XML
$Datatable.WriteXml("<YOUR-FULL-FILE-PATH>", [System.Data.XmlWriteMode]::WriteSchema)

# Reading XML back to DataTable
$newDatatable = New-Object System.Data.DataTable
$newDatatable.ReadXml("<YOUR-FULL-FILE-PATH>")

重用数据时,需要获取每个新搜索字符串的向量(但不是整个数据表)。 作为学习练习,尝试创建一个 PowerShell 脚本来自动执行 Invoke-RestMethod 命令,并将搜索字符串作为参数。

使用此方法,可以将嵌入用作知识库中跨文档的搜索机制。 然后,用户可以获取排名靠前的搜索结果,并将其用于其下游任务,这会提示其初始查询。

清理资源

如果只是为了完成本教程而创建了 Azure OpenAI 资源,并且想要清理和删除 Azure OpenAI 资源,则需要删除已部署的模型,然后删除专用于测试资源的资源或关联的资源组。 删除资源组同时也会删除与之相关联的任何其他资源。

后续步骤

详细了解 Azure OpenAI 的模型: