다음을 통해 공유


.NET 앱에서 벡터 검색을 사용하여 RAG로 Azure OpenAI 구현

이 자습서에서는 .NET 앱에서 Open AI 모델 및 벡터 검색 기능을 사용하는 RAG 패턴의 통합을 살펴봅니다. 샘플 애플리케이션은 MongoDB용 Azure Cosmos DB에 저장된 사용자 지정 데이터에 대한 벡터 검색을 수행하고 GPT-35 및 GPT-4와 같은 생성 AI 모델을 사용하여 응답을 더욱 구체화합니다. 다음 섹션에서는 샘플 애플리케이션을 설정하고 이러한 개념을 보여 주는 주요 코드 예제를 살펴봅니다.

필수 조건

앱 개요

Cosmos 레시피 가이드 앱을 사용하면 레시피 데이터 집합에 대해 벡터 및 AI 기반 검색을 수행할 수 있습니다. 사용 가능한 레시피를 직접 검색하거나 재료 이름으로 앱에 메시지를 표시하여 관련 레시피를 찾을 수 있습니다. 앱과 앞의 섹션에서는 다음 워크플로를 안내하여 이러한 유형의 기능을 보여 줍니다.

  1. Azure Cosmos DB for MongoDB 데이터베이스에 샘플 데이터를 업로드합니다.

  2. Azure OpenAI text-embedding-ada-002 모델을 사용하여 업로드된 샘플 데이터에 대한 포함 및 벡터 인덱스를 만듭니다.

  3. 사용자 프롬프트에 따라 벡터 유사성 검색을 수행합니다.

  4. Azure OpenAI gpt-35-turbo 완성 모델을 사용하여 검색 결과 데이터를 기반으로 보다 의미 있는 답변을 작성합니다.

    실행 중인 샘플 앱을 보여 주는 스크린샷.

시작하기

  1. 다음 GitHub 리포지토리를 복제합니다.

    git clone https://github.com/microsoft/AzureDataRetrievalAugmentedGenerationSamples.git
    
  2. C#/CosmosDB-MongoDBvCore 폴더에서 CosmosRecipeGuide.sln 파일을 엽니다.

  3. appsettings.json 파일에서 다음 구성 값을 Azure OpenAI 및 Azure CosmosDB for MongoDb 값으로 바꿉니다.

    "OpenAIEndpoint": "https://<your-service-name>.openai.azure.com/",
    "OpenAIKey": "<your-API-key>",
    "OpenAIEmbeddingDeployment": "<your-ADA-deployment-name>",
    "OpenAIcompletionsDeployment": "<your-GPT-deployment-name>",
    "MongoVcoreConnection": "<your-Mongo-connection-string>"
    
  4. Visual Studio 맨 위에 있는 시작 단추를 눌러 앱을 시작 합니다.

앱 탐색

