spring-ai/04.ToolCalling과 RAG

03. RAG와 QuestionAnswerAdvisor

은서파 2025. 6. 6. 12:21

이번 포스트에서는 RAG를 위한 Spring AI의 지원을 살펴보자.

 

RAG

 

RAG란

Retrieval Augmented Generation(RAG: 검색 증강 생성) 긴 형식의 콘텐츠 처리에 대한 어려움, 사실의 정확성,  맥락 인식의 어려움 등 문제를 해결하기 위한 기술이다.  모델이 학습 데이터에 포함되지 않은 외부 데이터를 실시간으로 검색(retrieval)하고 이를 기존 지식에 보강(Augmented) 해서 답변을 생성(generation)하는 것을 의미한다.

RAG 진행 절차 출처: Spring.io

Spring AI는 모듈형 아키텍쳐를 제공하여 사용자가 직접 맞춤형 RAG 흐름을 구축하거나 Advisor API를 사용하여 RAG 흐름을 이용할 수 있도록 지원한다.

 

ETL Pipeline과 Document

RAG의 출발은 데이터를 쌓는 것에서 시작하는데 ETL Pipeline(Extract: 추출, Transfer: 변환, Load: 적재)을 통해 처리 한다. 이때 처리되는 데이터의 단위 객체는 Document이다.

데이터는 Document로 저장된다. 출처: spring.io

Document는 content, metadata, media로 구성된다.

  • content: 주된 내용으로 문자열
  • metadata: 데이터 검색에 사용되는 부가적인 데이터(VectorStore의 MetadataField)
  • modal: 멀티 모달 형태의 입력 정보

 ETL Pipeline은 Document Reader, Document Transformer, DocumentWriter를 이용해 Document를 처리한다.

ETL Pipeline 구성 출처: Spring.io

  • DocumentReader: Supplier<List<Document>>의 구현체로 json, markdown, pdf, text 등에서 데이터 수집
  • DocumentTransformer: Function<List<Document>, List<Document>>의 구현체로 수집된 Document를 원하는 형태로 변환
  • DocumentWriter: Consumer<List<Document>>의 구현체로 VectorStore, File 등에 저장

 

Advisors

Spring AI는 RAG를 지원하기 위해 QuestionAnswerAdvisor나 RetrievalAugmentationAdvisor를 제공한다. 이들을 사용하기 위해서는 다음의 의존성이 추가로 필요하다.

<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>

 

QuestionAnswerAdvisor

 

QuestionAnswerAdvisor

QuestionAnswerAdvisor는 VectorStore에서 사용자 질문과 관련된 문서를 검색하고 검색 결과를 사용자 텍스트에 추가해서  모델이 응답을 생성할 수 있도록 컨텍스트를 제공한다.

QuestionAnswerAdvisor를 설정한느 방법을 일반 Advisor와 다르지 않다. 단지 사용할 VectorStore만 넘겨주면 된다. 다음은 기본 설정 만으로 구성된 QuestionAnswerAdvisor 사용법이다.

public String ragGeneration(String userInput) {
    return ollamaGemma3ChatClient.prompt()
            .user(userInput)
            .advisors(SimpleLoggerAdvisor.builder().order(Ordered.LOWEST_PRECEDENCE - 1).build(),
                    QuestionAnswerAdvisor.builder(vectorStore).build())
            .call()
            .content();
}

QuestionAnswerAdvisor는 user message를 일단 vectorstore에  질의(Retrieve)해서 결과로 user message를 보완(Augment) 시키고 그 보완된 prompt로 모델에게 요청해서 결과를 생성(Generate)한다.

QuestionAnswerAdvisor와 RAG

기본적으로 설정된 prompt를 살펴보면 조회된 내용만을 바탕으로 이야기 하며 조회된 내용이 없다면 할말 없다고 이야기 하도록 되어있다.

 

구조

다음은 QuestionAnswerAdvisor의 코드 일부이다. final로 선언된 다양한 요소들이 있고 일반적으로 이들을 builder pattern으로 수정하는 형태로 Advisor를 구성한다.

