Retrieval Augmented Generation
Retrieval Augmented Generation (RAG)
LLM은 긴 형식의 콘텐츠, 사실 정확성, 컨텍스트 인식에서 한계를 보일 때가 있어요. Retrieval Augmented Generation(RAG)은 그 한계를 극복하는 데 유용한 기법입니다. Spring AI는 직접 나만의 RAG 흐름을 만들거나, Advisor API로 바로 쓸 수 있는 RAG 흐름을 사용할 수 있게 해 주는 모듈형 아키텍처로 RAG를 지원해요.
출처: 공식문서
NOTE: Retrieval Augmented Generation에 대해 더 알아보려면 concepts 섹션을 참고하세요.
Advisors
Spring AI는 Advisor API로 흔한 RAG 흐름을 기본 제공합니다.
QuestionAnswerAdvisor나 VectorStoreChatMemoryAdvisor를 쓰려면 프로젝트에 spring-ai-vector-store-advisor 의존성을 추가해야 해요.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
QuestionAnswerAdvisor
벡터 데이터베이스는 AI 모델이 모르는 데이터를 저장합니다. 사용자 질문이 AI 모델로 전송될 때 QuestionAnswerAdvisor가 사용자 질문과 관련된 문서를 벡터 데이터베이스에 질의해요.
벡터 데이터베이스의 응답은 AI 모델이 응답을 생성할 컨텍스트를 제공하도록 사용자 텍스트에 덧붙여집니다.
VectorStore에 이미 데이터를 로드했다고 가정하면, ChatClient에 QuestionAnswerAdvisor 인스턴스를 제공해 RAG를 수행할 수 있어요.
ChatResponse response = ChatClient.builder(chatModel)
.build().prompt()
.advisors(QuestionAnswerAdvisor.builder(vectorStore).build())
.user(userText)
.call()
.chatResponse();
이 예시에서 QuestionAnswerAdvisor는 Vector Database의 모든 문서에 대해 유사도 검색을 수행합니다. 검색되는 문서 유형을 제한하려면 SearchRequest가 모든 VectorStore에서 이식 가능한 SQL 같은 필터 표현식을 받습니다.
이 필터 표현식은 QuestionAnswerAdvisor를 만들 때 구성할 수 있어, 항상 모든 ChatClient 요청에 적용되게 할 수도 있고, 요청별로 런타임에 제공할 수도 있습니다.
임계값이 0.8이고 상위 6개 결과를 반환하는 QuestionAnswerAdvisor 인스턴스를 만드는 방법입니다.
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.8d).topK(6).build())
.build();
동적 필터 표현식
FILTER_EXPRESSION advisor 컨텍스트 파라미터로 런타임에 SearchRequest 필터 표현식을 갱신하세요.
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().build())
.build())
.build();
// Update filter expression at runtime
String content = this.chatClient.prompt()
.user("Please answer my question XYZ")
.advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'Spring'"))
.call()
.content();
FILTER_EXPRESSION 파라미터를 사용하면 제공된 표현식에 따라 검색 결과를 동적으로 필터링할 수 있습니다.
사용자 지정 템플릿
QuestionAnswerAdvisor는 기본 템플릿을 사용해 검색된 문서로 사용자 질문을 증강합니다. .promptTemplate() 빌더 메서드로 직접 PromptTemplate 객체를 제공해 이 동작을 커스터마이즈할 수 있어요.
NOTE: 여기서 제공하는 PromptTemplate은 advisor가 검색된 컨텍스트를 사용자 쿼리와 병합하는 방식을 커스터마이즈합니다. 이는 ChatClient 자체에서 TemplateRenderer를 구성하는 것(.templateRenderer() 사용)과는 다릅니다. 후자는 advisor가 실행되기 전에 초기 사용자/시스템 프롬프트 내용 렌더링에 영향을 줍니다. 클라이언트 수준 템플릿 렌더링에 대한 자세한 내용은 ChatClient Prompt Templates를 참고하세요.
사용자 지정 PromptTemplate은 어떤 TemplateRenderer 구현이든 쓸 수 있습니다(기본값은 StringTemplate 엔진 기반 StPromptTemplate). 중요한 요구 사항은 템플릿이 다음 두 자리표시자를 포함해야 한다는 것입니다.
- 사용자 질문을 받는
query자리표시자. - 검색된 컨텍스트를 받는
question_answer_context자리표시자.
PromptTemplate customPromptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.template("""
<query>
Context information is below.
---------------------
<question_answer_context>
---------------------
Given the context information and no prior knowledge, answer the query.
Follow these rules:
1. If the answer is not in the context, just say that you don't know.
2. Avoid statements like "Based on the context..." or "The provided information...".
""")
.build();
String question = "Where does the adventure of Anacletus and Birba take place?";
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.promptTemplate(customPromptTemplate)
.build();
String response = ChatClient.builder(chatModel).build()
.prompt(question)
.advisors(qaAdvisor)
.call()
.content();
NOTE: QuestionAnswerAdvisor.Builder.userTextAdvise() 메서드는 더 유연한 커스터마이즈를 위해 .promptTemplate()을 쓰는 쪽으로 폐기(비권장)됐습니다.
RetrievalAugmentationAdvisor
Spring AI는 나만의 RAG 흐름을 만드는 데 쓸 수 있는 RAG 모듈 라이브러리를 포함합니다. RetrievalAugmentationAdvisor는 모듈형 아키텍처를 기반으로 가장 흔한 RAG 흐름의 기본 제공 구현을 제공하는 Advisor입니다.
RetrievalAugmentationAdvisor를 쓰려면 프로젝트에 spring-ai-rag 의존성을 추가해야 해요.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
순차 RAG 흐름
Naive RAG
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
기본적으로 RetrievalAugmentationAdvisor는 검색된 컨텍스트가 비어 있는 것을 허용하지 않습니다. 그런 일이 생기면 모델이 사용자 쿼리에 답하지 않도록 지시해요. 빈 컨텍스트를 허용하려면 다음과 같이 합니다.
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
VectorStoreDocumentRetriever는 메타데이터 기반으로 검색 결과를 필터링하는 FilterExpression을 받습니다. VectorStoreDocumentRetriever를 인스턴스화할 때 제공하거나, FILTER_EXPRESSION advisor 컨텍스트 파라미터로 요청별 런타임에 제공할 수 있어요.
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.advisors(a -> a.param(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "type == 'Spring'"))
.user(question)
.call()
.content();
자세한 내용은 VectorStoreDocumentRetriever를 참고하세요.
Advanced RAG
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder.build().mutate())
.build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
모델에 전달하기 전에 검색된 문서를 후처리하는 데 DocumentPostProcessor API를 쓸 수도 있습니다. 예를 들어 이 인터페이스로 검색된 문서를 쿼리와의 관련성에 기반해 재랭킹하거나, 관련 없거나 중복된 문서를 제거하거나, 각 문서의 내용을 압축해 노이즈와 중복을 줄일 수 있어요.
Modules
Spring AI는 "Modular RAG: Transforming RAG Systems into LEGO-like Reconfigurable Frameworks" 논문에 상세히 설명된 모듈성 개념에서 영감을 받은 Modular RAG 아키텍처를 구현합니다.
Pre-Retrieval
Pre-Retrieval 모듈은 최상의 검색 결과를 얻기 위해 사용자 쿼리를 처리하는 책임이 있습니다.
Query Transformation
입력 쿼리를 검색 작업에 더 효과적으로 만들기 위해 변환하는 컴포넌트입니다. 형편없이 만들어진 쿼리, 모호한 용어, 복잡한 어휘, 지원되지 않는 언어 같은 문제를 다룹니다.
IMPORTANT: QueryTransformer를 쓸 때는 ChatClient.Builder를 낮은 temperature(예: 0.0)로 구성하는 것을 권장합니다. 더 결정적이고 정확한 결과를 보장해 검색 품질을 높여요. 대부분의 채팅 모델 기본 temperature는 최적의 쿼리 변환에는 보통 너무 높아서 검색 효과가 떨어집니다.
CompressionQueryTransformer
CompressionQueryTransformer는 대형 언어 모델을 사용해 대화 기록과 후속 쿼리를, 대화의 본질을 포착하는 독립 쿼리로 압축합니다.
이 트랜스포머는 대화 기록이 길고 후속 쿼리가 대화 컨텍스트와 관련 있을 때 유용합니다.
Query query = Query.builder()
.text("And what is its second largest city?")
.history(new UserMessage("What is the capital of Denmark?"),
new AssistantMessage("Copenhagen is the capital of Denmark."))
.build();
QueryTransformer queryTransformer = CompressionQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
Query transformedQuery = queryTransformer.transform(query);
이 컴포넌트가 쓰는 프롬프트는 빌더의 promptTemplate() 메서드로 커스터마이즈할 수 있습니다.
RewriteQueryTransformer
RewriteQueryTransformer는 대형 언어 모델을 사용해 사용자 쿼리를 다시 써서, 벡터 스토어나 웹 검색 엔진 같은 대상 시스템을 질의할 때 더 나은 결과를 얻게 합니다.
이 트랜스포머는 사용자 쿼리가 장황하거나, 모호하거나, 검색 결과 품질에 영향을 줄 수 있는 관련 없는 정보를 포함할 때 유용합니다.
Query query = new Query("I'm studying machine learning. What is an LLM?");
QueryTransformer queryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.build();
Query transformedQuery = queryTransformer.transform(query);
이 컴포넌트가 쓰는 프롬프트는 빌더의 promptTemplate() 메서드로 커스터마이즈할 수 있습니다.
TranslationQueryTransformer
TranslationQueryTransformer는 대형 언어 모델을 사용해 쿼리를, 문서 임베딩을 생성하는 데 쓰인 임베딩 모델이 지원하는 대상 언어로 번역합니다. 쿼리가 이미 대상 언어면 그대로 반환되고, 쿼리 언어가 알 수 없으면 역시 그대로 반환됩니다.
이 트랜스포머는 임베딩 모델이 특정 언어로 학습됐는데 사용자 쿼리가 다른 언어일 때 유용합니다.
Query query = new Query("Hvad er Danmarks hovedstad?");
QueryTransformer queryTransformer = TranslationQueryTransformer.builder()
.chatClientBuilder(chatClientBuilder)
.targetLanguage("english")
.build();
Query transformedQuery = queryTransformer.transform(query);
이 컴포넌트가 쓰는 프롬프트는 빌더의 promptTemplate() 메서드로 커스터마이즈할 수 있습니다.
Query Expansion
입력 쿼리를 쿼리 목록으로 확장하는 컴포넌트입니다. 대안 쿼리 공식을 제공해 형편없이 만들어진 쿼리 같은 문제를 다루거나, 복잡한 문제를 더 단순한 하위 쿼리로 분해합니다.
MultiQueryExpander
MultiQueryExpander는 대형 언어 모델을 사용해 쿼리를 의미적으로 다양한 여러 변형으로 확장해 다른 관점을 포착합니다. 추가 컨텍스트 정보를 검색하고 관련 결과를 찾을 가능성을 높이는 데 유용합니다.
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.numberOfQueries(3)
.build();
List<Query> queries = queryExpander.expand(new Query("How to run a Spring Boot app?"));
기본적으로 MultiQueryExpander는 확장된 쿼리 목록에 원래 쿼리를 포함합니다. 빌더의 includeOriginal 메서드로 이 동작을 비활성화할 수 있어요.
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatClientBuilder(chatClientBuilder)
.includeOriginal(false)
.build();
이 컴포넌트가 쓰는 프롬프트는 빌더의 promptTemplate() 메서드로 커스터마이즈할 수 있습니다.
Retrieval
Retrieval 모듈은 벡터 스토어 같은 데이터 시스템을 질의하고 가장 관련성 높은 문서를 검색하는 책임이 있습니다.
Document Search
검색 엔진, 벡터 스토어, 데이터베이스, 지식 그래프 같은 기반 데이터 소스에서 Document를 검색하는 컴포넌트입니다.
VectorStoreDocumentRetriever
VectorStoreDocumentRetriever는 입력 쿼리와 의미적으로 유사한 문서를 벡터 스토어에서 검색합니다. 메타데이터 기반 필터링, 유사도 임계값, top-k 결과를 지원해요.
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.73)
.topK(5)
.filterExpression(new FilterExpressionBuilder()
.eq("genre", "fairytale")
.build())
.build();
List<Document> documents = retriever.retrieve(new Query("What is the main character of the story?"));
필터 표현식은 정적일 수도 있고 동적일 수도 있습니다. 동적 필터 표현식에서는 Supplier를 전달할 수 있어요.
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.filterExpression(() -> new FilterExpressionBuilder()
.eq("tenant", TenantContextHolder.getTenantIdentifier())
.build())
.build();
List<Document> documents = retriever.retrieve(new Query("What are the KPIs for the next semester?"));
Query API의 FILTER_EXPRESSION 파라미터로 요청별 필터 표현식을 제공할 수도 있습니다. 요청별 필터 표현식과 리트리버별 필터 표현식이 모두 제공되면 요청별 필터 표현식이 우선합니다.
Query query = Query.builder()
.text("Who is Anacletus?")
.context(Map.of(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "location == 'Whispering Woods'"))
.build();
List<Document> retrievedDocuments = documentRetriever.retrieve(query);
Document Join
여러 쿼리와 여러 데이터 소스에 기반해 검색된 문서들을 단일 문서 컬렉션으로 결합하는 컴포넌트입니다. 조인 과정의 일부로 중복 문서와 상호 랭킹(reciprocal ranking) 전략도 다룰 수 있습니다.
ConcatenationDocumentJoiner
ConcatenationDocumentJoiner는 여러 쿼리와 여러 데이터 소스에 기반해 검색된 문서들을 단일 문서 컬렉션으로 연결해 결합합니다. 중복 문서가 있으면 첫 번째 항목이 유지됩니다. 각 문서의 점수는 그대로 유지돼요.
Map<Query, List<List<Document>>> documentsForQuery = ...
DocumentJoiner documentJoiner = new ConcatenationDocumentJoiner();
List<Document> documents = documentJoiner.join(documentsForQuery);
Post-Retrieval
Post-Retrieval 모듈은 최상의 생성 결과를 얻기 위해 검색된 문서를 처리하는 책임이 있습니다.
Document Post-Processing
쿼리에 기반해 검색된 문서를 후처리하는 컴포넌트입니다. lost-in-the-middle, 모델의 컨텍스트 길이 제한, 검색된 정보의 노이즈·중복 감소 필요성 같은 문제를 다룹니다.
예를 들어 문서를 쿼리와의 관련성에 따라 랭킹하거나, 관련 없거나 중복된 문서를 제거하거나, 각 문서의 내용을 압축해 노이즈와 중복을 줄일 수 있어요.
Generation
Generation 모듈은 사용자 쿼리와 검색된 문서에 기반해 최종 응답을 생성하는 책임이 있습니다.
Query Augmentation
사용자 쿼리에 추가 데이터를 증강하는 컴포넌트입니다. LLM이 사용자 쿼리에 답하는 데 필요한 컨텍스트를 제공하는 데 유용해요.
ContextualQueryAugmenter
ContextualQueryAugmenter는 제공된 문서의 내용에서 컨텍스트 데이터로 사용자 쿼리를 증강합니다.
QueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder().build();
기본적으로 ContextualQueryAugmenter는 검색된 컨텍스트가 비어 있는 것을 허용하지 않습니다. 그런 일이 생기면 모델이 사용자 쿼리에 답하지 않도록 지시해요.
allowEmptyContext 옵션을 켜면 검색된 컨텍스트가 비어 있어도 모델이 응답을 생성하도록 허용할 수 있습니다.
QueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build();
이 컴포넌트가 쓰는 프롬프트는 빌더의 promptTemplate()와 emptyContextPromptTemplate() 메서드로 커스터마이즈할 수 있습니다.
더 알아보기
- Advisors — RAG advisor 패턴
- Vector Stores — 벡터 스토어
- Observability — RAG 연산 관측