RAG는 Retrieval-Augmented Generation의 줄임말이다. 말 그대로 검색으로 가져온 정보를 언어 모델의 답변 생성에 함께 쓰는 방식이다.

언어 모델은 학습된 지식을 바탕으로 답하지만, 그 지식은 오래됐을 수 있고 특정 회사의 문서나 서비스 데이터처럼 학습에 포함되지 않은 정보는 모를 수 있다. RAG는 이 문제를 해결하기 위해 질문과 관련된 문서를 먼저 찾고, 그 내용을 프롬프트에 넣어 모델이 답하게 만든다.

기본 흐름

간단히 보면 흐름은 이렇다.

  1. 사용자가 질문한다.
  2. 질문을 임베딩으로 바꾼다.
  3. 벡터 DB에서 질문과 가까운 문서 조각을 찾는다.
  4. 찾은 문서 조각을 프롬프트에 붙인다.
  5. 언어 모델이 그 내용을 근거로 답한다.
sequenceDiagram
  participant 사용자
  participant 애플리케이션
  participant 임베딩모델
  participant 벡터DB
  participant 언어모델

  사용자->>애플리케이션: 질문 입력
  애플리케이션->>임베딩모델: 질문 임베딩 생성
  임베딩모델->>벡터DB: 유사한 문서 검색
  벡터DB-->>애플리케이션: 관련 문서 조각 반환
  애플리케이션->>언어모델: 질문 + 문서 조각 전달
  언어모델-->>사용자: 근거를 반영한 답변 반환

RAG를 프롬프트 엔지니어링의 한 종류처럼 볼 수도 있지만, 실제로는 프롬프트를 매번 수동으로 잘 쓰는 문제가 아니라 검색 파이프라인을 설계하는 문제에 더 가깝다. 어떤 문서를 넣을지, 문서를 얼마나 잘게 나눌지, 검색 결과를 몇 개까지 넣을지, 오래된 문서와 최신 문서를 어떻게 구분할지 같은 선택이 답변 품질에 큰 영향을 준다.

문서를 준비하고 검색하는 과정

문서 준비 단계에서는 원본 문서를 적당한 크기로 나누고, 각 조각을 임베딩해서 벡터 DB에 저장한다. 질문이 들어오면 질문도 같은 방식으로 임베딩한 뒤, 벡터 DB에서 가까운 조각을 찾는다. 이후 애플리케이션은 검색된 조각을 언어 모델이 읽기 좋은 형태로 정리해서 프롬프트에 넣는다.

stateDiagram
  [*] --> LoadDocs: 문서 로딩
  LoadDocs --> SplitDocs: 문서 분할
  SplitDocs --> EmbedChunks: 임베딩 생성
  EmbedChunks --> StoreToVectorDB: 벡터DB 저장

  [*] --> AskQuestion: 질문 입력
  AskQuestion --> EmbedQuery: 쿼리 임베딩
  EmbedQuery --> SearchDB: 유사 문서 검색
  SearchDB --> BuildPrompt: 질문과 문서 병합
  BuildPrompt --> [*]

이커머스 예시

예를 들어 이커머스 상품 검색 도우미를 만든다고 해보자. 사용자가 “15만 원 이하 친환경 소재 러닝화를 추천해줘”라고 물으면, 시스템은 상품명, 가격, 카테고리, 리뷰, 소재 설명이 들어 있는 문서 조각을 먼저 찾는다. 그다음 언어 모델은 검색된 상품 정보를 바탕으로 조건에 맞는 후보를 정리하고, 왜 추천하는지 자연어로 설명한다.

이때 중요한 것은 모델이 모든 상품 정보를 외우고 있을 필요가 없다는 점이다. 상품 카탈로그는 계속 바뀌고, 재고나 가격도 변한다. 이런 데이터는 모델을 다시 학습시키기보다 검색 가능한 형태로 관리하고, 답변 시점에 필요한 정보만 가져오는 편이 현실적이다.

작은 POC로 시작하기

구현할 때는 보통 아래 구성으로 시작할 수 있다.

  • 벡터 DB: Chroma, Faiss, Weaviate, Pinecone
  • 임베딩 모델: OpenAI embedding API, Snowflake/arctic-embed, nomic-embed-text
  • 언어 모델: GPT 계열 API, Claude, 로컬 LLM
  • 애플리케이션 서버: FastAPI, Spring, Node.js 등

아주 작은 POC라면 로컬 Chroma나 Faiss에 문서를 넣고, 임베딩과 LLM은 API를 호출하는 방식이 가장 단순하다. 이 방식은 운영 비용은 들지만, 로컬에서 큰 모델을 돌리지 않아도 되기 때문에 빠르게 검증하기 좋다. 반대로 모든 것을 로컬에서 처리하려면 임베딩 모델과 LLM을 직접 띄워야 해서 메모리와 응답 시간이 부담이 될 수 있다.

간단한 Weaviate 예시는 다음과 같다.

import os
import weaviate
from weaviate.auth import Auth
from weaviate.agents.query import QueryAgent
 
client = weaviate.connect_to_weaviate_cloud(
    cluster_url=os.environ["WEAVIATE_URL"],
    auth_credentials=Auth.api_key(os.environ["WEAVIATE_API_KEY"]),
)
 
agent = QueryAgent(client=client, collections=["ECommerce"])
answer = agent.run("I like vintage clothes, can you list me some options under $200?").final_answer
print(answer)

한계부터 확인하기

RAG가 만능은 아니다. 검색이 잘못되면 모델도 엉뚱한 근거를 보고 답한다. 문서가 너무 잘게 쪼개져도 맥락이 부족해지고, 너무 크게 들어가도 중요한 정보가 묻힌다. 결국 RAG의 품질은 “좋은 모델을 붙였는가”보다 “필요한 정보를 얼마나 정확히 찾아서 넣었는가”에 많이 좌우된다.

그래서 RAG를 설계할 때는 처음부터 거창하게 만들기보다, 작은 문서 집합으로 검색 품질을 먼저 확인하는 편이 좋다. 질문을 몇 개 정해두고 어떤 문서가 검색되는지, 답변이 실제 문서에 근거하는지, 모르는 내용은 모른다고 말하는지부터 보는 것이 출발점이다.