RAG(Retrieval-Augmented Generation, “검색 증강 생성”)은 외부 지식의 검색을 통해 대형 언어 모델(LLM)의 응답 생성을 향상시키는 기술이다.
LLM 단독으로는 훈련 데이터에 없는 최신 정보나 세부사항을 모를 수 있고, 존재하지 않는 답변을 그럴듯하게 만들어내는 환각(hallucination) 문제가 있다.
RAG는 이러한 한계를 해결하기 위해 신뢰할 수 있는 데이터 소스의 정보를 미리 찾아서 LLM에 제공함으로써, 응답의 정확성과 신뢰성을 높이는 역할을 수행한다

RAG 파이프라인 개념도
문서를 사전 처리하여 임베딩(벡터 표현)을 생성하고 벡터 DB에 저장해 둔 뒤, 사용자 질문(query)에 따라 관련 임베딩을 검색한다. 검색된 문서 내용들을 LLM 프롬프트에 추가(증강)하여, 생성 모델이 최종 답변을 생성한다.
RAG는 응답 생성 전 문서 검색 단계를 거치므로, 실시간 데이터 활용과 환각 감소, 출처 제시 등의 장점이 있다.
RAG란 무엇이며 왜 필요한가?
RAG는 LLM 기반 응용에서 지식 한계를 보완하기 위한 일반적인 설계 패턴이다.
기본적으로 LLM은 훈련 시점까지의 내재된 지식에만 의존하기 때문에, 최신 사건이나 사내 전용 자료 같은 외부 정보는 알 수 없다.
또한 질문에 답이 없거나 모르는 경우에도 LLM은 그럴듯한 문장을 만들어 내기 쉽다.
이런 문제를 해결하고자, RAG 시스템은 사용자 질문에 답변을 생성하기 전에 관련 정보 조각들을 검색(Retrieve)하여 가져오게 된다.
검색된 맥락(context)을 LLM에 제공하면, 모델은 자신의 지식과 함께 이 추가 정보를 활용하여 더 정확하고 구체적인 답변을 생성하게 된다.
도입 시 장점
- 최신 정보 반영: 사전에 학습되지 않은 최신 뉴스나 업데이트된 데이터를 포함시켜 업데이트된 답변을 제공한다.
- 도메인 특화 지식 활용: 기업 내부 문서, 정책, 매뉴얼 등 특정 분야의 자료를 활용하여 해당 분야에 특화된 정확한 답변을 생성한다.
- 응답 신뢰도 증대: 모델이 제공하는 답변에 출처(citations)를 명시할 수 있어 사용자 신뢰를 높인다. 사용자는 모델의 답변 근거를 직접 확인할 수 있고, 민감한 경우엔 내부 데이터만 사용하도록 구성해 프라이버시도 지킬 수 있다.
- 환각 감소: LLM이 잘 모르는 질문에 대해 지어내는 오류를 줄인다. 실제 문서를 근거로 답변하도록 유도하므로 사실과 다른 내용 생성 가능성이 낮아진다.
이처럼 RAG는 LLM의 강력한 언어 생성 능력에 신뢰할 수 있는 지식 검색 능력을 결합함으로써, 챗봇, 지식 Q&A, 고객지원, 검색 엔진 등 폭넓은 응용 분야에서 높은 정확도의 대화형 AI를 구현하는 데 활용된다.
RAG의 주요 구성 요소
RAG 파이프라인은 크게 임베딩 생성, 벡터 저장소, 검색(질의), LLM 생성의 단계로 나눌 수 있다.
문서 데이터셋(Document Dataset) 준비
우선 모델에 제공할 지식 소스를 준비해야 한다.
여기서 소스는 PDF, 마크다운, TXT, HTML 등 형식의 문서 모음일 수 있고, 데이터베이스나 위키 문서, 사내 저장소 등 구조화되지 않은 텍스트 데이터 전체를 의미한다.
- RAG의 효과는 결국 여기에 어떤 정보를 넣느냐에 달려 있으므로, 답변에 필요한 도메인 문서들을 수집해 둬야 한다.
- 또한 한 문서가 너무 길면 LLM 입력 한도를 넘지 않도록 청크(chunk) 단위로 자르는 전처리를 수행해야 한다. (예: 긴 문서를 의미 단위로 여러 조각 분할)
임베딩(Embeddings)
임베딩이란, 텍스트를 숫자 벡터로 표현한 것을 의미한다.
임베딩 모델(예: OpenAI의 Ada, SentenceTransformers의 모델 등)을 이용해 각 문서나 문서 조각을 고차원 벡터로 변환해둔다.
임베딩 벡터는 텍스트의 의미적 유사성을 수치화하여 나타내며, 비슷한 주제의 문서는 벡터 공간에서 가깝게 위치한다.
예를 들어 “인공지능 법규란?” 이라는 문장 임베딩은 내용이 유사한 다른 법률 문서 임베딩과 가까운 값을 가지게 된다.
이렇게 문서를 벡터로 바꾸어 두면, 이후 사용자의 질문과 유사한 내용의 문서를 벡터 거리 계산으로 효율적으로 찾을 수 있다.
이해하면 좋을 관련 내용: Word2Vec
[밑바닥부터 시작하는 딥러닝] word2vec
본 내용은 밑바닥부터 시작하는 딥러닝 2도서를 참고하여 작성되었습니다. 밑바닥부터 시작하는 딥러닝 2 - 예스24직접 구현하면서 배우는 본격 딥러닝 입문서 이번에는 순환 신경망과 자연어
dev.go-gradually.me
벡터 데이터베이스(Vector Store)
벡터 데이터베이스는 임베딩된 벡터들을 저장하고 유사도 검색을 수행하는 특화된 데이터베이스이다.
일반적인 키워드 검색과 달리, 임베딩 벡터 간의 코사인 유사도나 L2 거리를 기반으로 가장 가까운 항목을 찾는다.
이때, 보통 수만~수억 개의 벡터를 빠르게 검색하기 위해 Faiss, Chroma, Milvus 등의 벡터 DB를 사용한다.
벡터 DB는 대규모 벡터 연산에 최적화되어 실시간 검색에도 높은 성능을 보인다.
미리 구축된 벡터 인덱스 덕분에, RAG 시스템은 질의 시점에 즉각적으로 관련 정보를 찾을 수 있다.
검색기(Retriever)
Retriever란, 벡터 DB에서 질문에 해당하는 벡터와 가장 가까운 문서 벡터들을 찾아주는 모듈이다.
사용자의 질문 역시 동일한 임베딩 모델로 벡터화하여 벡터 DB에 질의하면, 가장 유사도가 높은 상위 k개의 문서 조각을 반환받게 된다.
이 때 임계값이나 re-ranker 등을 활용해 검색 정확도를 높일 수도 있다.
Retriever는 RAG의 핵심으로서, 올바른 문맥 추출을 책임지게 된다.
예를 들어 “제품 A의 보증 기간은 몇 년인가요?”라는 질문 벡터를 검색하면 제품 A 매뉴얼에서 보증 기간이 언급된 부분이 상위 결과로 반환되는 식이다.
생성 모델(Generator, LLM)
검색된 문맥과 사용자 질문을 결합하여 최종 답변을 생성하는 역할을 수행한다.
OpenAI GPT-3.5/4, GPT-NeoX, Llama 2 등의 사전훈련 언어모델을 사용한다.
- 프롬프트에는 일반적으로 사용자 질문과 검색된 문서 내용이 함께 들어간다.
- 모델은 이 프롬프트를 보고, 질문에 대한 답을 문서 내용을 활용하여 생성한다.
- 이때 “주어진 정보만 활용하라”는 지시를 내리면 모델이 추측으로 엉뚱한 말을 하는 것을 방지할 수 있다.
생성 결과는 최종적으로 사용자에게 전달되는 응답 텍스트이며, 필요에 따라 답변과 함께 출처 링크나 증거 문장을 포함시킬 수도 있다.
요약하면, RAG 시스템은 사전 지식베이스 구성(임베딩 & 벡터DB)과 질문 시점 검색 및 생성의 두 흐름으로 이뤄지게 된다.
지금부터 이러한 구성 요소들을 직접 구현하며 기본 개념을 실습해보자.
1단계: 문서 데이터셋 준비하기
첫 번째 단계는 데이터 수집 및 로딩이다.
예제로 로컬에 몇 개의 마크다운 파일이 있다고 가정해보겠습니다.
필자의 경우, 본인의 이력서를 활용했다.
우선 파이썬으로 해당 파일들을 열어 내용을 불러오자.
파일 형식은 필요에 따라 .md 외에 .txt나 .pdf 등으로 변경 가능하다 (이 경우 다른 추가적인 라이브러리가 필요할 수 있다).
# 예시 문서 파일 목록
docs = ["./docs/doc1.md", "./docs/doc2.md", "./docs/doc3.md"]
documents = []
for fname in docs:
with open(fname, 'r', encoding='utf-8') as f:
text = f.read()
documents.append(text)
print(f"문서 개수: {len(documents)}")
print(f"첫 번째 문서 내용 일부:\n{documents[0][:200]}...")
위 코드에서는 docs 리스트에 지정된 경로의 파일을 모두 읽어 documents 리스트에 텍스트로 저장한다.
문서가 길 경우 텍스트 분할 작업이 필요해진다.
예를 들어 10,000자 분량의 긴 문서는 500자 내외의 단락으로 쪼개어 documents 리스트에 추가하는 편이 좋다.
이는 뒤에서 임베딩 생성시 토큰 제한을 피하고, 검색 효율을 높이기 위함이다. 간단히 text[i:i+500] 처럼 잘라서 리스트에 넣거나, LangChain의 RecursiveCharacterTextSplitter 등을 활용할 수 있다.
2단계: 임베딩 생성하기
이제 문서 텍스트를 벡터 표현(embedding)으로 변환해보자.
임베딩을 생성하는 방법은 여러 가지가 있는데, 여기서는 간단하게 SentenceTransformers 라이브러리를 사용해 볼 것이다.
Hugging Face의 all-MiniLM-L6-v2 모델을 다운로드하여 사용해볼 것이다.
먼저 SentenceTransformers를 설치하고 모델을 불러오자.
!pip install -q sentence-transformers # 필요한 라이브러리 설치
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
# 준비된 documents 리스트를 임베딩 벡터로 변환
embeddings = model.encode(documents)
print(f"임베딩 벡터 shape: {embeddings.shape}")
위 코드가 실행되면 embeddings에는 각 문서에 대한 384차원짜리 벡터들이 NumPy 배열 형태로 들어간다.
all-MiniLM-L6-v2 모델의 출력 차원이 384이므로 (모델마다 다름), 문서 3개라면 embeddings.shape는 (3, 384)처럼 나올 것이다.
벡터 하나를 출력해보면 사람에게는 무의미한 숫자들의 나열이지만, 이 벡터 공간에서의 위치가 해당 텍스트의 의미를 나타내게 된다.

