Tutorial: Implantar um aplicativo .NET Blazor conectado ao Azure SQL e ao Azure OpenAI no Serviço de Aplicativo do Azure

Pode usar os seus próprios dados SQL para fundamentar o contexto da sua aplicação inteligente. Este artigo explica a criação de uma aplicação de geração aumentada de recuperação (RAG) para .NET Blazor, configurando uma pesquisa vetorial híbrida numa base de dados Azure SQL que possui embeddings vetorizados. O suporte ao vetor Azure SQL fornece novas funções vetoriais que ajudam a gerir os dados vetoriais.

Pré-requisitos

  • Uma base de dados Azure SQL com dados que pode vetorizar
  • Um recurso Azure OpenAI
  • Uma aplicação web .NET 8 ou 9 Blazor implementada no Azure App Service

Este tutorial utiliza um exemplo complementar do Clone e implementa uma aplicação de chat .NET 9 Blazor ligada ao Azure OpenAI.

1. Configurar a aplicação web Blazor

Cria uma caixa de chat básica para interagir.

  1. Certifique-se de que os Microsoft.SemanticKernel pacotes e Microsoft.Data.SqlClient estão instalados no ambiente de desenvolvimento.

  2. Na árvore de projetos da aplicação web, expanda as Páginas de Componentes> e crie um novo ficheiro em Páginas chamado OpenAI.razor.

  3. Adicione o seguinte código da caixa de chat ao ficheiro OpenAI.razor e guarde o ficheiro.

    @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;
    
        }
    

2. Configurar o cliente Azure OpenAI

Depois de adicionar a interface de chat, configure o cliente Azure OpenAI usando o Semantic Kernel. O código seguinte cria o cliente que se liga ao recurso Azure OpenAI.

O código faz referência a DEPLOYMENT_NAME, ENDPOINT, API_KEY, e MODEL_ID. Certifique-se de guardar estes valores em appsettings.json ou como variáveis de ambiente. Para instruções sobre como obter e gerir a informação de chaves e endpoints do Azure OpenAI, consulte Usar referências do Key Vault como definições de aplicação no Azure App Service e Azure Functions.

Observação

Se possível, deve usar a identidade gerida para proteger o seu cliente sem ter de gerir chaves API. Consulte o Tutorial: Construa um chatbot com Azure App Service e Azure OpenAI (.NET) para instruções sobre como configurar um cliente Azure OpenAI para usar identidade gerida.

Adicione o seguinte código ao ficheiro OpenAI.razor .

@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;

Agora tem uma aplicação de chat funcional ligada ao OpenAI. De seguida, configura a tua base de dados Azure SQL para trabalhar com a tua aplicação de chat.

3. Implantar modelos do Azure OpenAI

Para preparar uma base de dados Azure SQL para pesquisa vetorial híbrida, utiliza-se um modelo de embedding para gerar embeddings a usar na pesquisa. Depois de as incorporações apropriadas estarem na base de dados, pode usá-las juntamente com o seu modelo de linguagem inicial para realizar uma pesquisa vetorial híbrida na base de dados.

Este exemplo utiliza o text-embedding-ada-002 modelo para gerar embeddings e gpt-4o-mini para o modelo de linguagem. Antes de continuar, utilize o portal Microsoft Foundry para implementar os dois modelos no seu recurso OpenAI. Para mais informações, consulte Implementar Modelos Microsoft Foundry no portal Foundry.

4. Vetorizar a sua base de dados Azure SQL

Para realizar uma pesquisa vetorial híbrida numa base de dados Azure SQL, os embeddings apropriados devem estar na base de dados. Vetorize a sua base de dados antes de continuar. Existem muitas formas de vetorizar uma base de dados. Uma opção é usar o vetorizador Azure SQLDB.

5. Criar um procedimento armazenado que gere embeddings

Pode usar suporte vetorial Azure SQL para criar um procedimento armazenado que utiliza um VECTOR tipo de dado para armazenar embeddings gerados para consultas de pesquisa. O procedimento armazenado invoca um endpoint externo da API REST para obter as incorporações.

