🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Self-RAG 완벽 가이드 - 슬라이드 1/6
A

AI Generated

2025. 12. 25. · 3 Views

Self-RAG 완벽 가이드

RAG 시스템이 스스로 검색 필요성을 판단하고, 검색 결과의 품질을 평가하며, 자체 성찰을 통해 더 나은 답변을 생성하는 Self-RAG 기술을 초급 개발자도 쉽게 이해할 수 있도록 실무 스토리와 함께 설명합니다.


목차

  1. Retrieval 필요성 판단
  2. 일반 상식으로 답변 가능한가?
  3. 검색 결과 품질 평가
  4. 내용이 구체적이고 명확한가?
  5. Self-Reflection
  6. 답변이 명확하고 이해하기 쉬운가?
  7. 실습: Self-RAG 시스템
  8. 실습: 불필요한 검색 줄이기

1. Retrieval 필요성 판단

어느 날 김개발 씨는 회사의 챗봇 시스템을 운영하다가 이상한 현상을 발견했습니다. "오늘 날씨 어때?"라는 간단한 질문에도 데이터베이스를 뒤지고 있었습니다.

선배 박시니어 씨가 모니터링 화면을 보며 말했습니다. "불필요한 검색이 너무 많네요.

Self-RAG를 도입해 봐요."

Retrieval 필요성 판단은 RAG 시스템이 모든 질문에 대해 무조건 검색을 수행하는 것이 아니라, 질문의 특성을 분석하여 검색이 정말 필요한지 스스로 판단하는 기능입니다. 마치 도서관 사서가 질문을 듣고 "이건 책을 찾아봐야 해요" 또는 "이건 제가 바로 답해드릴 수 있어요"라고 판단하는 것과 같습니다.

이를 통해 불필요한 검색 비용을 줄이고 응답 속도를 높일 수 있습니다.

다음 코드를 살펴봅시다.

from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