임베딩 생성에는 OpenAI의 API를 사용할 수도 있다.
예를 들어 OpenAI의 text-embedding-ada-002 모델은 1536차원의 임베딩을 돌려준다.
OpenAI API를 쓰려면 사전에 openai 라이브러리를 설치하고 API 키를 설정한 후 openai.Embedding.create(input=text) 형태로 호출하면 된다.
하지만 OpenAI API는 비용이 발생하고 인터넷 연결이 필요하므로, 본 튜토리얼에서는 로컬 임베딩 방법으로 진행할 것이다.
궁금하면 직접 해보기 - 유사도 측정
임베딩 벡터들 간의 유사도를 직접 계산해보자.
예를 들어 embeddings[0]과 embeddings[1] 두 벡터의 코사인 유사도를 구해보는 것도 좋다.
벡터 유사도는 numpy.dot(vec1, vec2) / (||vec1||*||vec2||) 로 계산할 수 있다.
이 값을 출력하여, 내용이 비슷한 문서끼리는 높은 유사도를 가지는지 직접 두 눈으로 확인해보자.
3단계: 벡터 DB에 임베딩 저장하고 질의하기
임베딩을 얻었으므로, 이를 벡터 데이터베이스(또는 유사도 검색 엔진)에 저장할 것이다.
여러 벡터 DB 중 여기서는 간단히 Faiss 라이브러리를 이용한다.
Faiss는 Facebook AI Research에서 개발한 벡터 검색 라이브러리로, 메모리상에 인덱스를 만들고 고속 최근접 이웃 검색(KNN)을 지원한다.
대용량 데이터의 경우 Milvus, ElasticSearch+vector plugin, Pinecone 등의 솔루션을 고려해야 하지만, 소규모 실습에는 Faiss가 적합하다.
먼저 Faiss를 설치하고 인덱스를 생성해보자.
!pip install -q faiss-cpu
import faiss
import numpy as np
# 임베딩 배열을 float32형으로 변환 (Faiss는 float32를 사용)
embedding_dim = embeddings.shape[1] # 벡터 차원 (예: 384)
index = faiss.IndexFlatL2(embedding_dim) # L2 거리 기반의 플랫 인덱스 생성
index.add(np.array(embeddings, dtype='float32')) # 우리의 임베딩들을 인덱스에 추가
print(f"현재 저장된 벡터 개수: {index.ntotal}")
Faiss의 IndexFlatL2는 가장 단순한 완전 탐색 인덱스로, L2(유클리드)거리를 기준으로 K-최근접 이웃을 찾아준다.
index.add()로 우리의 임베딩 배열을 추가했고, index.ntotal로 인덱스에 벡터가 몇 개 저장되었는지 확인했다.
이제 이 인덱스를 활용하여 질의 벡터와 가장 가까운 문서를 찾아볼 수 있다.
예시 질의를 하나 만들자.
나의 경우 Pinit/SuperBoad/FeedHanjum 각 프로젝트 중 하나에 대한 질문을 던졌다.
핀잇은 어떤 서비스인가요?
이 질문에 답하려면 Pinit 문서 내용이 필요할 것이다. 임베딩 공간에서 질문 벡터를 생성하여 검색해볼 것이다.
question = "핀잇은 어떤 서비스인가요?"
q_vec = model.encode([question]) # 질문도 동일한 모델로 임베딩
# Faiss를 통해 질문 벡터와 가장 가까운 문서 벡터를 1개 찾는다.
D, I = index.search(np.array(q_vec, dtype='float32'), k=1)
nearest_idx = I[0][0]
print(f"질문에 가장 관련 있는 문서 인덱스: {nearest_idx}")
print(f"해당 문서 내용 (일부분):\n{documents[nearest_idx][:300]}...")

