플러그인으로 RAG(Retrieval Augmented Generation) 구현하기
플러그인으로 RAG(Retrieval Augmented Generation) 구현하기
AI 에이전트는 근거 있는(grounded) 응답을 생성하기 위해 종종 외부 소스에서 데이터를 가져와야 해요. 이 추가 컨텍스트가 없으면 AI 에이전트가 환각(hallucination)을 일으키거나 잘못된 정보를 제공할 수 있어요. 이 문제를 해결하기 위해 플러그인을 사용해 외부 소스에서 데이터를 가져올 수 있어요.
RAG(검색 증강 생성)용 플러그인을 고려할 때는 두 가지 질문을 스스로에게 해야 해요.
- 필요 데이터를 어떻게 "검색"할 건가? 의미 검색(semantic search)이 필요한가, 전통 검색(classic search)이 필요한가?
- AI 에이전트가 필요로 하는 데이터를 미리 알고 있는가(사전 로드 데이터), 아니면 AI 에이전트가 데이터를 동적으로 가져와야 하는가?
- 데이터를 어떻게 안전하게 유지하고 민감 정보의 과도한 공유를 방지할 것인가?
의미 검색 vs 전통 검색
RAG용 플러그인을 개발할 때 사용할 수 있는 검색은 의미 검색과 전통 검색 두 가지가 있어요.
의미 검색(Semantic Search)
의미 검색은 벡터 데이터베이스를 활용해 단순히 키워드를 매칭하는 대신 쿼리의 의미와 맥락을 기반으로 정보를 이해하고 가져와요. 이 방식은 검색 엔진이 동의어, 관련 개념, 쿼리 뒤의 전반적인 의도 같은 언어의 뉘앙스를 파악할 수 있게 해 줘요.
의미 검색은 사용자 쿼리가 복잡하거나, 개방형이거나, 콘텐츠에 대한 더 깊은 이해가 필요한 환경에서 뛰어나요. 예를 들어 "사진 촬영에 가장 좋은 스마트폰"을 검색하면 "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; }
}
}
전통 검색(Classic Search)
전통 검색은 속성 기반 또는 기준 기반 검색이라고도 하는데, 데이터셋 내에서 정확한 용어나 값을 필터링·매칭하는 데 의존해요. 데이터베이스 쿼리, 재고 검색, 특정 속성으로 필터링이 필요한 상황에 특히 효과적이에요.
예를 들어 사용자가 특정 고객 ID의 모든 주문을 찾거나 특정 가격 범위와 카테고리의 상품을 가져오려고 한다면, 전통 검색이 정확하고 신뢰할 수 있는 결과를 제공해요. 하지만 전통 검색은 맥락이나 언어의 변형을 이해하지 못한다는 한계가 있어요.
[!TIP] 대부분의 경우 기존 서비스가 이미 전통 검색을 지원해요. 의미 검색을 구현하기 전에 기존 서비스가 AI 에이전트에 필요한 컨텍스트를 제공할 수 있는지 먼저 고려해 보세요.
예를 들어 CRM 시스템에서 전통 검색으로 고객 정보를 가져오는 플러그인을 생각해 볼게요. 여기서 AI는 고객 ID로 GetCustomerInfoAsync 함수를 호출해 필요한 정보를 가져오기만 하면 돼요.
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);
}
}
의미 검색으로 같은 검색 기능을 구현하는 것은 의미 쿼리의 비결정성 때문에 불가능하거나 비현실적일 가능성이 높아요.
각각 언제 사용할까
의미 검색과 전통 검색 중 선택은 쿼리의 성격에 따라 달라져요. 의미 검색은 지식 베이스나 고객 지원처럼 사용자가 자연어로 질문하거나 상품을 찾는 콘텐츠가 많은 환경에 이상적이에요. 반면 전통 검색은 정밀도와 정확한 매칭이 중요한 경우에 사용해야 해요.
일부 시나리오에서는 두 접근 방식을 결합해 포괄적인 검색 기능을 제공해야 할 수도 있어요. 예를 들어 e커머스 매장에서 고객을 지원하는 챗봇은 의미 검색으로 사용자 질문을 이해하고, 전통 검색으로 가격·브랜드·재고 같은 특정 속성을 기준으로 상품을 필터링할 수 있어요.
아래는 의시 검색과 전통 검색을 결합해 e커머스 데이터베이스에서 상품 정보를 가져오는 플러그인의 예시예요.
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 에이전트는 함수 호출을 사용해 데이터베이스나 API에서 상품 정보를 동적으로 가져와 응답을 생성해야 해요. 모든 가능한 상품 정보를 미리 가져와 AI 에이전트에 주는 것은 비현실적이죠.
아래는 동적 데이터 검색이 필요한 사용자와 AI 에이전트 사이의 주고받는 대화 예시예요.
| 역할 | 메시지 |
|---|---|
| 🔵 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 에이전트로 비즈니스 프로세스를 자동화하는 방법을 배울 수 있어요. 자세한 내용은 태스크 자동화 함수 사용을 참고하세요.
[!div class="nextstepaction"] 태스크 자동화 함수 알아보기