RAG(Retrieval Augmented Generation)를 위한 데이터 검색 함수 사용하기

RAG(Retrieval Augmented Generation)를 위한 데이터 검색 함수 사용하기

AI 에이전트가 근거 있는(grounded) 응답을 만들려면 외부 소스에서 데이터를 검색해야 하는 경우가 많아요. 이런 추가 컨텍스트가 없으면 AI 에이전트가 환각(hallucination)을 일으키거나 잘못된 정보를 제공할 수 있습니다. 이를 해결하기 위해 플러그인을 사용해 외부 소스에서 데이터를 검색할 수 있어요.

RAG(Retrieval Augmented Generation)를 위한 플러그인을 고려할 때는 스스로 두 가지 질문을 해보세요.

  1. 필요한 데이터를 어떻게(또는 AI 에이전트가 어떻게) "검색"할까? 시맨틱 검색이 필요한가, 클래식 검색이 필요한가?
  2. AI 에이전트가 필요로 하는 데이터를 미리 알고 있는가(사전 패치 데이터), 아니면 AI 에이전트가 동적으로 검색해야 하는가?
  3. 데이터를 안전하게 유지하고 민감 정보의 과공유를 막을 방법은 무엇인가?

출처: 공식 문서 — Using plugins for Retrieval Augmented Generation (RAG)

시맨틱 vs 클래식 검색

RAG를 위한 플러그인을 개발할 때 두 가지 유형의 검색을 쓸 수 있어요. 시맨틱 검색과 클래식 검색입니다.

시맨틱 검색

시맨틱 검색은 벡터 데이터베이스를 활용해, 키워드를 단순히 매칭하는 대신 질의의 의미와 컨텍스트에 기반해 정보를 이해하고 검색합니다. 이 방식 덕분에 검색 엔진이 동의어, 관련 개념, 질의 뒤의 전반적 의도 같은 언어의 뉘앙스를 파악할 수 있어요.

시맨틱 검색은 사용자 질의가 복잡하거나 개방적이거나 내용에 대한 더 깊은 이해를 요구하는 환경에서 뛰어납니다. 예를 들어 "사진 찍기 좋은 최고의 스마트폰"을 검색하면, "best", "smartphones", "photography"라는 단어를 그냥 매칭하는 대신 스마트폰의 카메라 기능 컨텍스트를 고려한 결과를 돌려줍니다.

LLM에 시맨틱 검색 함수를 제공할 때는 보통 단일 검색 질의를 가진 함수 하나만 정의하면 돼요. LLM은 이 함수를 사용해 필요한 정보를 검색합니다. 아래는 Azure AI Search를 사용해 주어진 질의와 유사한 문서를 찾는 시맨틱 검색 함수 예시예요.

using System.ComponentModel;
using System.Text.Json.Serialization;
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Models;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Embeddings;

public class InternalDocumentsPlugin
{
    private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService;
    private readonly SearchIndexClient _indexClient;

    public AzureAISearchPlugin(ITextEmbeddingGenerationService textEmbeddingGenerationService, SearchIndexClient indexClient)
    {
        _textEmbeddingGenerationService = textEmbeddingGenerationService;
        _indexClient = indexClient;
    }

    [KernelFunction("Search")]
    [Description("Search for a document similar to the given query.")]
    public async Task<string> SearchAsync(string query)
    {
        // Convert string query to vector
        ReadOnlyMemory<float> embedding = await _textEmbeddingGenerationService.GenerateEmbeddingAsync(query);

        // Get client for search operations
        SearchClient searchClient = _indexClient.GetSearchClient("default-collection");

        // Configure request parameters
        VectorizedQuery vectorQuery = new(embedding);
        vectorQuery.Fields.Add("vector");

        SearchOptions searchOptions = new() { VectorSearch = new() { Queries = { vectorQuery } } };

        // Perform search request
        Response<SearchResults<IndexSchema>> response = await searchClient.SearchAsync<IndexSchema>(searchOptions);

        // Collect search results
        await foreach (SearchResult<IndexSchema> result in response.Value.GetResultsAsync())
        {
            return result.Document.Chunk; // Return text from first result
        }

        return string.Empty;
    }