# Retrieval 필요성 판단 프롬프트
retrieval_grader_prompt = PromptTemplate(
    template="""질문을 분석하여 외부 검색이 필요한지 판단하세요.

질문: {question}

다음 기준으로 판단하세요:

3. 일반 상식으로 답변 가능한가?

김개발 씨는 회사에 입사한 지 6개월 된 주니어 개발자입니다. 최근 고객 문의 챗봇 시스템을 담당하게 되었는데, 운영하면서 묘한 점을 발견했습니다.

서버 비용이 생각보다 많이 나오는 것이었습니다. 로그를 살펴보니 "안녕하세요", "감사합니다" 같은 인사말에도 매번 데이터베이스를 검색하고 있었습니다.

심지어 "1 더하기 1은?"이라는 간단한 계산 질문에도 문서 임베딩 검색을 수행했습니다. 이건 분명히 비효율적이었습니다.

박시니어 씨에게 고민을 털어놓자, 그는 화이트보드에 그림을 그리며 설명하기 시작했습니다. "RAG 시스템의 첫 번째 문제는 바로 이거예요.

모든 질문에 대해 무조건 검색부터 하니까 비용이 많이 들죠." Retrieval 필요성 판단이란 무엇일까요? 쉽게 비유하자면, 음식점에서 손님이 "화장실이 어디예요?"라고 물었을 때 주방장을 부를 필요가 없는 것과 같습니다.

홀 직원이 바로 답할 수 있는 질문이니까요. 하지만 "오늘 특선 요리의 알레르기 유발 성분이 뭐예요?"라고 물으면 주방장에게 확인해야 합니다.

이처럼 질문의 특성에 따라 검색이 필요한지 판단하는 것이 핵심입니다. 전통적인 RAG 시스템의 문제점을 살펴볼까요?

대부분의 RAG 시스템은 질문이 들어오면 무조건 다음 순서를 따릅니다. 먼저 질문을 임베딩으로 변환하고, 벡터 데이터베이스에서 유사한 문서를 검색하고, 검색 결과를 LLM에 전달하여 답변을 생성합니다.

이 과정에서 임베딩 API 비용, 벡터 검색 비용, 그리고 시간이 소요됩니다. 문제는 많은 질문들이 실제로는 검색이 필요 없다는 점입니다.

"안녕하세요"는 인사말이고, "고마워요"는 감사 표현이며, "2+2는?"은 단순 계산입니다. 이런 질문들에 매번 데이터베이스를 뒤지는 것은 마치 사전을 찾아보지 않아도 아는 단어를 매번 사전에서 찾는 것과 같습니다.

Self-RAG의 첫 번째 혁신이 바로 여기에 있습니다. 시스템이 질문을 받으면 먼저 "이 질문에 답하기 위해 검색이 필요한가?"를 스스로 판단합니다.

LLM에게 특별한 프롬프트를 주어 YES 또는 NO로 대답하게 만드는 것입니다. 이를 Retrieval Grader라고 부릅니다.

위의 코드를 자세히 살펴보겠습니다. 먼저 프롬프트 템플릿을 정의합니다.

이 프롬프트는 LLM에게 "판사" 역할을 부여합니다. 질문을 분석하여 최신 정보가 필요한지, 특정 도메인 지식이 필요한지, 아니면 일반 상식으로 답변 가능한지 판단하게 합니다.

중요한 점은 답변 형식을 YES 또는 NO로 제한한다는 것입니다. 이렇게 하면 파싱이 쉬워지고 신뢰성이 높아집니다.

needs_retrieval 함수는 이 판단 로직을 캡슐화합니다. 질문을 받아서 프롬프트를 생성하고, LLM을 호출하여 결과를 받습니다.

응답이 YES면 True를, NO면 False를 반환하여 다음 단계를 결정합니다. 실무에서는 어떻게 활용할까요?

예를 들어 전자상거래 고객센터 챗봇을 운영한다고 가정해봅시다. "배송 조회하고 싶어요"라는 질문은 고객의 주문 데이터베이스를 검색해야 하므로 YES입니다.

하지만 "결제 수단으로 뭐가 있나요?"는 이미 LLM이 학습한 일반적인 정보로 답할 수 있으므로 NO입니다. 이렇게 구분하면 검색 비용을 50% 이상 절감할 수 있습니다.

주의할 점도 있습니다. 너무 엄격하게 판단하면 검색이 필요한 경우에도 NO라고 판단할 수 있습니다.

이를 False Negative라고 하는데, 이 경우 부정확한 답변을 제공하게 됩니다. 반대로 너무 느슨하게 판단하면 불필요한 검색이 많아져 비용이 증가합니다.

따라서 프롬프트를 신중하게 설계하고, 실제 데이터로 테스트하면서 튜닝해야 합니다. 김개발 씨는 이 방법을 적용한 후 놀라운 결과를 얻었습니다.

전체 질문 중 약 40%가 검색 없이 답변 가능했고, 서버 비용이 눈에 띄게 줄어들었습니다. 더 중요한 것은 응답 속도가 빨라져 고객 만족도가 올라갔다는 점입니다.

Retrieval 필요성 판단은 Self-RAG의 출발점입니다. 똑똑한 시스템은 언제 검색해야 하고 언제 직접 답해야 하는지 알고 있습니다.

여러분의 RAG 시스템도 이런 판단 능력을 갖추면 훨씬 효율적으로 운영할 수 있을 것입니다.

실전 팁

💡 - temperature=0으로 설정하여 판단의 일관성을 높이세요

  • 프롬프트에 구체적인 판단 기준을 명시하면 정확도가 올라갑니다
  • 실제 로그 데이터로 YES/NO 비율을 모니터링하며 프롬프트를 개선하세요

2. 검색 결과 품질 평가

김개발 씨가 검색 필요성 판단 기능을 적용한 다음 날, 또 다른 문제가 발견되었습니다. 검색은 수행했는데 엉뚱한 문서를 가져와서 이상한 답변을 만들어내는 것이었습니다.

박시니어 씨가 로그를 보며 말했습니다. "검색 결과의 품질도 평가해야 해요.

쓸모없는 문서로 답변을 만들면 안 되죠."

검색 결과 품질 평가는 벡터 검색으로 가져온 문서들이 실제로 질문에 답하는 데 유용한지 판단하는 과정입니다. 마치 도서관에서 사서가 찾아준 책이 정말 내가 원하는 정보를 담고 있는지 확인하는 것과 같습니다.

유사도 점수만으로는 품질을 보장할 수 없기 때문에, LLM을 활용하여 각 문서의 관련성을 평가하고 필터링합니다.

다음 코드를 살펴봅시다.

from langchain.prompts import PromptTemplate

# 문서 관련성 평가 프롬프트
document_grader_prompt = PromptTemplate(
    template="""문서가 질문에 답하는 데 유용한지 평가하세요.

질문: {question}

문서 내용:
{document}

평가 기준:

3. 내용이 구체적이고 명확한가?

김개발 씨는 Retrieval 필요성 판단 기능을 적용한 후 뿌듯해했습니다. 불필요한 검색이 줄어들어 비용도 절감되고 속도도 빨라졌으니까요.

하지만 기쁨도 잠시, 다음 날 아침 고객 불만이 접수되었습니다. "챗봇이 엉뚱한 답변을 해요.

제품 A에 대해 물었는데 제품 B 설명을 해주네요." 로그를 확인해보니 검색은 정상적으로 수행되었지만, 가져온 문서들이 질문과 동떨어진 내용이었습니다. 벡터 유사도는 높았지만 실제 내용은 무관했던 것입니다.

박시니어 씨가 화이트보드 앞으로 다시 김개발 씨를 불렀습니다. "벡터 검색의 한계예요.

단어가 비슷하면 유사도가 높게 나오지만, 실제 의미는 다를 수 있죠. 예를 들어 '사과'라는 단어가 과일일 수도 있고 사죄일 수도 있잖아요." 검색 결과 품질 평가가 왜 필요할까요?

벡터 검색은 강력한 도구이지만 완벽하지 않습니다. 코사인 유사도 점수가 0.85라고 해서 그 문서가 정말 유용하다는 보장은 없습니다.

마치 검색 엔진에서 상위에 노출된 페이지가 항상 내가 원하는 정보를 담고 있지 않은 것과 같습니다. 실제 문제 상황을 하나 살펴볼까요?

사용자가 "2024년 신제품 스마트폰 가격은?"이라고 질문했습니다. 벡터 검색은 "스마트폰", "가격"이라는 키워드가 들어간 문서들을 찾아왔습니다.

그런데 문서 중 하나는 2022년 구형 모델 가격표였고, 다른 하나는 스마트폰 수리 비용 안내였습니다. 단어는 비슷하지만 질문에 대한 답은 아니었던 것입니다.

Document Grader가 이 문제를 해결합니다. 검색으로 가져온 각 문서를 LLM에게 보여주고 "이 문서가 질문에 답하는 데 실제로 도움이 되는가?"를 판단하게 합니다.

LLM은 단순히 단어 유사도가 아니라 의미를 이해하기 때문에 훨씬 정확한 판단을 내릴 수 있습니다. 위의 코드를 단계별로 분석해봅시다.

먼저 문서 평가용 프롬프트를 정의합니다. 이 프롬프트는 LLM에게 "심사위원" 역할을 부여합니다.

질문과 문서를 모두 보여주고, 세 가지 기준으로 평가하게 합니다. 직접적 관련성, 필요한 정보 포함 여부, 내용의 구체성입니다.

답변은 RELEVANT 또는 NOT_RELEVANT로 명확하게 구분합니다. grade_documents 함수는 이 평가 로직을 반복 실행합니다.

검색된 모든 문서를 순회하면서 각각을 평가하고, RELEVANT라고 판단된 것만 새 리스트에 담습니다. 또한 어떤 문서가 선택되고 제외되었는지 로그를 출력하여 디버깅을 쉽게 만듭니다.

실무에서는 이렇게 활용합니다. 먼저 벡터 검색으로 상위 5~10개 문서를 가져옵니다.

이것을 Recall 단계라고 합니다. 그다음 Document Grader로 필터링하는데, 이것이 Precision 단계입니다.

최종적으로 2~3개의 고품질 문서만 LLM에 전달하여 답변을 생성합니다. 이렇게 하면 답변의 정확도가 크게 향상됩니다.

실제 효과는 어땠을까요? 김개발 씨가 이 기능을 적용한 후 측정해보니 놀라운 결과가 나왔습니다.

검색된 문서 중 약 30~40%가 실제로는 무관한 내용이었습니다. 이것들을 제외하자 답변의 정확도가 65%에서 87%로 상승했습니다.

고객 불만도 현저히 줄어들었습니다. 주의할 점이 있습니다.

모든 문서가 NOT_RELEVANT로 판단되는 경우가 있습니다. 이때는 검색 자체가 실패한 것이므로, "관련 정보를 찾을 수 없습니다"라고 솔직하게 답하거나 다른 검색 전략을 시도해야 합니다.

절대로 무관한 문서를 억지로 사용해서는 안 됩니다. 또 하나 고려할 점은 비용입니다.

문서 하나당 LLM을 한 번씩 호출하므로, 검색된 문서가 많으면 비용이 증가합니다. 따라서 벡터 검색에서 k=3~5 정도의 적절한 개수만 가져오는 것이 좋습니다.

박시니어 씨가 김개발 씨에게 말했습니다. "좋은 RAG 시스템의 비결은 많은 문서를 사용하는 게 아니라 좋은 문서를 사용하는 거예요.

품질 평가는 그래서 중요합니다." 검색 결과 품질 평가는 Self-RAG의 두 번째 핵심입니다. 양보다 질을 추구하는 시스템이 더 신뢰받습니다.

여러분도 이 기법을 적용하여 RAG 시스템의 정확도를 높여보세요.

실전 팁

💡 - 검색 결과는 3~5개로 제한하여 평가 비용을 줄이세요

  • 평가 프롬프트에 도메인 특화 기준을 추가하면 정확도가 향상됩니다
  • 모든 문서가 무관하다면 "정보를 찾을 수 없습니다"라고 솔직하게 답변하세요

3. Self-Reflection

김개발 씨의 챗봇은 이제 불필요한 검색도 줄이고 좋은 문서만 골라내게 되었습니다. 하지만 여전히 문제가 있었습니다.

가끔 생성된 답변이 문서 내용과 어긋나거나 환각을 일으키는 것이었습니다. 박시니어 씨가 말했습니다.

"이제 마지막 단계예요. 생성한 답변을 스스로 검증하는 Self-Reflection이 필요합니다."

Self-Reflection은 LLM이 생성한 답변을 스스로 평가하여 문서와의 일치성, 질문과의 관련성, 환각 여부를 검증하는 과정입니다. 마치 작가가 글을 쓴 후 다시 읽어보며 오류를 확인하는 것과 같습니다.

이를 통해 답변의 신뢰성을 높이고, 문제가 있으면 재생성하거나 경고를 표시할 수 있습니다.

다음 코드를 살펴봅시다.

from langchain.prompts import PromptTemplate

# 답변 검증 프롬프트
answer_grader_prompt = PromptTemplate(
    template="""생성된 답변을 평가하세요.

질문: {question}
제공된 문서: {documents}
생성된 답변: {answer}

다음 기준으로 평가하세요:

3. 답변이 명확하고 이해하기 쉬운가?

김개발 씨의 챗봇 시스템은 이제 꽤 똑똑해졌습니다. 불필요한 검색은 하지 않고, 검색을 하더라도 좋은 문서만 골라냅니다.

그런데 어느 날 품질 관리팀에서 연락이 왔습니다. "챗봇이 가끔 문서에 없는 내용을 지어내는 것 같아요." 김개발 씨가 로그를 확인해보니 정말 그랬습니다.

문서에는 "제품 가격은 50만 원입니다"라고 되어 있는데, 챗봇은 "제품 가격은 50만 원이며 할인 시 40만 원입니다"라고 답변했습니다. 할인 정보는 문서 어디에도 없었습니다.

전형적인 환각(Hallucination) 현상이었습니다. 박시니어 씨가 설명했습니다.

"LLM은 아무리 좋은 문서를 줘도 가끔 창작을 해요. 학습 데이터에 있는 패턴을 무의식적으로 따라 하거든요.

그래서 생성한 답변을 다시 검증하는 단계가 필요합니다." Self-Reflection이란 정확히 무엇일까요? 이름 그대로 스스로를 되돌아본다는 뜻입니다.

사람으로 치면 글을 쓴 후 다시 읽어보는 과정입니다. "내가 쓴 이 문장이 정말 맞나?

출처가 확실한가? 질문에 제대로 답했나?" 하고 자문하는 것이죠.

Self-RAG에서는 LLM이 이 역할을 담당합니다. 전통적인 RAG는 어땠을까요?

대부분의 시스템은 답변을 생성하면 그것으로 끝입니다. 사용자에게 바로 전달하죠.

답변이 정확한지, 문서에 근거한 내용인지 검증하지 않습니다. 마치 교정 없이 출판하는 것과 같습니다.

당연히 오류가 발생할 확률이 높습니다. Self-Reflection은 3단계로 검증합니다.

첫 번째는 근거성(Grounding) 평가입니다. 답변의 내용이 제공된 문서에 실제로 있는 내용인지 확인합니다.

만약 문서에 없는 내용이 포함되어 있다면 환각으로 판단합니다. 두 번째는 관련성(Relevance) 평가입니다.

답변이 질문에 직접 대답하는지 확인합니다. 문서 내용은 맞지만 질문과 무관한 답변도 문제니까요.

세 번째는 명확성(Clarity) 평가입니다. 답변이 이해하기 쉽고 명확한지 확인합니다.

코드를 자세히 살펴보겠습니다. 프롬프트는 LLM에게 "검수자" 역할을 부여합니다.

질문, 문서, 그리고 생성된 답변을 모두 제공하고 세 가지 기준으로 평가하게 합니다. 중요한 점은 각 항목을 개별적으로 PASS/FAIL로 판단한 후, 종합 결과를 APPROVED/REJECTED로 내린다는 것입니다.

reflect_on_answer 함수는 이 검증 과정을 자동화합니다. 모든 문서를 하나의 텍스트로 합치고, 프롬프트를 생성하여 LLM을 호출합니다.

응답을 파싱하여 각 평가 항목과 최종 승인 여부를 딕셔너리로 반환합니다. 실무에서는 이렇게 활용합니다.

답변을 생성한 후 즉시 reflect_on_answer를 호출합니다. 결과가 APPROVED면 사용자에게 전달하고, REJECTED면 재생성을 시도하거나 "확실한 답변을 드리기 어렵습니다"라고 솔직하게 답변합니다.

일부 시스템은 승인되지 않은 답변에 경고 메시지를 붙이기도 합니다. 김개발 씨가 이 기능을 적용한 결과는 놀라웠습니다.

환각 발생률이 15%에서 3%로 크게 감소했습니다. 더 중요한 것은 시스템이 자신의 한계를 인식하게 되었다는 점입니다.

확실하지 않은 답변은 "정보가 부족합니다"라고 솔직하게 말하게 되었고, 이것이 오히려 사용자 신뢰를 높였습니다. 한 가지 재미있는 발견도 있었습니다.

Self-Reflection을 적용하자 모델이 더 보수적으로 답변하게 되었습니다. 문서에 명확히 나와 있는 내용만 이야기하고, 추측이나 일반화를 줄였습니다.

창의성은 조금 떨어졌지만 정확성은 크게 향상되었습니다. 주의할 점도 있습니다.

Self-Reflection은 추가적인 LLM 호출이 필요하므로 비용과 시간이 증가합니다. 따라서 중요한 업무(의료, 법률, 금융 등)에서는 필수이지만, 캐주얼한 대화 챗봇에서는 선택적으로 적용할 수 있습니다.

또한 LLM이 자신의 오류를 항상 정확하게 찾는 것은 아니므로, 이중 검증이나 사람의 최종 확인이 필요한 경우도 있습니다. 박시니어 씨가 김개발 씨에게 말했습니다.

"Self-Reflection은 시스템에 겸손함을 가르치는 거예요. '나도 틀릴 수 있어'라고 인정하는 AI가 더 신뢰받습니다." Self-Reflection은 Self-RAG의 마지막 안전장치입니다.

아무리 좋은 문서를 찾아도 답변 생성에서 실수할 수 있으니, 마지막에 한 번 더 검증하는 것입니다. 여러분의 시스템도 이런 자기 점검 능력을 갖추면 훨씬 신뢰할 수 있게 됩니다.

실전 팁

💡 - 비용이 부담된다면 중요한 쿼리에만 선택적으로 적용하세요

  • REJECTED된 답변은 로그에 남겨 시스템 개선에 활용하세요
  • 각 평가 항목별 점수를 모니터링하여 어떤 부분이 자주 실패하는지 파악하세요

4. 실습: Self-RAG 시스템

이론은 충분히 배웠으니 이제 실제로 Self-RAG 시스템을 구축할 차례입니다. 김개발 씨는 지금까지 배운 세 가지 핵심 기능을 하나로 통합하여 완전한 Self-RAG 파이프라인을 만들기로 했습니다.

박시니어 씨가 격려했습니다. "하나씩 차근차근 연결하면 됩니다.

어렵지 않아요."

Self-RAG 시스템은 검색 필요성 판단, 문서 품질 평가, 답변 검증을 하나의 파이프라인으로 통합한 완전한 RAG 시스템입니다. 마치 공장의 생산 라인처럼 각 단계가 순차적으로 실행되며, 각 단계에서 품질 관리가 이루어집니다.

이를 통해 높은 정확도와 신뢰성을 가진 답변 시스템을 구축할 수 있습니다.

다음 코드를 살펴봅시다.

from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.chat_models import ChatOpenAI

class SelfRAG:
    def __init__(self, vector_store):
        self.vector_store = vector_store
        self.llm = ChatOpenAI(temperature=0)

    def query(self, question: str) -> dict:
        # 1단계: 검색 필요성 판단
        print("1단계: 검색 필요성 판단")
        if not needs_retrieval(question):
            print("→ 검색 불필요, 직접 답변")
            answer = self.llm.predict(question)
            return {'answer': answer, 'source': 'direct'}

        # 2단계: 문서 검색 및 품질 평가
        print("2단계: 문서 검색 및 평가")
        docs = self.vector_store.similarity_search(question, k=5)
        relevant_docs = grade_documents(question, docs)

        if not relevant_docs:
            print("→ 관련 문서 없음")
            return {'answer': '관련 정보를 찾을 수 없습니다.', 'source': 'none'}

        # 3단계: 답변 생성
        print("3단계: 답변 생성")
        context = "\n\n".join([d.page_content for d in relevant_docs])
        prompt = f"다음 문서를 바탕으로 질문에 답하세요.\n\n문서:\n{context}\n\n질문: {question}"
        answer = self.llm.predict(prompt)

        # 4단계: Self-Reflection
        print("4단계: 답변 검증")
        validation = reflect_on_answer(question, relevant_docs, answer)

        if validation['approved']:
            print("→ 답변 승인")
            return {'answer': answer, 'source': 'rag', 'validation': validation}
        else:
            print("→ 답변 거부, 재시도")
            return {'answer': '확실한 답변을 제공하기 어렵습니다.',
                    'source': 'rejected', 'validation': validation}

# 사용 예시
rag_system = SelfRAG(vector_store)
result = rag_system.query("Self-RAG가 뭐예요?")
print(f"\n최종 답변: {result['answer']}")

김개발 씨는 지금까지 배운 내용을 정리하며 노트북을 펼쳤습니다. Retrieval 필요성 판단, 문서 품질 평가, Self-Reflection.

세 가지 핵심 기능이 있었습니다. 이제 이것들을 하나로 연결하여 완전한 시스템을 만들 차례입니다.

박시니어 씨가 아키텍처 다이어그램을 그려주었습니다. "파이프라인이라고 생각하세요.

물이 흐르듯이 질문이 각 단계를 거치면서 정제되는 겁니다." Self-RAG 시스템의 전체 흐름을 살펴봅시다. 첫 번째 단계는 질문을 받아서 검색이 필요한지 판단합니다.

여기서 NO가 나오면 바로 LLM이 직접 답변하고 끝납니다. 파이프라인을 조기 종료하는 것이죠.

이것만으로도 30~40%의 쿼리를 빠르게 처리할 수 있습니다. 두 번째 단계는 검색이 필요하다고 판단된 경우에만 실행됩니다.

벡터 검색으로 상위 5개 문서를 가져온 후, Document Grader로 각각을 평가합니다. 여기서 걸러지지 않은 문서들만 다음 단계로 전달됩니다.

만약 모든 문서가 무관하다면 "정보를 찾을 수 없습니다"로 종료합니다. 세 번째 단계는 선별된 문서들을 컨텍스트로 사용하여 답변을 생성합니다.

문서 내용을 하나의 텍스트로 합치고, "다음 문서를 바탕으로 답하세요"라는 프롬프트와 함께 LLM에 전달합니다. 이때 생성된 답변은 아직 최종 답변이 아닙니다.

네 번째 단계는 생성된 답변을 Self-Reflection으로 검증합니다. 근거성, 관련성, 명확성을 평가하여 APPROVED 또는 REJECTED를 결정합니다.

APPROVED면 사용자에게 전달하고, REJECTED면 솔직하게 "확실한 답변을 제공하기 어렵습니다"라고 답변합니다. 코드의 핵심 구조를 분석해봅시다.

SelfRAG 클래스는 전체 파이프라인을 캡슐화합니다. 초기화할 때 벡터 스토어와 LLM을 설정하고, query 메서드 하나로 모든 단계를 순차 실행합니다.

이렇게 클래스로 만들면 재사용성이 높아지고 코드 관리가 쉬워집니다. 각 단계마다 print 문으로 진행 상황을 출력합니다.

실제 서비스에서는 이것을 로깅으로 대체하여 모니터링과 디버깅에 활용할 수 있습니다. 어느 단계에서 시간이 많이 걸리는지, 어느 단계에서 자주 실패하는지 파악할 수 있습니다.

반환값은 딕셔너리 형태로 통일했습니다. 답변뿐 아니라 출처(source)와 검증 결과(validation)도 함께 반환하여, 호출하는 쪽에서 추가 처리를 할 수 있게 했습니다.

예를 들어 UI에서 "이 답변은 문서 기반입니다" 같은 메시지를 표시할 수 있습니다. 실무에서 이 시스템을 어떻게 확장할 수 있을까요?

먼저 재시도 로직을 추가할 수 있습니다. Self-Reflection에서 REJECTED가 나오면 바로 포기하지 않고, 다른 검색 키워드로 재시도하거나 더 많은 문서를 가져와볼 수 있습니다.

두 번째로 하이브리드 검색을 적용할 수 있습니다. 벡터 검색과 키워드 검색을 병행하여 더 다양한 문서를 찾을 수 있습니다.

또한 캐싱도 중요합니다. 같은 질문이 반복되면 매번 전체 파이프라인을 실행하지 않고 캐시된 답변을 반환합니다.

특히 FAQ 같은 경우 캐싱 효과가 큽니다. 그리고 비동기 처리를 적용하면 여러 질문을 동시에 처리할 수 있어 처리량이 증가합니다.

김개발 씨가 이 시스템을 프로덕션에 배포한 후 모니터링을 시작했습니다. 전체 질문의 35%는 1단계에서 조기 종료되었습니다.

검색이 필요한 65% 중에서 2단계에서 약 20%가 "관련 문서 없음"으로 처리되었습니다. 나머지 45%는 답변을 생성했고, 그중 92%가 Self-Reflection을 통과했습니다.

최종적으로 전체 질문의 약 41%가 성공적인 RAG 답변을, 35%가 직접 답변을, 나머지 24%가 "답변 불가"로 처리되었습니다. "답변 불가"가 24%나 되는 게 문제 아니냐고 물을 수 있습니다.

하지만 박시니어 씨는 오히려 긍정적으로 평가했습니다. "틀린 답변을 자신 있게 말하는 것보다, 모른다고 솔직하게 말하는 게 훨씬 낫습니다.

신뢰가 쌓이죠." 실제로 사용자 피드백도 그랬습니다. "이전에는 이상한 답변이 많았는데 이제는 확실한 것만 알려줘서 좋아요"라는 의견이 많았습니다.

정확도가 신뢰를 만들고, 신뢰가 사용자 만족도를 높인다는 것을 확인했습니다. 주의할 점은 파이프라인이 길어질수록 전체 응답 시간이 증가한다는 것입니다.

각 단계에서 LLM을 호출하므로 총 3~4번의 API 콜이 발생할 수 있습니다. 따라서 각 단계의 프롬프트를 최적화하여 토큰 수를 줄이고, 가능한 경우 더 작고 빠른 모델을 사용하는 것이 좋습니다.

김개발 씨는 이제 자신감을 얻었습니다. Self-RAG의 각 구성 요소를 이해했고, 이것들을 통합하여 실제 작동하는 시스템을 만들었습니다.

다음 단계는 성능 최적화입니다. Self-RAG 시스템은 단순히 기능을 모아놓은 것이 아니라, 각 단계가 서로 협력하는 지능형 파이프라인입니다.

품질 관리가 내재화된 시스템이죠. 여러분도 이 구조를 기반으로 자신만의 RAG 시스템을 구축해보세요.

실전 팁

💡 - 각 단계의 실행 시간을 측정하여 병목 지점을 파악하세요

  • 로그를 잘 남겨서 어느 단계에서 질문이 걸러지는지 모니터링하세요
  • 캐싱과 비동기 처리로 성능을 개선할 수 있습니다

5. 실습: 불필요한 검색 줄이기

Self-RAG 시스템이 안정적으로 운영되던 어느 날, 김개발 씨는 월말 비용 리포트를 받았습니다. 생각보다 API 비용이 높았습니다.

분석해보니 Retrieval 필요성 판단 단계에서 미세하게 조정하면 더 많은 검색을 줄일 수 있다는 것을 발견했습니다. 박시니어 씨가 조언했습니다.

"프롬프트 엔지니어링과 캐싱으로 비용을 크게 줄일 수 있어요."

불필요한 검색 줄이기는 Self-RAG 시스템에서 검색 빈도를 최적화하여 비용과 응답 시간을 줄이는 고급 기법입니다. 프롬프트 개선, 패턴 기반 필터링, 쿼리 캐싱 등을 활용하여 검색이 정말 필요한 경우에만 수행하도록 최적화합니다.

마치 교통 신호 시스템처럼 불필요한 정차를 줄여 전체 흐름을 개선하는 것입니다.

다음 코드를 살펴봅시다.

import hashlib
from functools import lru_cache
from typing import Optional

class OptimizedSelfRAG:
    def __init__(self, vector_store):
        self.vector_store = vector_store
        self.llm = ChatOpenAI(temperature=0)
        self.query_cache = {}

        # 검색 불필요 패턴 정의
        self.no_retrieval_patterns = [
            r'^안녕',
            r'^감사',
            r'^고마워',
            r'^\d+\s*[\+\-\*\/]\s*\d+',  # 간단한 계산
        ]

    def quick_filter(self, question: str) -> bool:
        """패턴 기반 빠른 필터링"""
        import re
        for pattern in self.no_retrieval_patterns:
            if re.match(pattern, question.strip()):
                print(f"→ 패턴 매칭: 검색 불필요")
                return False
        return True

    def get_cache_key(self, question: str) -> str:
        """질문을 캐시 키로 변환"""
        normalized = question.strip().lower()
        return hashlib.md5(normalized.encode()).hexdigest()

    def query_with_cache(self, question: str) -> dict:
        # 캐시 확인
        cache_key = self.get_cache_key(question)
        if cache_key in self.query_cache:
            print("→ 캐시 히트!")
            return self.query_cache[cache_key]

        # 빠른 패턴 필터링
        if not self.quick_filter(question):
            answer = self.llm.predict(question)
            result = {'answer': answer, 'source': 'pattern_filtered'}
            self.query_cache[cache_key] = result
            return result

        # LLM 기반 정교한 판단
        if not needs_retrieval(question):
            answer = self.llm.predict(question)
            result = {'answer': answer, 'source': 'direct'}
            self.query_cache[cache_key] = result
            return result

        # 검색 수행 (캐싱은 선택적)
        docs = self.vector_store.similarity_search(question, k=3)
        relevant_docs = grade_documents(question, docs)

        # ... 나머지 RAG 로직

        return result

# 사용 예시
optimized_rag = OptimizedSelfRAG(vector_store)

# 같은 질문 반복
for i in range(3):
    result = optimized_rag.query_with_cache("안녕하세요")
    print(f"시도 {i+1}: {result['source']}")

김개발 씨는 Self-RAG 시스템이 잘 작동한다는 사실에 만족했습니다. 하지만 월말에 받은 AWS 청구서를 보고 놀랐습니다.

OpenAI API 비용이 예상보다 30% 높았던 것입니다. 무엇이 문제일까요?

로그를 분석해보니 흥미로운 패턴이 발견되었습니다. "안녕하세요"라는 인사말이 하루에 수백 번 들어왔고, 매번 Retrieval 필요성 판단을 위해 LLM을 호출하고 있었습니다.

또한 "1+1은?"같은 단순 계산 질문도 반복적으로 LLM 판단을 거치고 있었습니다. 박시니어 씨가 말했습니다.

"모든 질문을 LLM에게 물어볼 필요는 없어요. 명백한 패턴은 정규식으로 걸러낼 수 있습니다.

그리고 같은 질문은 캐싱하세요." 불필요한 검색을 줄이는 전략은 크게 세 가지입니다. 첫 번째는 패턴 기반 필터링입니다.

정규식이나 간단한 규칙으로 명백하게 검색이 불필요한 질문을 조기에 걸러냅니다. 인사말, 감사 표현, 단순 계산 등이 여기에 해당합니다.

이런 질문들은 LLM 판단 없이 바로 직접 답변 모드로 보낼 수 있습니다. 두 번째는 쿼리 캐싱입니다.

같은 질문이나 유사한 질문이 반복되면 이전 결과를 재사용합니다. 특히 FAQ 성격의 질문은 캐싱 효과가 큽니다.

"영업시간이 어떻게 되나요?" 같은 질문은 하루에 수십 번 들어올 수 있는데, 첫 번째만 처리하고 나머지는 캐시에서 가져오면 됩니다. 세 번째는 계층적 판단입니다.

빠르고 저렴한 필터를 먼저 적용하고, 그것을 통과한 질문만 정교한 LLM 판단을 수행합니다. 마치 공항 보안검색처럼 1차 간단한 검사를 통과한 사람만 2차 정밀 검사를 받는 것과 같습니다.

코드를 단계별로 살펴봅시다. quick_filter 메서드는 정규식 패턴을 사용하여 빠른 1차 필터링을 수행합니다.

패턴 리스트를 순회하며 질문이 매칭되는지 확인합니다. 매칭되면 False를 반환하여 "검색 불필요"를 알립니다.

이 과정은 밀리초 단위로 완료되므로 API 호출보다 수백 배 빠릅니다. get_cache_key 메서드는 질문을 정규화하여 캐시 키를 생성합니다.

대소문자를 통일하고 공백을 제거한 후 MD5 해시로 변환합니다. 이렇게 하면 "안녕하세요"와 "안녕하세요 "를 같은 질문으로 인식합니다.

query_with_cache 메서드는 전체 최적화 로직을 통합합니다. 먼저 캐시를 확인하고, 없으면 패턴 필터링을 수행하고, 그것도 통과하면 LLM 판단을 요청합니다.

각 단계는 이전 단계보다 비용이 높지만 정확도도 높습니다. 실제 효과를 측정해봅시다.

김개발 씨가 이 최적화를 적용한 후 1주일간 모니터링한 결과, 전체 질문의 25%가 패턴 필터링으로 처리되었습니다. 또한 캐시 히트율이 15%였습니다.

즉, 총 40%의 질문이 LLM 판단 없이 처리된 것입니다. API 호출 횟수가 40% 감소했고, 평균 응답 시간도 200ms에서 120ms로 줄어들었습니다.

비용 절감 효과도 명확했습니다. 월 API 비용이 $300에서 $180으로 40% 감소했습니다.

이 절감된 비용으로 더 좋은 모델을 사용하거나 더 많은 문서를 처리할 수 있게 되었습니다. 패턴 리스트는 어떻게 관리할까요?

초기에는 수동으로 명백한 패턴들을 추가합니다. 그런 다음 실제 로그 데이터를 분석하여 자주 반복되는 질문 패턴을 찾아냅니다.

예를 들어 "고마워", "감사해", "땡큐" 같은 변형들을 발견하면 정규식으로 일반화하여 추가합니다. 또한 잘못된 필터링(검색이 필요한데 걸러진 경우)을 모니터링하여 패턴을 조정합니다.

캐시의 유효 기간은 어떻게 설정할까요? 정적인 정보(회사 소개, 제품 스펙 등)는 캐시를 길게 유지할 수 있습니다.

하지만 동적인 정보(재고 상황, 날씨 등)는 캐시를 짧게 하거나 아예 캐싱하지 않아야 합니다. 따라서 질문의 특성에 따라 TTL(Time To Live)을 차등 적용하는 것이 좋습니다.

주의할 점도 있습니다. 패턴 필터링이 너무 공격적이면 검색이 필요한 질문까지 걸러낼 수 있습니다.

예를 들어 "안녕하세요, 제품 A 가격이 궁금합니다"라는 질문이 "안녕" 패턴에 걸려 인사말로 처리되면 안 됩니다. 따라서 패턴은 전체 질문이 그 패턴과 정확히 일치하는 경우에만 적용해야 합니다.

또한 캐시 메모리 사용량도 고려해야 합니다. 무한정 캐싱하면 메모리가 부족해질 수 있으므로, LRU(Least Recently Used) 캐시나 크기 제한을 두어야 합니다.

Python의 functools.lru_cache나 Redis 같은 외부 캐시 시스템을 활용할 수 있습니다. 김개발 씨는 이 최적화를 통해 중요한 교훈을 얻었습니다.

"항상 최고의 기술을 쓸 필요는 없다. 간단한 규칙으로 해결되는 것은 간단하게 처리하고, 복잡한 것만 고급 기술을 사용하면 된다." 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 엔지니어링의 핵심은 적재적소에 적절한 도구를 사용하는 거예요.

Self-RAG는 강력하지만 모든 질문에 풀 파이프라인을 적용할 필요는 없습니다." 불필요한 검색 줄이기는 Self-RAG의 성능 최적화 핵심입니다. 품질을 유지하면서 비용과 시간을 줄이는 지혜입니다.

여러분도 실제 데이터를 분석하여 자신만의 최적화 전략을 만들어보세요.

실전 팁

💡 - 실제 로그 데이터를 분석하여 반복 패턴을 찾아내세요

  • 캐시 히트율과 패턴 필터링 비율을 모니터링하세요
  • 패턴이 너무 광범위하지 않도록 주의하세요 (False Positive 방지)

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#RAG#SelfRAG#Retrieval#LangChain#RAG,SelfRAG,고급

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.