A consulta SQL seguinte cria o procedimento armazenado. Substitua o <resourcename> marcador de posição no @url parâmetro pelo nome do seu recurso Azure OpenAI, e substitua o <openAIkey> marcador de posição pela chave API do seu modelo de embedding de texto. O nome do modelo faz parte do @url, que é preenchido com o termo de pesquisa.

Pode usar o Query Editor no portal Azure ou o Visual Studio Code para se ligar à sua base de dados e executar a consulta. Para mais informações sobre a utilização do Visual Studio Code, consulte a documentação da extensão MSSQL.

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

Após a criação, este procedimento armazenado aparece em Procedimentos Armazenados na pasta Programmability da base de dados Azure SQL. Pode usar o nome do seu modelo de incorporação de texto para executar uma pesquisa de similaridade de teste no editor de consultas SQL. Este teste utiliza o procedimento armazenado para gerar embeddings, calcular a distância vetorial usando uma função de distância vetorial e devolver resultados com base na consulta de texto.

6. Conecte-se e pesquise seu banco de dados

Agora que a sua base de dados está configurada para criar embeddings, pode ligar-se à base de dados na sua aplicação e configurar a consulta de pesquisa vetorial híbrida. Adicione o seguinte código ao ficheiro OpenAI.razor , certificando-se de que a cadeia de ligação utiliza a cadeia de ligação da base de dados Azure SQL implementada.

O código utiliza um parâmetro SQL que transmite de forma segura a entrada do utilizador da aplicação de chat para a consulta. O exemplo utiliza o conjunto de dados Amazon Fine Food Reviews .

// 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");

A própria consulta SQL utiliza uma pesquisa híbrida para executar o procedimento armazenado que cria embeddings e filtra os resultados usando SQL. Este exemplo atribui pontuações aos resultados, ordena os resultados para selecionar os melhores e usa-os como contexto base para gerar uma resposta.

Proteja os seus dados com identidade gerida

O Azure SQL pode usar identidade gerida com o Microsoft Entra para proteger o recurso SQL configurando autenticação sem palavra-passe. Use os seguintes passos para configurar uma cadeia de ligação sem palavra-passe para usar na sua aplicação.

  1. No recurso Azure SQL Server no portal Azure, selecione Definições>Microsoft Entra ID no menu de navegação à esquerda.
  2. Seleccionar Definir administrador, pesquisar e seleccionar o teu nome, e depois seleccionar Guardar. O Entra ID está agora configurado no seu servidor SQL e aceita autenticação Entra ID.
  3. Vai ao recurso da tua base de dados, seleciona Definições>cadeia de ligação no menu de navegação da esquerda, e copia a cadeia de ligação ADO.NET (autenticação sem palavra-passe Microsoft Entra).
  4. Atualize a cadeia de ligação na appsettings.jsonda sua aplicação.

Agora pode testar a sua aplicação localmente com a sua cadeia de ligação sem palavra-passe.

Conceder acesso à base de dados ao Serviço de Aplicações

Antes de poder usar a sua aplicação web para chamar a base de dados Azure SQL usando identidade gerida, deve conceder à aplicação o acesso necessário à base de dados.

  1. Na sua aplicação web no portal Azure, selecione Definições>Identidade no menu de navegação à esquerda.

  2. No separador Sistema Atribuído , defina Estado para Ligado se ainda não estiver definido, e depois selecione Guardar.

  3. Vá ao seu recurso de base de dados Azure SQL e selecione Editor de Consultas no menu de navegação da esquerda. Inicie sessão na sua base de dados se necessário.

  4. No editor de Consultas, execute os seguintes comandos SQL que criam a aplicação web como utilizador e lhe atribuem as permissões necessárias, substituindo <your-app-name> pelo nome da sua aplicação 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
    
  5. De seguida, conceda à aplicação web acesso ao uso do procedimento armazenado e do endpoint 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
    

A sua base de dados Azure SQL está agora segura.

7. Implementar a aplicação

Agora pode implementar a sua aplicação no Azure App Service. Se quiser implementar a aplicação usando um azd modelo, veja Deployar com Azure Developer CLI.

Exemplo completo

O código seguinte mostra a página completa adicionada do 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;

    }
}