public class QuestionAnswerAdvisor implements BaseAdvisor {
    private static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = new PromptTemplate("""
      {query}
      Context information is below, surrounded by ---------------------
      ---------------------
      {question_answer_context}
      ---------------------
      Given the context and provided history information and not prior knowledge,
      reply to the user comment. If the answer is not in the context, inform
      the user that you can't answer the question.
      """);
      
    private final VectorStore vectorStore;       // vector store
    private final PromptTemplate promptTemplate; // prompt template 구성
    private final SearchRequest searchRequest;   // 검색 조건
    private final Scheduler scheduler;           // 비동기 방식에서 사용
    private final int order;                     // advisor 적용 순위
    . . .
  	public static Builder builder(VectorStore vectorStore) {
		return new Builder(vectorStore);
	}
}

 

사용

먼저 상황에 따라 사용할 prompt를 다양하게 해보자. 첫번째는 기본적으로 제공되는 prompt와 같이 검색된 내용이 있으면 그것을 바탕으로 응답하고 없다면 이야기 하는 형태이다. 두 번째는 만약 검색된 내용이 없다면 사전 학습 내용을 바탕으로 응답하는 형태이다.

quietjun.ai.rag-prompt-strict={query}\
            [context information] is surrounded by <context> tag.\
            <context>\
            {question_answer_context}\
            </context>\
            The response format follows these guidelines:\
            1. Answer the user's question based on the provided [context information].\
            1-1. Do not use pre-trained prior knowledge.\
            2. If there is no [context information], inform that "I cannot answer the question"\

quietjun.ai.rag-prompt-nostrict={query}\
            [context information] is surrounded by <context> tag.\
            <context>\
            {question_answer_context}\
            </context>\
            The response format follows these guidelines:\
            1. Answer the user's question based on the provided [context information].\
            1-1. Do not use pre-trained prior knowledge.\
            2. If there is no [context information], inform that "No context information is provided. The following is based on pre-trained knowledge."\
            2-1. Then answer the user's question based on pre-trained knowledge.

다음은 Service 부분이다. 핵심은 QuestionAnswerAdvisor를 구성하는 부분이다.

private final VectorStore vectorStore;

public String ragGeneration(String userInput, SearchRequest searchRequest, String ragPrompt) {

    var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(searchRequest)
            .promptTemplate(PromptTemplate.builder().template(ragPrompt).build())
            .build();

    return ollamaGemma3ChatClient.prompt()
            .user(userInput)
            .advisors(SimpleLoggerAdvisor.builder().order(Ordered.LOWEST_PRECEDENCE - 1).build(),
                    qaAdvisor)
            .call()
            .content();
}

마지막으로 사용해보자.

@Value("${quietjun.ai.rag-prompt-strict}")
String prompt1;

@Value("${quietjun.ai.rag-prompt-nostrict}")
String prompt2;

@Autowired
AiChatService aiService;

@Test
void simpleRAGTest() {
  String userInput = "세상에 대해서 알려줘.";
  // 조건에 맞는 데이터 검색
  var searchRequest = SearchRequest.builder()
                .query(userInput)
                .topK(3)
                .filterExpression("category=='simple'")
                .similarityThreshold(0)
                .build();
                
  String content = aiService.ragGeneration(userInput, searchRequest, prompt1);
  log.debug("값 있음: {}", content);
  searchRequest = SearchRequest.from(searchRequest).similarityThreshold(1).build();
  content = aiService.ragGeneration(userInput, searchRequest, prompt1);
  log.debug("값 없음(그대로): {}", content);
  content = aiService.ragGeneration(userInput, searchRequest, prompt2);
  log.debug("값 없음(창의적): {}", content);
}

한번쯤은 SimpleLoggerAdvisor의 내용을 분석해보는 것도 의미 있다. 