위 코드에서 index.search(query_vector, k)는 거리(D)와 이웃 인덱스(I)를 반환한다.
k=1로 설정했으므로 가장 가까운 문서 하나를 찾은 것이고, nearest_idx에 그 문서의 인덱스를 담았다.
만약 상위 3개의 문서를 보고 싶다면 k=3으로 지정하면 된다.
콘솔 출력 결과로 선택된 문서를 확인하고, 질문에 알맞은 내용인지 검증해보자.
추가적으로, 문서 내용에 기반한 사실을 묻는 질문들 몇 가지를 준비하여, 각 질문에 대해 index.search로 나온 상위 결과가 관련 문서인지 확인해보자.
만약 전혀 관련 없는 결과가 나온다면 임베딩 모델이 해당 영역에 대해 제대로 학습되지 않았을 가능성이 있다.
이런 경우 sentence-transformers의 다른 모델이나 도메인 특화 임베딩 모델을 사용하는 것을 고려해야 한다.
벡터 검색 품질을 높이기 위해 코사인 유사도를 사용할 수도 있다.
Faiss에서는 코사인 유사도를 쓰려면 벡터를 정규화한 후 Inner Product 지표를 사용하면 된다.
여기서는 간단히 L2 거리로 충분하지만, 필요하다면 faiss.normalize_L2() 함수를 활용해보는 것도 좋다.
의문점
그럼 다음과 같은 의문점이 들 수도 있다.
이거 그냥 프롬프트로 LLM에 직접 넣어도 되지 않나?
맞다.
사실 핵심은, KNN/벡터 검색 알고리즘을 이용해 방대한 문서 내 필요한 문서만을 찾고, 해당 문서만을 이용해 LLM 사용 시의 토큰 비용을 줄이는 데 있다.
즉, RAG의 핵심 목표는
- 문서가 크고(컨텍스트 한계 초과 또는 근접),
- 질문이 문서 일부만 필요하고,
- 최신 정보 반영이 필요하고,
- 근거 제시(어느 문단을 기반으로 답했는지)가 중요할 때
가장 강력한 도구가 될 수 있는것이다.
4단계: 검색된 문맥을 LLM에 전달하여 답변 생성하기
마지막으로, LLM을 활용한 답변 생성 단계이다.
지금까지 우리는 문서 -> 임베딩 -> 벡터DB 저장 -> 질의 -> 관련 문서 추출 순서를 따라왔고, 특정 질문에 대해 관련 문서 조각(들)을 얻을 수 있게 되었다.
이제 이 문서 조각들을 사용하여 최종 답변을 생성해보겠다.
LLM으로는 OpenAI의 GPT-3.5 Turbo 모델을 사용한다고 가정할 것이다. OpenAI API 키를 보유하고 있다 가정하고, 프로젝트를 진행해보겠다.
우선 OpenAI API로 답변 생성하는 예시 코드이다.
!pip install -q openai
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
)
context = documents[nearest_idx]
prompt = f"다음 문서를 참고하여 질문에 답하세요:\n'''{context}'''\n질문: {question}\n답변:"
response = client.responses.create(
model="gpt-4o",
input=prompt,
max_output_tokens=100,
temperature=0.2,
top_p=1.0,
)
answer = response.output[0].content[0].text
print("답변:", answer)
환경변수로 OPENAI_API_KEY 를 세팅해두고 시작하자.
위 프롬프트에서는 문맥(context)으로 전달할 문서를 triple quotes(''' ''')로 감싸 첨부하고, 그 아래에 실제 질문을 적은 후 답변:으로 끝냈다.
이런 식으로 모델에게 주어진 문서를 참고해서 답하라고 지시하면, 모델은 문서 내용을 활용하여 답변을 생성하게 된다.
temperature=0.2로 설정하여 출력의 랜덤성을 낮추고 (더 확정적인 답변을 유도), max_output_tokens를 적절히 제한했다.
이처럼 검색된 문맥을 프롬프트에 포함함으로써, LLM이 원래 학습되지 않았거나 몰랐던 정보도 활용하여 답을 구성할 수 있게 된다.
이것이 RAG의 핵심 효과이다.
우리의 예시에서는 간단히 문서 1개의 내용을 첨부했지만, 만약 상위 3개의 문서를 썼다면 모두 합쳐 prompt에 넣을 수도 있다.
다만 너무 많은 내용을 한꺼번에 넣으면 모델이 프롬프트 입력 한도를 초과할 수 있으니, 중요도가 높은 순으로 몇 개만 고르거나, 요약된 형태로 제공하는 것이 좋다.
5단계: 결과 평가와 고찰 (Evaluation)
RAG 파이프라인을 구축했다면, 결과가 제대로 나오는지 평가하는 과정도 중요하다.
Backend Engineering 만큼이나, AI Agent Engineering에도 Observability는 중요하다.
초보 단계에서는 정량적인 척도보다는 아래와 같은 질적인 측면을 중심으로 확인해 보는 것이 좋다.
검색된 문서의 적절성
Retriever가 반환한 문서 조각들이 질문과 관련성이 높은지 살펴보자.
만약 엉뚱한 문서가 결과로 나왔다면, 임베딩 모델을 바꾸거나 문서 전처리(예: 다른 텍스트 분할 전략)를 조정해야 할 수 있다.
이상적인 RAG 동작에서는 정답의 근거가 되는 문서가 상위에 위치해야 한다.
LLM 응답의 정확성
생성된 답변이 실제 문서 내용과 부합하는지 검사하자.
RAG를 썼다면 답변 내 정보는 최대한 문서에서 나온 사실에 기반해야 한다.
만약 모델이 문서에 없는 정보를 언급한다면, 그것이 바로 환각이다.
이런 경우 프롬프트에 더 강한 제약을 주거나, 검색된 문맥이 충분한지 (혹은 너무 많아서 혼란을 준 건 아닌지) 살펴볼 필요가 있다.
출처 표기 및 신뢰도
가능하면 LLM의 답변에 어떤 문서에 근거했는지 출처를 표시하도록 할 수도 있다.
예를 들어 *"~~ (출처: 문서1)"* 형식으로 알려주면 사용자 신뢰도가 높아진다.
이는 LLM이 잘 수행하지 못할 경우, 코드 단계에서 답변과 함께 문서 제목이나 링크를 합쳐 후처리하는 것도 고려해볼 수 있다.
모델 튜닝
LLM의 응답 품질을 높이기 위해 프롬프트 설계를 바꿔볼 수 있다.
예컨대, 시스템 메시지에 "당신은 친절한 전문가입니다..." 등의 톤 조절을 하거나, 중요 단어를 하이라이트해서 포함하는 등의 기법이 있다.
또한 상위 문서 여러 개를 주었을 때 답변이 두루뭉술해진다면, 차라리 한두 개만 주도록 조절해볼 수도 있다.
평가 단계에서는 사용자 피드백을 받거나, 몇 가지 테스트 질문에 대한 정답지를 가지고 비교하면서 RAG 시스템을 개선해 나가면 된다.
RAG 자체는 정보를 추가하는 역할이므로, 최종 답변의 품질은 LLM 모델 성능에도 크게 의존한다.
작은 모델일수록 문맥 활용 능력이 떨어질 수 있고, 큰 모델일수록 안정성이 올라간다.
따라서 예산과 서비스 목적에 맞는 모델을 선택하는 것도 중요하다.
마지막으로, RAG로 많은 문제를 완화할 수 있지만 완벽한 해결책은 아니라는 점도 염두에 두어야 한다.
- 예를 들어 검색된 문서들이 애매하거나 틀린 정보를 담고 있다면, LLM은 여전히 그 범위 내에서 잘못된 답변을 만들어낼 수 있다.
- 지식베이스의 품질 관리도 필수적이다. 새로운 데이터가 생기면 주기적으로 벡터 DB를 업데이트하고, 오래되거나 잘못된 문서는 제거해야 한다.
- 이러한 유지보수까지 포함해서 RAG 파이프라인이 지속적으로 정확한 정보를 제공하도록 설계해야 한다.
특히, 현재 랭그래프까지 나아가는 과정에서, 다음 세가지를 집중하는 것이 중요하다.
- 검색 결과를 그대로 붙이는 게 아니라, 필터링/랭킹/컨텍스트 구성이 중요하다는 걸 경험
- 인용/근거 문장을 함께 반환하거나, 답변에 사용한 근거를 추적하는 방식에 관심
- 실패 케이스(헛검색, 컨텍스트 과다, 질문-문서 불일치)를 일부라도 재현
이 경험이 있어야, 나중에 LangGraph에서 “검색 재시도”, “쿼리 재작성”, “컨텍스트 축약” 같은 노드를 설계할 수 있다.
출처