TIL/[TIL]

[TIL]RAG와 OpenAI Embeddings 기반 문서 검색 흐름

namerong 2026. 7. 21. 15:00

이번 학습에서는 RAG의 기본 개념과 문서 기반 검색 흐름을 정리했다.
RAG는 LLM이 모델 내부 지식만으로 답변하는 한계를 줄이기 위해, 외부 문서를 검색하고 그 내용을 답변 근거로 사용하는 방식이다.

1. RAG란?

RAG는 Retrieval-Augmented Generation의 줄임말로, 검색 증강 생성이라고 한다.

일반 LLM은 질문을 받으면 모델이 이미 알고 있는 내용으로 답변한다.

질문 -> LLM -> 모델이 알고 있는 내용으로 답변

RAG는 질문이 들어오면 먼저 관련 문서를 검색하고, 검색된 문서를 질문과 함께 LLM에 전달한다.

질문 -> 관련 문서 검색 -> 질문 + 검색 문서 -> LLM -> 문서 기반 답변

즉, RAG의 핵심은 모델이 기억에만 의존하지 않고 외부 문서를 근거로 답변하게 만드는 것이다.


2. RAG가 필요한 이유

LLM은 많은 내용을 알고 있지만, 모든 것을 정확히 아는 것은 아니다.

특히 다음과 같은 한계가 있다.

  • 최신 정보를 모를 수 있다.
  • 회사 내부 문서나 수업 자료를 알 수 없다.
  • 모르는 내용을 그럴듯하게 답할 수 있다.
  • 답변의 근거가 불명확할 수 있다.

RAG는 이런 문제를 줄이기 위해 사용한다.
예를 들어 환불 규정, 실습실 운영 시간, API 키 저장 위치처럼 특정 문서에 있는 내용을 답해야 한다면, 모델의 기억보다 실제 문서를 검색해서 답하는 편이 더 정확하다.


3. 일반 LLM과 RAG 차이

구분 일반 LLM RAG
답변 근거 모델 내부 지식 검색된 외부 문서
최신 정보 반영 어려움 문서만 최신화하면 가능
내부 문서 활용 어려움 가능
출처 표시 불명확 metadata로 출처 표시 가능
적합한 경우 일반 설명, 요약, 대화 문서 기반 QA, 매뉴얼 검색, 회사 지식 검색

RAG는 모델을 다시 학습시키는 것이 아니라, 답변 전에 필요한 자료를 찾아 프롬프트에 넣어주는 방식이다.


4. RAG 전체 흐름

RAG는 보통 다음 순서로 동작한다.

문서 로드
-> Document 생성
-> Text Splitter로 chunk 분리
-> chunk를 embedding vector로 변환
-> 질문도 embedding vector로 변환
-> 질문과 가까운 chunk 검색
-> 검색 결과를 Context로 구성
-> 질문 + Context를 LLM에 전달
-> 문서 기반 답변 생성

용어별 역할은 다음과 같다.

용어 역할
Loader TXT, PDF, 웹 페이지 같은 원본 파일 읽기
Document 본문과 metadata를 함께 가진 객체
Metadata 파일명, 출처, 페이지 번호 같은 부가 정보
Text Splitter 긴 문서를 검색 가능한 작은 단위로 분리
Chunk 분리된 문서 조각
Embedding 텍스트 의미를 숫자 벡터로 변환
VectorStore 벡터와 원문 chunk를 저장하는 검색 저장소
Retriever 질문과 가까운 chunk를 찾아오는 역할
Context LLM에게 전달할 검색 근거
LLM Context를 읽고 최종 답변 생성

5. Loader와 Metadata

실습에서는 data 폴더 안의 .txt 파일을 읽어서 문서 목록을 만들었다.

documents.append({
    "source": path.name,
    "text": text
})

여기서 source는 metadata 역할을 한다.
metadata는 본문 자체는 아니지만, 문서를 설명하는 정보다.

예를 들면 다음과 같다.

  • 파일명
  • 출처
  • 페이지 번호
  • URL
  • 작성일
  • 카테고리

RAG 답변에서 “어떤 문서를 근거로 답했는지” 표시하려면 metadata가 중요하다.


6. Text Splitter와 Chunk

긴 문서를 그대로 검색하면 정확도가 떨어질 수 있다.
그래서 문서를 작은 단위인 chunk로 나눈다.