더보기
[
  {
    "prompt": {
      "messages": [
        {
          "textContent": "You are a very helpful assistant.Your response must be in the following language only: korean. Your response tone must be strictly as follows: chill.Ensure your response strictly adheres to these two conditions. Let's think step by step.",
          "messageType": "SYSTEM",
          "metadata": {
            "messageType": "SYSTEM"
          }
        },
        {
          "content": "세상에 대해서 알려줘.[context information] is surrounded by <context> tag.<context>Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!\nYou walk forward facing the past and you turn back toward the future.\nThe World is Big and Salvation Lurks Around the Corner</context>The response format follows these guidelines:1. Answer the user's question based on the provided [context information].1-1. Do not use pre-trained prior knowledge.2. If there is no [context information], inform that \"I cannot answer the question\"",
          "properties": {
            "messageType": "USER"
          },
          "messageType": "USER"
        }
      ],
      "modelOptions": "org.springframework.ai.ollama.api.OllamaOptions@42da7f23"
    },
    "context": {
      "qa_retrieved_documents": [
        {
          "id": "b7941a23-0ec9-4126-9663-a517eb0d609d",
          "text": "Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!",
          "media": null,
          "metadata": {
            "vector_score": 0.22521853,
            "distance": 0.22521853,
            "category": "simple",
            "meta_txt": "hong",
            "meta_num": 2000
          },
          "score": 0.7747814655303955
        },
        {
          "id": "4ecfe756-aad3-4d9c-a87e-b7088771a1ff",
          "text": "You walk forward facing the past and you turn back toward the future.",
          "media": null,
          "metadata": {
            "vector_score": 0.25476533,
            "distance": 0.25476533,
            "category": "simple",
            "meta_num": 2001
          },
          "score": 0.7452346682548523
        },
        {
          "id": "bb5d38b2-33e9-4db9-b369-86999d138437",
          "text": "The World is Big and Salvation Lurks Around the Corner",
          "media": null,
          "metadata": {
            "vector_score": 0.25564194,
            "distance": 0.25564194,
            "category": "simple",
            "meta_txt": "jang",
            "meta_num": 2001
          },
          "score": 0.7443580627441406
        }
      ]
    }
  }
]

다음은 전체적인 동작 결과이다.

12:14:40 [DEBUG] c.q.s.t.SimpleRAGTest.simpleRAGTest.104 값 있음: 
음, 세상에 대해서 이야기해달라고 하셨네요. 주어진 정보로는 “세상은 커요. 구원의 그림자가 곧 곁에 있을 것 같아요.” 라고 말할 수 있겠네요. 좀 더 자세히 알고 싶으시면, 다른 질문을 해주세요. 😉

12:14:42 [DEBUG] c.q.s.t.SimpleRAGTest.simpleRAGTest.107 값 없음(그대로): 
아, 세상에 대해 궁금하시군요. 제가 지금 주어진 정보가 없어서 세상에 대해 자세히 알려드릴 수는 없어요. 
제가 가진 정보가 없으니까요. 😅 혹시 다른 질문 있으시면 물어보세요!

12:14:50 [DEBUG] c.q.s.t.SimpleRAGTest.simpleRAGTest.109 값 없음(창의적): 
No context information is provided. The following is based on pre-trained knowledge.

음, 세상에 대해서 궁금하시군요! 세상은 정말 방대하고 복잡하죠. 간단하게 설명하자면, 세상은 우주라는 거대한 공간 안에 존재하며, 그 안에는 수많은 별, 행성, 그리고 그 주위를 도는 모든 것들이 있습니다. 지구는 우리가 살고 있는 행성이고, 지구에는 다양한 생물들이 살아가고 있어요. 인간은 지구상에서 특별한 존재라고 할 수 있죠.
세상은 끊임없이 변화하고 발전하고 있습니다. 자연은 자연의 법칙에 따라 움직이고, 인간은 그 안에서 끊임없이 배우고, 탐구하고, 만들어나가죠. 세상은 때로는 아름다우면서도 때로는 험난하지만, 그 안에는 무궁무진한 가능성이 숨겨져 있다고 생각해요.
좀 더 구체적으로 어떤 점이 궁금하신가요? 예를 들어, 세상의 기원, 인간의 역할, 혹은 세상의 미래에 대해 알고 싶으신가요?