    private sealed class IndexSchema
    {
        [JsonPropertyName("chunk")]
        public string Chunk { get; set; }

        [JsonPropertyName("vector")]
        public ReadOnlyMemory<float> Vector { get; set; }
    }
}

클래식 검색

클래식 검색은 속성 기반 또는 기준 기반 검색이라고도 불리며, 데이터셋 내 정확한 용어나 값을 필터링·매칭하는 데 의존합니다. DB 질의, 재고 검색, 특정 속성으로 필터링이 필요한 모든 상황에서 특히 효과적이에요.

예를 들어 사용자가 특정 고객 ID로 주문한 모든 주문을 찾거나, 특정 가격 범위·카테고리 내 제품을 검색하려 할 때 클래식 검색은 정밀하고 신뢰할 수 있는 결과를 줍니다. 다만 클래식 검색은 컨텍스트나 언어의 변형을 이해하지 못한다는 한계가 있어요.

팁. 대부분의 경우 여러분의 기존 서비스가 이미 클래식 검색을 지원하고 있어요. 시맨틱 검색을 구현하기 전에, 기존 서비스가 AI 에이전트에 필요한 컨텍스트를 제공할 수 있는지 먼저 고려해 보세요.

예를 들어 클래식 검색으로 CRM 시스템에서 고객 정보를 검색하는 플러그인을 생각해 볼게요. 여기서 AI는 GetCustomerInfoAsync 함수를 고객 ID와 함께 호출하기만 하면 필요한 정보를 얻을 수 있습니다.

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class CRMPlugin
{
    private readonly CRMService _crmService;

    public CRMPlugin(CRMService crmService)
    {
        _crmService = crmService;
    }

    [KernelFunction("GetCustomerInfo")]
    [Description("Retrieve customer information based on the given customer ID.")]
    public async Task<Customer> GetCustomerInfoAsync(string customerId)
    {
        return await _crmService.GetCustomerInfoAsync(customerId);
    }
}

같은 검색 기능을 시맨틱 검색으로 구현하는 것은, 시맨틱 질의의 비결정적 성질 때문에 불가능하거나 비현실적일 가능성이 높아요.

각각 언제 쓸까

시맨틱과 클래식 검색 중 무엇을 고를지는 질의의 성격에 달려 있어요. 사용자가 자연어로 질문하거나 제품을 찾을 수 있는 지식 베이스, 고객 지원 같은 콘텐츠가 많은 환경에서는 시맨틱 검색이 이상적입니다. 반면 정밀도와 정확한 매칭이 중요한 경우에는 클래식 검색을 써야 해요.

어떤 시나리오에서는 두 접근법을 결합해 포괄적인 검색 능력을 제공해야 할 수도 있어요. 예를 들어 이커머스 매장에서 고객을 돕는 챗봇은 시맨틱 검색으로 사용자 질의를 이해하고, 클래식 검색으로 가격·브랜드·재고 같은 특정 속성에 따라 제품을 필터링할 수 있습니다.

아래는 이커머스 DB에서 제품 정보를 검색하기 위해 시맨틱과 클래식 검색을 결합한 플러그인 예시예요.

using System.ComponentModel;
using Microsoft.SemanticKernel;

public class ECommercePlugin
{
    [KernelFunction("search_products")]
    [Description("Search for products based on the given query.")]
    public async Task<IEnumerable<Product>> SearchProductsAsync(string query, ProductCategories category = null, decimal? minPrice = null, decimal? maxPrice = null)
    {
        // Perform semantic and classic search with the given parameters
    }
}

동적 vs 사전 패치 데이터 검색

RAG를 위한 플러그인을 개발할 때는 데이터 검색 과정이 정적인지 동적인지도 고려해야 해요. 그러면 필요한 때에만 데이터를 검색해 AI 에이전트의 성능을 최적화할 수 있습니다.

동적 데이터 검색