chunk_size는 한 chunk의 최대 크기를 의미한다.

  • 너무 작으면 문맥이 끊긴다.
  • 너무 크면 검색 결과가 뭉뚱그려질 수 있다.

overlap은 앞 chunk의 일부를 다음 chunk에도 겹쳐 넣는 방식이다.

chunk 1: A B C
chunk 2: C D E

이렇게 하면 문장 경계에서 중요한 정보가 잘리는 문제를 줄일 수 있다.

실습에서는 여러 설정을 비교했다.

settings = [
    (120, 0),
    (120, 1),
    (260, 1),
    (50, 0),
    (50, 1)
]

이를 통해 chunk 크기와 overlap 설정에 따라 검색 결과가 달라질 수 있음을 확인했다.


7. Embedding과 Vector

Embedding은 텍스트를 숫자 벡터로 바꾸는 과정이다.
컴퓨터는 문장의 의미를 그대로 비교할 수 없기 때문에, 문장과 문서를 숫자 배열로 바꿔야 한다.

실습에서는 두 가지 방식이 있었다.

첫 번째는 단어 빈도 기반 벡터화다.

Counter(tokenize(text))

이 방식은 구조를 이해하기 쉽다.
단어가 얼마나 겹치는지를 기준으로 유사도를 계산한다.

두 번째는 OpenAI Embeddings API를 사용하는 방식이다.

response = client.embeddings.create(
    model="text-embedding-3-small",
    input=texts,
)

이 방식은 단순 단어 겹침보다 의미 기반 검색에 더 가깝다.


8. Retrieval과 top_k

Retrieval은 질문과 가장 관련 있는 chunk를 찾는 과정이다.

질문을 벡터로 바꾸고, 문서 chunk도 벡터로 바꾼 뒤 코사인 유사도를 계산한다.

score = cosine_similarity(question_vector, item["vector"])

검색 결과는 점수가 높은 순서대로 정렬한다.

results.sort(key=lambda item: item["score"], reverse=True)

top_k는 상위 몇 개의 chunk를 가져올지 정하는 값이다.

retrieve(index, question, top_k=3)

top_k가 너무 작으면 필요한 근거를 놓칠 수 있고, 너무 크면 불필요한 문서까지 섞일 수 있다.


9. 일반 RAG와 Hybrid RAG

일반 RAG는 보통 벡터 유사도 검색을 중심으로 관련 문서를 찾는다.

Hybrid RAG는 벡터 검색과 키워드 검색을 함께 사용한다.

방식 특징
벡터 검색 의미가 비슷한 문서를 찾는 데 강함
키워드 검색 정확한 단어, 코드, 고유명사 검색에 강함
Hybrid RAG 의미 검색과 키워드 검색을 함께 사용

예를 들어 “무선 인터넷이 안 된다”와 “와이파이 연결 문제”는 표현이 달라도 의미가 비슷하므로 벡터 검색이 유리하다.
반면 OPENAI_API_KEY, API-200 같은 정확한 문자열은 키워드 검색이 더 유리할 수 있다.


10. 검색 품질 평가

RAG에서는 검색이 되는지만 보는 것이 아니라, 원하는 문서가 상위 결과에 들어오는지도 확인해야 한다.

실습에서는 질문과 기대 문서를 미리 정해두고 검색 성공 여부를 확인했다.

TEST_CASES = [
    ("수강 취소 환불 기준은 무엇인가요?", "center_policy.txt"),
    ("Text Splitter와 chunk overlap은 왜 필요한가요?", "course_guide.txt")
]

그리고 설정별로 Hit@k를 계산했다.

Hit@1: 상위 1개 안에 정답 문서가 있는가
Hit@2: 상위 2개 안에 정답 문서가 있는가

이 과정은 RAG 품질을 개선할 때 중요하다.
chunk size, overlap, top_k 설정이 검색 정확도에 직접 영향을 주기 때문이다.


11. OpenAI Embeddings RAG 실습

추가 실습에서는 OpenAI Embeddings API를 사용해 질문과 문서 chunk를 벡터로 변환하고, 가장 가까운 chunk를 검색하는 구조를 작성했다.

전체 흐름은 다음과 같다.

문서 파일 읽기
-> 문단 기준으로 chunk 분리
-> 질문 + chunk 목록을 Embeddings API에 전달
-> 질문 embedding과 chunk embedding 분리
-> 코사인 유사도 계산
-> score가 높은 chunk를 top_k만큼 출력