앱을 처음으로 실행하면 Azure Cosmos DB에 연결되고 아직 사용할 수 있는 레시피가 없다고 보고합니다. 앱에서 표시하는 단계에 따라 핵심 워크플로를 시작합니다.

  1. Cosmos DB에 레시피 업로드를 선택하고 Enter 키를 누릅니. 이 명령은 로컬 프로젝트에서 샘플 JSON 파일을 읽고 Cosmos DB 계정에 업로드합니다.

    Utility.cs 클래스의 코드는 로컬 JSON 파일을 구문 분석합니다.

    public static List<Recipe> ParseDocuments(string Folderpath)
    {
        List<Recipe> recipes = new List<Recipe>();
    
        Directory.GetFiles(Folderpath)
            .ToList()
            .ForEach(f =>
            {
                var jsonString= System.IO.File.ReadAllText(f);
                Recipe recipe = JsonConvert.DeserializeObject<Recipe>(jsonString);
                recipe.id = recipe.name.ToLower().Replace(" ", "");
                recipes.Add(recipe);
            }
        );
    
        return recipes;
    }
    

    UpsertVectorAsync 파일의 메서드는 MongoDB용 Azure Cosmos DB에 문서를 업로드합니다.

    public async Task UpsertVectorAsync(Recipe recipe)
        {
            BsonDocument document = recipe.ToBsonDocument();
    
            if (!document.Contains("_id"))
            {
                Console.WriteLine("UpsertVectorAsync: Document does not contain _id.");
                throw new ArgumentException("UpsertVectorAsync: Document does not contain _id.");
            }
    
            string? _idValue = document["_id"].ToString();
    
            try
            {
                var filter = Builders<BsonDocument>.Filter.Eq("_id", _idValue);
                var options = new ReplaceOptions { IsUpsert = true };
                await _recipeCollection.ReplaceOneAsync(filter, document, options);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Exception: UpsertVectorAsync(): {ex.Message}");
                throw;
            }
        }
    
  2. 레시피 벡터화를 선택하고 Cosmos DB에 저장합니다.

    Cosmos DB에 업로드된 JSON 항목에는 임베딩이 포함되어 있지 않아 벡터 검색을 통한 RAG에 최적화되지 않습니다. 임베딩은 텍스트의 의미를 나타내는 정보 밀도가 높은 숫자 표현입니다. 벡터 검색은 상황에 맞는 유사한 포함 항목이 있는 항목을 찾을 수 있습니다.

    GetEmbeddingsAsync 파일의 메서드는 데이터베이스의 각 항목에 대한 포함을 만듭니다.

    public async Task<float[]?> GetEmbeddingsAsync(dynamic data)
    {
        try
        {
            EmbeddingsOptions options = new EmbeddingsOptions(data)
            {
                Input = data
            };
    
            var response = await _openAIClient.GetEmbeddingsAsync(openAIEmbeddingDeployment, options);
    
            Embeddings embeddings = response.Value;
            float[] embedding = embeddings.Data[0].Embedding.ToArray();
    
            return embedding;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetEmbeddingsAsync Exception: {ex.Message}");
            return null;
        }
    }
    

    CreateVectorIndexIfNotExists VCoreMongoService.cs 파일에서 벡터 유사성 검색을 수행할 수 있는 벡터 인덱스를 만듭니다.

    public void CreateVectorIndexIfNotExists(string vectorIndexName)
    {
        try
        {
            //Find if vector index exists in vectors collection
            using (IAsyncCursor<BsonDocument> indexCursor = _recipeCollection.Indexes.List())
            {
                bool vectorIndexExists = indexCursor.ToList().Any(x => x["name"] == vectorIndexName);
                if (!vectorIndexExists)
                {
                    BsonDocumentCommand<BsonDocument> command = new BsonDocumentCommand<BsonDocument>(
                        BsonDocument.Parse(@"
                            { createIndexes: 'Recipe',
                                indexes: [{
                                name: 'vectorSearchIndex',
                                key: { embedding: 'cosmosSearch' },
                                cosmosSearchOptions: {
                                    kind: 'vector-ivf',
                                    numLists: 5,
                                    similarity: 'COS',
                                    dimensions: 1536 }
                                }]
                            }"));
    
                    BsonDocument result = _database.RunCommand(command);
                    if (result["ok"] != 1)
                    {
                        Console.WriteLine("CreateIndex failed with response: " + result.ToJson());
                    }
                }
            }
        }
        catch (MongoException ex)
        {
            Console.WriteLine("MongoDbService InitializeVectorIndex: " + ex.Message);
            throw;
        }
    }
    
  3. 애플리케이션에서 AI 길잡이에게 질문(이름 또는 설명으로 레시피 검색 또는 질문) 옵션을 선택하여 사용자 쿼리를 실행합니다.

    사용자 쿼리는 Open AI 서비스와 임베딩 모델을 사용하여 임베딩으로 변환됩니다. 그런 다음 포함이 MongoDB용 Azure Cosmos DB로 전송되고 벡터 검색을 수행하는 데 사용됩니다. VectorSearchAsync 파일의 메서드는 제공된 벡터에 가까운 벡터를 찾기 위해 벡터 검색을 수행하고 MongoDB vCore용 Azure Cosmos DB에서 문서 목록을 반환합니다.

    public async Task<List<Recipe>> VectorSearchAsync(float[] queryVector)
        {
            List<string> retDocs = new List<string>();
            string resultDocuments = string.Empty;
    
            try
            {
                //Search Azure Cosmos DB for MongoDB vCore collection for similar embeddings
                //Project the fields that are needed
                BsonDocument[] pipeline = new BsonDocument[]
                {
                    BsonDocument.Parse(
                        @$"{{$search: {{
                                cosmosSearch:
                                    {{ vector: [{string.Join(',', queryVector)}],
                                       path: 'embedding',
                                       k: {_maxVectorSearchResults}}},
                                       returnStoredSource:true
                                    }}
                                }}"),
                    BsonDocument.Parse($"{{$project: {{embedding: 0}}}}"),
                };
    
                var bsonDocuments = await _recipeCollection
                    .Aggregate<BsonDocument>(pipeline).ToListAsync();
    
                var recipes = bsonDocuments
                    .ToList()
                    .ConvertAll(bsonDocument =>
                        BsonSerializer.Deserialize<Recipe>(bsonDocument));
                return recipes;
            }
            catch (MongoException ex)
            {
                Console.WriteLine($"Exception: VectorSearchAsync(): {ex.Message}");
                throw;
            }
        }
    

    이 메서드는 GetChatCompletionAsync 사용자 프롬프트 및 관련 벡터 검색 결과에 따라 향상된 채팅 완료 응답을 생성합니다.

    public async Task<(string response, int promptTokens, int responseTokens)> GetChatCompletionAsync(string userPrompt, string documents)
    {
        try
        {
            ChatMessage systemMessage = new ChatMessage(
                ChatRole.System, _systemPromptRecipeAssistant + documents);
            ChatMessage userMessage = new ChatMessage(
                ChatRole.User, userPrompt);
    
            ChatCompletionsOptions options = new()
            {
                Messages =
                {
                    systemMessage,
                    userMessage
                },
                MaxTokens = openAIMaxTokens,
                Temperature = 0.5f, //0.3f,
                NucleusSamplingFactor = 0.95f,
                FrequencyPenalty = 0,
                PresencePenalty = 0
            };
    
            Azure.Response<ChatCompletions> completionsResponse =
                await openAIClient.GetChatCompletionsAsync(openAICompletionDeployment, options);
            ChatCompletions completions = completionsResponse.Value;
    
            return (
                response: completions.Choices[0].Message.Content,
                promptTokens: completions.Usage.PromptTokens,
                responseTokens: completions.Usage.CompletionTokens
            );
    
        }
        catch (Exception ex)
        {
            string message = $"OpenAIService.GetChatCompletionAsync(): {ex.Message}";
            Console.WriteLine(message);
            throw;
        }
    }
    

    또한 앱은 프롬프트 엔지니어링을 사용하여 Open AI 서비스가 제공된 레시피에 대한 응답을 제한하고 형식을 지정하도록 합니다.

    //System prompts to send with user prompts to instruct the model for chat session
    private readonly string _systemPromptRecipeAssistant = @"
        You are an intelligent assistant for Contoso Recipes.
        You are designed to provide helpful answers to user questions about
        recipes, cooking instructions provided in JSON format below.
    
        Instructions:
        - Only answer questions related to the recipe provided below.
        - Don't reference any recipe not provided below.
        - If you're unsure of an answer, say ""I don't know"" and recommend users search themselves.
        - Your response  should be complete.
        - List the Name of the Recipe at the start of your response followed by step by step cooking instructions.
        - Assume the user is not an expert in cooking.
        - Format the content so that it can be printed to the Command Line console.
        - In case there is more than one recipe you find, let the user pick the most appropriate recipe.";