대부분의 경우 사용자 질의가 AI 에이전트가 검색해야 할 데이터를 결정합니다. 예를 들어 사용자가 두 제품의 차이를 물어볼 수 있어요. 그러면 AI 에이전트는 함수 호출을 사용해 DB나 API에서 제품 정보를 동적으로 검색해 응답을 만들어야 합니다. 가능한 모든 제품 정보를 미리 가져와 AI 에이전트에게 주는 건 비현실적이에요.

아래는 동적 데이터 검색이 필요한 사용자와 AI 에이전트 간의 대화 예시입니다.

Role Message
🔵 User Can you tell me about the best mattresses?
🔴 Assistant (function call) Products.Search("mattresses")
🟢 Tool [{"id": 25323, "name": "Cloud Nine"},{"id": 63633, "name": "Best Sleep"}]
🔴 Assistant Sure! We have both Cloud Nine and Best Sleep
🔵 User What's the difference between them?
🔴 Assistant (function call) Products.GetDetails(25323) Products.GetDetails(63633)
🟢 Tool { "id": 25323, "name": "Cloud Nine", "price": 1000, "material": "Memory foam" }
🟢 Tool { "id": 63633, "name": "Best Sleep", "price": 1200, "material": "Latex" }
🔴 Assistant Cloud Nine is made of memory foam and costs $1000. Best Sleep is made of latex and costs $1200.

사전 패치 데이터 검색

정적 데이터 검색은 외부 소스에서 데이터를 가져와 항상 AI 에이전트에 제공하는 방식이에요. 데이터가 모든 요청에 필요하거나, 데이터가 상대적으로 안정적이고 자주 변하지 않을 때 유용합니다.

예를 들어 항상 지역 날씨에 대한 질문에 답하는 에이전트를 생각해 보세요. WeatherPlugin이 있다고 가정하면, 날씨 API에서 날씨 데이터를 사전 패치해 채팅 기록에 넣을 수 있어요. 그러면 에이전트가 매번 API에 데이터를 요청하는 시간을 낭비하지 않고 날씨에 대한 응답을 만들 수 있습니다.

using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

IKernelBuilder builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey);
builder.Plugins.AddFromType<WeatherPlugin>();
Kernel kernel = builder.Build();

// Get the weather
var weather = await kernel.Plugins.GetFunction("WeatherPlugin", "get_weather").InvokeAsync(kernel);

// Initialize the chat history with the weather
ChatHistory chatHistory = new ChatHistory("The weather is:\n" + JsonSerializer.Serialize(weather));

// Simulate a user message
chatHistory.AddUserMessage("What is the weather like today?");

// Get the answer from the AI agent
IChatCompletionService chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var result = await chatCompletionService.GetChatMessageContentAsync(chatHistory);

데이터 보안 유지하기

외부 소스에서 데이터를 검색할 때는 데이터가 안전하고 민감 정보가 노출되지 않도록 하는 게 중요합니다. 민감 정보의 과공유를 막으려면 다음 전략을 쓸 수 있어요.

전략 설명
사용자의 인증 토큰 사용 AI 에이전트가 사용자 정보를 검색하는 데 쓰는 서비스 주체(service principal)를 만들지 마세요. 그러면 사용자가 검색된 정보에 접근할 권한이 있는지 검증하기 어려워집니다.
검색 서비스 재생성 피하기 벡터 DB로 새 검색 서비스를 만들기 전에, 필요한 데이터가 있는 서비스에 이미 존재하는지 확인하세요. 기존 서비스를 재사용하면 민감 콘텐츠를 복제하는 일을 피하고, 기존 접근 제어를 활용하며, 사용자가 접근할 수 있는 데이터만 반환하는 기존 필터링 메커니즘을 쓸 수 있습니다.
콘텐츠 대신 벡터 DB에 참조 저장 민감 콘텐츠를 벡터 DB에 복제하는 대신, 실제 데이터에 대한 참조를 저장할 수 있어요. 사용자가 이 정보에 접근하려면 먼저 인증 토큰으로 실제 데이터를 검색해야 합니다.

다음 단계

이제 외부 소스의 데이터로 AI 에이전트를 근거화(grounding)하는 방법을 알았으니, AI 에이전트로 비즈니스 프로세스를 자동화하는 방법을 배울 수 있어요.

더 알아보기 (Learn more)