문서를 나누는 함수는 긴 텍스트를 일정 글자 수 기준으로 chunk로 나눈다.

def split_text(text: str, max_chars: int = 180) -> list[str]:
    ...

질문과 chunk를 한 번에 embedding으로 변환한다.

embeddings = create_embeddings([question] + chunks)
question_embedding = embeddings[0]
chunk_embeddings = embeddings[1:]

그 다음 질문 벡터와 각 chunk 벡터의 코사인 유사도를 계산한다.

scored_chunks = [
    (cosine_similarity(question_embedding, chunk_embedding), chunk)
    for chunk, chunk_embedding in zip(chunks, chunk_embeddings)
]

마지막으로 점수가 높은 순서대로 정렬해 상위 chunk를 가져온다.

return sorted(scored_chunks, reverse=True)[:top_k]

12. argparse로 실행 옵션 받기

추가 커밋에서는 argparse를 사용해 실행 시 옵션을 받을 수 있도록 했다.

parser.add_argument("--document", default=str(DEFAULT_DOCUMENT), help="검색할 문서")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="검색 질문")
parser.add_argument("--top-k", type=int, default=2, help="가져올 chunk개수")

이렇게 하면 코드 안의 값을 직접 수정하지 않고도 터미널에서 질문, 문서, 검색 개수를 바꿔 테스트할 수 있다.

예시 흐름은 다음과 같다.

python 06_openai_embeddings_rag.py --question "FastAPI에서 Swagger는 왜 필요한가?" --top-k 2

또한 .env에서 OPENAI_API_KEY를 읽도록 load_dotenv()를 추가했고, 문서 파일이나 API Key가 없으면 실행을 중단하도록 처리했다.

if not document_path.exists():
    raise SystemExit(f"문서 파일이 없습니다: {document_path}")

if not os.getenv("OPENAI_API_KEY"):
    raise SystemExit("OPENAI_API_KEY가 없습니다.")

이 부분은 RAG 코드가 실제로 실행 가능한 작은 CLI 실습 형태가 되었다는 점에서 의미가 있다.


13. LangChain과 RAG

LangChain은 LLM, 프롬프트, 문서 로더, 검색기, 파서 등을 연결해주는 오픈소스 오케스트레이션 프레임워크로 볼 수 있다.

RAG에서는 다음 작업들이 이어진다.

Loader
-> Text Splitter
-> Embedding
-> VectorStore
-> Retriever
-> Prompt
-> LLM
-> Output

즉, LangChain은 RAG에 필요한 여러 단계를 조립하고 실행 흐름을 관리하기 쉽게 도와주는 도구다.


14. 핵심 정리

  1. RAG는 외부 문서를 검색해 LLM 답변의 근거로 사용하는 방식이다.
  2. 일반 LLM은 내부 지식으로 답하지만, RAG는 검색된 문서를 함께 보고 답한다.
  3. Loader는 파일을 읽고, Document는 본문과 metadata를 함께 가진다.
  4. Metadata는 파일명, 출처, 페이지 번호처럼 문서를 설명하는 정보다.
  5. Text Splitter는 긴 문서를 chunk 단위로 나눈다.
  6. Chunk size가 너무 작으면 문맥이 끊기고, 너무 크면 검색 정확도가 떨어질 수 있다.
  7. Overlap은 chunk 사이 문맥 손실을 줄이기 위해 일부 내용을 겹치는 방식이다.
  8. Embedding은 텍스트 의미를 숫자 벡터로 바꾸는 과정이다.
  9. Retriever는 질문과 가까운 chunk를 찾아오며, top_k로 가져올 개수를 정한다.
  10. OpenAI Embeddings API를 사용하면 단어 빈도보다 의미 기반 검색에 더 가까운 RAG를 만들 수 있다.
  11. argparse를 사용하면 문서, 질문, top_k 값을 실행 시점에 바꿔가며 테스트할 수 있다.
  12. RAG 품질은 chunk 설정, embedding 품질, retrieval 방식에 따라 달라진다.
  13. Hybrid RAG는 벡터 검색과 키워드 검색을 함께 사용하는 방식이다.
  14. 지식 주입이 목적이면 Fine-tuning보다 RAG가 더 적합한 경우가 많다.