你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
创建智能应用时,可能需要使用自己的 SQL 数据为应用上下文提供基础。 通过最近宣布的 Azure SQL 矢量支持(预览版),您可以使用已有的 Azure SQL 数据建立语境,利用新的 矢量函数 来帮助管理矢量数据。
在本教程中,你将使用 .NET 8 Blazor 应用设置针对 Azure SQL 数据库的混合矢量搜索来创建 RAG 示例应用程序。 此示例基于之前的文档来部署一个 使用 OpenAI 的 .NET Blazor 应用。 若要使用 azd 模板部署应用,可以使用部署说明访问 Azure 示例存储库 。
先决条件
- 具有已部署模型的 Azure OpenAI 资源
- 在应用服务上部署的 .NET 8 或 9 Blazor Web 应用
- 具有矢量嵌入的 Azure SQL 数据库资源。
1.设置 Blazor Web 应用
在本示例中,我们将创建一个简单的聊天框来与之交互。 如果使用的是 上一篇文章中的先决条件 .NET Blazor 应用,则可以跳过对 OpenAI.razor 文件的更改,因为内容相同。 但是,需要确保已安装以下包:
安装以下包以与 Azure OpenAI 和 Azure SQL 交互。
Microsoft.SemanticKernel
Microsoft.Data.SqlClient
- 右键单击在“组件”文件夹下的“Pages”文件夹,并添加名为“OpenAI.razor”的新项
- 将以下代码添加到 OpenAI.razor 文件,然后单击“保存”
@page "/openai"
@rendermode InteractiveServer
@inject Microsoft.Extensions.Configuration.IConfiguration _config
<PageTitle>OpenAI</PageTitle>
<h3>OpenAI input query: </h3>
<input class="col-sm-4" @bind="userMessage" />
<button class="btn btn-primary" @onclick="SemanticKernelClient">Send Request</button>
<br />
<br />
<h4>Server response:</h4> <p>@serverResponse</p>
@code {
@using Microsoft.SemanticKernel;
@using Microsoft.SemanticKernel.ChatCompletion;
}
API 密钥和终结点
使用 Azure OpenAI 资源需要使用 API 密钥和终结点值。 请参阅 使用 Key Vault 引用作为 Azure 应用服务和 Azure Functions 中的应用设置 ,以使用 Azure OpenAI 管理和处理机密。 虽然并无强制要求,但我们建议使用托管标识来保护客户端,而无需管理 API 密钥。 请参阅前面的 文档 ,在下一步中设置 Azure OpenAI 客户端,以便将托管标识与 Azure OpenAI 配合使用。
2.添加 Azure OpenAI 客户端
添加聊天界面后,可以使用语义内核设置 Azure OpenAI 客户端。 添加以下代码来创建连接到 Azure OpenAI 资源的客户端。 需要使用在上一步中设置和处理的 Azure OpenAI API 密钥和终结点信息。
@inject Microsoft.Extensions.Configuration.IConfiguration _config
@code {
@using Microsoft.SemanticKernel;
@using Microsoft.SemanticKernel.ChatCompletion;
private string? userMessage;
private string? serverResponse;
private async Task SemanticKernelClient()
{
// App settings
string deploymentName = _config["DEPLOYMENT_NAME"];
string endpoint = _config["ENDPOINT"];
string apiKey = _config["API_KEY"];
string modelId = _config["MODEL_ID"];
var builder = Kernel.CreateBuilder();
// Chat completion service
builder.Services.AddAzureOpenAIChatCompletion(
deploymentName: deploymentName,
endpoint: endpoint,
apiKey: apiKey,
modelId: modelId
);
var kernel = builder.Build();
// Create prompt template
var chat = kernel.CreateFunctionFromPrompt(
@"{{$history}}
User: {{$request}}
Assistant: ");
ChatHistory chatHistory = new("""You are a helpful assistant that answers questions""");
var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
chat,
new()
{
{ "request", userMessage },
{ "history", string.Join("\n", chatHistory.Select(x => x.Role + ": " + x.Content)) }
}
);
string message = "";
await foreach (var chunk in chatResult)
{
message += chunk;
}
// Add messages to chat history
chatHistory.AddUserMessage(userMessage!);
chatHistory.AddAssistantMessage(message);
serverResponse = message;
在此,应该准备一个连接到 OpenAI 并可正常使用的聊天应用程序。 接下来,我们将设置 Azure SQL 数据库来使用此聊天应用程序。
3.部署 Azure OpenAI 模型
为了给 Azure SQL 数据库做好矢量搜索的准备,除了初始部署的语言模型外,还需要使用嵌入模型来生成用于搜索的嵌入。 在本示例中,我们使用以下模型:
-
text-embedding-ada-002
用于生成嵌入 -
gpt-3.5-turbo
用于语言模型
在继续下一步之前,需要部署这两个模型。 请访问有关使用 Azure AI Foundry 通过 Azure OpenAI 部署模型 的文档 。
4.矢量化 SQL 数据库
若要在 Azure SQL 数据库上执行混合矢量搜索,首先需要在数据库中包含适当的嵌入。 可通过多种方式矢量化数据库。 一个选项是使用以下 Azure SQL 数据库向量器 为 SQL 数据库生成嵌入内容。 在继续操作之前,请矢量化 Azure SQL 数据库。
5.创建用于生成嵌入的过程
借助 Azure SQL 向量支持(预览版),可以创建一个存储过程,该存储过程将使用 Vector 数据类型来存储为搜索查询生成的嵌入内容。 存储过程调用外部 REST API 终结点来获取嵌入。 在运行查询之前,请参阅使用 Azure Data Studio 连接到数据库 的文档 。
- 使用以下命令通过首选 SQL 查询编辑器创建存储过程。 需要使用 Azure OpenAI 资源名称填充 @url 参数,并使用文本嵌入模型中的 API 密钥填充其余终结点。 你会注意到模型名称是 @url 的一部分,其中将填入搜索查询。
CREATE PROCEDURE [dbo].[GET_EMBEDDINGS]
(
@model VARCHAR(MAX),
@text NVARCHAR(MAX),
@embedding VECTOR(1536) OUTPUT
)
AS
BEGIN
DECLARE @retval INT, @response NVARCHAR(MAX);
DECLARE @url VARCHAR(MAX);
DECLARE @payload NVARCHAR(MAX) = JSON_OBJECT('input': @text);
-- Set the @url variable with proper concatenation before the EXEC statement
SET @url = 'https://<resourcename>.openai.azure.com/openai/deployments/' + @model + '/embeddings?api-version=2023-03-15-preview';
EXEC dbo.sp_invoke_external_rest_endpoint
@url = @url,
@method = 'POST',
@payload = @payload,
@headers = '{"Content-Type":"application/json", "api-key":"<openAIkey>"}',
@response = @response OUTPUT;
-- Use JSON_QUERY to extract the embedding array directly
DECLARE @jsonArray NVARCHAR(MAX) = JSON_QUERY(@response, '$.result.data[0].embedding');
SET @embedding = CAST(@jsonArray as VECTOR(1536));
END
GO
创建存储过程后,应该能够在 SQL 数据库的“Programmability”文件夹中的“Stored Procedures”文件夹下看到它。 创建后,可以使用文本嵌入模型名称在 SQL 查询编辑器中运行测试 相似性搜索 。 这将使用存储过程生成嵌入,并使用矢量距离函数根据文本查询计算矢量距离并返回结果。
6.连接和搜索数据库
现在数据库已设置完毕,可以创建嵌入了。可以在应用程序中连接到数据库,并设置混合矢量搜索查询。
将以下代码添加到 OpenAI.razor
文件,并确保将连接字符串更新为使用已部署的 Azure SQL 数据库连接字符串。 该代码使用 SQL 参数,该参数将安全地将用户输入从聊天应用传递到查询。
// Database connection string
var connectionString = _config["AZURE_SQL_CONNSTRING"];
try
{
await using var connection = new SqlConnection(connectionString);
Console.WriteLine("\nQuery results:");
await connection.OpenAsync();
// Hybrid search query
var sql =
@"DECLARE @e VECTOR(1536);
EXEC dbo.GET_EMBEDDINGS @model = 'text-embedding-ada-002', @text = '@userMessage', @embedding = @e OUTPUT;
-- Comprehensive query with multiple filters.
SELECT TOP(5)
f.Score,
f.Summary,
f.Text,
VECTOR_DISTANCE('cosine', @e, VectorBinary) AS Distance,
CASE
WHEN LEN(f.Text) > 100 THEN 'Detailed Review'
ELSE 'Short Review'
END AS ReviewLength,
CASE
WHEN f.Score >= 4 THEN 'High Score'
WHEN f.Score BETWEEN 2 AND 3 THEN 'Medium Score'
ELSE 'Low Score'
END AS ScoreCategory
FROM finefoodembeddings10k$ f
WHERE
f.UserId NOT LIKE 'Anonymous%' -- User-based filter to exclude anonymous users
AND f.Score >= 4 -- Score threshold filter
AND LEN(f.Text) > 50 -- Text length filter for detailed reviews
AND (f.Text LIKE '%juice%') -- Inclusion of specific words
ORDER BY
Distance, -- Order by distance
f.Score DESC, -- Secondary order by review score
ReviewLength DESC; -- Tertiary order by review length
";
// Set SQL Parameter to pass in user message
SqlParameter param = new SqlParameter();
param.ParameterName = "@userMessage";
param.Value = userMessage;
await using var command = new SqlCommand(sql, connection);
// add parameter to SqlCommand
command.Parameters.Add(param);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
// write results to console logs
Console.WriteLine("{0} {1} {2} {3}", "Score: " + reader.GetDouble(0), "Text: " + reader.GetString(1), "Summary: " + reader.GetString(2), "Distance: " + reader.GetDouble(3));
Console.WriteLine();
// add results to chat history
chatHistory.AddSystemMessage(reader.GetString(1) + ", " + reader.GetString(2));
}
}
catch (SqlException e)
{
Console.WriteLine($"SQL Error: {e.Message}");
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Console.WriteLine("Done");
SQL 查询本身使用混合搜索,该搜索执行之前设置的存储过程来创建嵌入,并使用 SQL 筛选出所需的结果。 在此示例中,我们将提供结果分数并对输出排序,以获取最佳结果,然后再将其用作要从中生成响应的有依据的上下文。
使用托管标识保护数据
通过配置无密码身份验证,Azure SQL 可以将托管标识与 Microsoft Entra 配合使用来保护 SQL 资源。 按照以下步骤配置将在应用程序中使用的无密码连接字符串。
- 导航到 Azure SQL 服务器资源,然后在“设置”下单击“Microsoft Entra ID”。
- 然后单击“+ 设置管理员”,搜索并选择自行设置 Entra ID,然后单击“保存”。 现在即在 SQL 服务器上设置了 Entra ID,并接受 Entra ID 身份验证。
- 接下来,转到数据库资源并复制 ADO.NET(Microsoft Entra 无密码身份验证)连接字符串,并将其添加到用于保存连接字符串的代码中。
此时,可以使用无密码连接字符串在本地测试应用程序。
授予对应用服务的访问权限
在将托管标识与 Azure SQL 配合使用时,必须先向数据库授予对应用服务的访问权限,然后才能调用数据库。 如果目前尚未这样做,则需要先创建 Web 应用,然后才能完成后续步骤。
请按照这些步骤授予对 Web 应用的访问权限:
- 导航到 Web 应用,然后单击“设置”下的“标识”边栏选项卡。
- 如果尚未打开系统分配的托管标识,请打开。
- 导航到数据库资源,并打开左侧菜单中的查询编辑器。 可能需要登录才能使用编辑器。
- 运行以下命令以创建用户并更改角色,将 Web 应用添加为成员
-- Create member, alter roles to your database
CREATE USER "<your-app-name>" FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER "<your-app-name>";
ALTER ROLE db_datawriter ADD MEMBER "<your-app-name>";
ALTER ROLE db_ddladmin ADD MEMBER "<your-app-name>";
GO
- 接下来,授予使用存储过程和 Azure OpenAI 终结点的访问权限
-- Grant access to use stored procedure
GRANT EXECUTE ON OBJECT::[dbo].[GET_EMBEDDINGS]
TO "<your-app-name>"
GO
-- Grant access to use Azure OpenAI endpoint in stored procedure
GRANT EXECUTE ANY EXTERNAL ENDPOINT TO "<your-app-name>";
GO
此时,Azure SQL 数据库是安全的,可以将应用程序部署到应用服务。
下面是添加的 OpenAI.razor 页的完整示例:
@page "/openai"
@rendermode InteractiveServer
@inject Microsoft.Extensions.Configuration.IConfiguration _config
<PageTitle>OpenAI</PageTitle>
<h3>OpenAI input query: </h3>
<input class="col-sm-4" @bind="userMessage" />
<button class="btn btn-primary" @onclick="SemanticKernelClient">Send Request</button>
<br />
<br />
<h4>Server response:</h4> <p>@serverResponse</p>
@code {
@using Microsoft.SemanticKernel;
@using Microsoft.SemanticKernel.ChatCompletion;
@using Microsoft.Data.SqlClient;
private string? userMessage;
private string? serverResponse;
private async Task SemanticKernelClient()
{
// App settings
string deploymentName = _config["DEPLOYMENT_NAME"];
string endpoint = _config["ENDPOINT"];
string apiKey = _config["API_KEY"];
string modelId = _config["MODEL_ID"];
// Semantic Kernel builder
var builder = Kernel.CreateBuilder();
// Chat completion service
builder.Services.AddAzureOpenAIChatCompletion(
deploymentName: deploymentName,
endpoint: endpoint,
apiKey: apiKey,
modelId: modelId
);
var kernel = builder.Build();
// Create prompt template
var chat = kernel.CreateFunctionFromPrompt(
@"{{$history}}
User: {{$request}}
Assistant: ");
ChatHistory chatHistory = new("""You are a helpful assistant that answers questions about my data""");
#region Azure SQL
// Database connection string
var connectionString = _config["AZURE_SQL_CONNECTIONSTRING"];
try
{
await using var connection = new SqlConnection(connectionString);
Console.WriteLine("\nQuery results:");
await connection.OpenAsync();
// Hybrid search query
var sql =
@"DECLARE @e VECTOR(1536);
EXEC dbo.GET_EMBEDDINGS @model = 'text-embedding-ada-002', @text = '@userMessage', @embedding = @e OUTPUT;
-- Comprehensive query with multiple filters.
SELECT TOP(5)
f.Score,
f.Summary,
f.Text,
VECTOR_DISTANCE('cosine', @e, VectorBinary) AS Distance,
CASE
WHEN LEN(f.Text) > 100 THEN 'Detailed Review'
ELSE 'Short Review'
END AS ReviewLength,
CASE
WHEN f.Score >= 4 THEN 'High Score'
WHEN f.Score BETWEEN 2 AND 3 THEN 'Medium Score'
ELSE 'Low Score'
END AS ScoreCategory
FROM finefoodembeddings10k$ f
WHERE
f.UserId NOT LIKE 'Anonymous%' -- User-based filter to exclude anonymous users
AND f.Score >= 4 -- Score threshold filter
AND LEN(f.Text) > 50 -- Text length filter for detailed reviews
AND (f.Text LIKE '%juice%') -- Inclusion of specific words
ORDER BY
Distance, -- Order by distance
f.Score DESC, -- Secondary order by review score
ReviewLength DESC; -- Tertiary order by review length
";
// Set SQL Parameter to pass in user message
SqlParameter param = new SqlParameter();
param.ParameterName = "@userMessage";
param.Value = userMessage;
await using var command = new SqlCommand(sql, connection);
// add parameter to SqlCommand
command.Parameters.Add(param);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
// write results to console logs
Console.WriteLine("{0} {1} {2} {3}", "Score: " + reader.GetDouble(0), "Text: " + reader.GetString(1), "Summary: " + reader.GetString(2), "Distance: " + reader.GetDouble(3));
Console.WriteLine();
// add results to chat history
chatHistory.AddSystemMessage(reader.GetString(1) + ", " + reader.GetString(2));
}
}
catch (SqlException e)
{
Console.WriteLine($"SQL Error: {e.Message}");
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Console.WriteLine("Done");
#endregion
var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
chat,
new()
{
{ "request", userMessage },
{ "history", string.Join("\n", chatHistory.Select(x => x.Role + ": " + x.Content)) }
}
);
string message = "";
await foreach (var chunk in chatResult)
{
message += chunk;
}
// Append messages to chat history
chatHistory.AddUserMessage(userMessage!);
chatHistory.AddAssistantMessage(message);
serverResponse = message;
}
}